Learning Ada
Learning Ada
Release 2024-06
Various authors
I Introduction to Ada 3
1 Introduction 7
1.1 History . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Ada today . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3 Philosophy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.4 SPARK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2 Imperative language 9
2.1 Hello world . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Imperative language - If/Then/Else . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Imperative language - Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.1 For loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.2 Bare loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.3 While loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.4 Imperative language - Case statement . . . . . . . . . . . . . . . . . . . . . . . . 16
2.5 Imperative language - Declarative regions . . . . . . . . . . . . . . . . . . . . . . 18
2.6 Imperative language - conditional expressions . . . . . . . . . . . . . . . . . . . 20
2.6.1 If expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6.2 Case expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3 Subprograms 23
3.1 Subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.1 Subprogram calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.1.2 Nested subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.1.3 Function calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.2 Parameter modes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.3 Subprogram calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.3.1 In parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.3.2 In out parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.3.3 Out parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3.4 Forward declaration of subprograms . . . . . . . . . . . . . . . . . . . . . 31
3.4 Renaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
4 Modular programming 35
4.1 Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 Using a package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.3 Package body . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.4 Child packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.4.1 Child of a child package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.4.2 Multiple children . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.4.3 Visibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.5 Renaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
i
5.2 Integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.2.1 Operational semantics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.3 Unsigned types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.4 Enumerations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.5 Floating-point types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.1 Basic properties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.2 Precision of floating-point types . . . . . . . . . . . . . . . . . . . . . . . . 53
5.5.3 Range of floating-point types . . . . . . . . . . . . . . . . . . . . . . . . . 54
5.6 Strong typing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.7 Derived types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.8 Subtypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
5.8.1 Subtypes as type aliases . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
6 Records 65
6.1 Record type declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.2 Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.3 Component selection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.4 Renaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7 Arrays 71
7.1 Array type declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
7.2 Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
7.3 Simpler array declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.4 Range attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.5 Unconstrained arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
7.6 Predefined array type: String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
7.7 Restrictions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
7.8 Returning unconstrained arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
7.9 Declaring arrays (2) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
7.10 Array slices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
7.11 Renaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
12 Privacy 113
12.1 Basic encapsulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
12.2 Abstract data types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
12.3 Limited types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
12.4 Child packages & privacy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
ii
13 Generics 123
13.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
13.2 Formal type declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
13.3 Formal object declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
13.4 Generic body definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
13.5 Generic instantiation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
13.6 Generic packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
13.7 Formal subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
13.8 Example: I/O instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
13.9 Example: ADTs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
13.10Example: Swap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
13.11Example: Reversing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
13.12Example: Test application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
14 Exceptions 143
14.1 Exception declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
14.2 Raising an exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
14.3 Handling an exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
14.4 Predefined exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
15 Tasking 147
15.1 Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
15.1.1 Simple task . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
15.1.2 Simple synchronization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
15.1.3 Delay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
15.1.4 Synchronization: rendezvous . . . . . . . . . . . . . . . . . . . . . . . . . 151
15.1.5 Select loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
15.1.6 Cycling tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
15.2 Protected objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
15.2.1 Simple object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
15.2.2 Entries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
15.3 Task and protected types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
15.3.1 Task types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
15.3.2 Protected types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
iii
18.7 Classwide access types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
24 Appendices 269
24.1 Appendix A: Generic Formal Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
24.1.1 Indefinite version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
24.2 Appendix B: Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
iv
25.1.4 Incomplete types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
25.1.5 Type view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
25.1.6 Type conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314
25.1.7 Qualified Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
25.1.8 Default initial values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 334
25.1.9 Deferred Constants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
25.1.10User-defined literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
25.2 Types and Representation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
25.2.1 Enumeration Representation Clauses . . . . . . . . . . . . . . . . . . . . . 347
25.2.2 Data Representation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
25.2.3 Record Representation and storage clauses . . . . . . . . . . . . . . . . . 367
25.2.4 Changing Data Representation . . . . . . . . . . . . . . . . . . . . . . . . 376
25.2.5 Valid Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
25.2.6 Unchecked Union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
25.2.7 Shared variable control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
25.2.8 Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
25.2.9 Discarding names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
25.3 Records . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
25.3.1 Default Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
25.3.2 Mutually dependent types . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
25.3.3 Null records . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
25.3.4 Per-Object Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
25.4 Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
25.4.1 Container Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
25.4.2 Record aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
25.4.3 Full coverage rules for Aggregates . . . . . . . . . . . . . . . . . . . . . . 448
25.4.4 Array aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
25.4.5 Extension Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470
25.4.6 Delta Aggregates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
25.5 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
25.5.1 Unconstrained Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484
25.5.2 Multidimensional Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . 486
25.6 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
25.6.1 Wide and Wide-Wide Strings . . . . . . . . . . . . . . . . . . . . . . . . . . 495
25.6.2 String Encoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
25.6.3 Image attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 512
25.6.4 Put_Image aspect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519
25.6.5 Universal text buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
25.7 Numerics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 529
25.7.1 Modular Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 529
25.7.2 Numeric Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535
25.7.3 Floating-Point Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
25.7.4 Fixed-Point Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 555
25.7.5 Big Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 563
v
26.2.6 Extended return statement . . . . . . . . . . . . . . . . . . . . . . . . . . . 615
26.3 Subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 618
26.3.1 Parameter Modes and Associations . . . . . . . . . . . . . . . . . . . . . . 618
26.3.2 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 631
26.3.3 Expression functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 637
26.3.4 Overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 640
26.3.5 Operator Overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645
26.3.6 Operator Overriding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645
26.3.7 Nonreturning procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
26.3.8 Inline subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651
26.3.9 Null Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653
26.4 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
26.4.1 Asserts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
26.4.2 Assertion policies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 658
26.4.3 Checks and exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 661
26.4.4 Ada.Exceptions package . . . . . . . . . . . . . . . . . . . . . . . . . . . 674
26.4.5 Exception renaming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 682
26.4.6 Out and Uninitialized . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 684
26.4.7 Suppressing checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 688
vi
28.2.7 User-Defined References . . . . . . . . . . . . . . . . . . . . . . . . . . . . 886
28.2.8 Anonymous Access Types and Accessibility Rules . . . . . . . . . . . . . 896
28.2.9 Anonymous Access-To-Subprograms . . . . . . . . . . . . . . . . . . . . . 901
28.2.10Accessibility Rules and Anonymous Access-To-Subprograms . . . . . . . 910
28.3 Limited Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 920
28.3.1 Assignment and equality . . . . . . . . . . . . . . . . . . . . . . . . . . . . 920
28.3.2 Limited private types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 928
28.3.3 Explicitly limited types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 936
28.3.4 Subtypes of Limited Types . . . . . . . . . . . . . . . . . . . . . . . . . . . 938
28.3.5 Deriving from limited types . . . . . . . . . . . . . . . . . . . . . . . . . . 939
28.3.6 Immutably Limited Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . 946
28.3.7 Limited Types and Discriminants . . . . . . . . . . . . . . . . . . . . . . . 949
28.3.8 Record components of limited type . . . . . . . . . . . . . . . . . . . . . . 949
28.3.9 Limited types and aggregates . . . . . . . . . . . . . . . . . . . . . . . . . 950
28.3.10Constructor functions for limited types . . . . . . . . . . . . . . . . . . . . 954
28.3.11Return objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 958
28.3.12Building objects from constructors . . . . . . . . . . . . . . . . . . . . . . 964
28.3.13Limited types as parameter . . . . . . . . . . . . . . . . . . . . . . . . . . 967
vii
30.5.1 Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1005
30.5.2 Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1006
30.5.3 Example #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1007
30.5.4 Example #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1008
30.5.5 Example #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1010
30.5.6 Example #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1011
30.5.7 Example #7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1012
30.5.8 Example #8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1013
30.5.9 Example #9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1015
30.5.10Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1016
viii
32.10.10
Example #10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1071
ix
37.2 Memory-Mapped Devices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1178
37.3 Dynamic Address Conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1185
37.4 Address Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1188
40 Conclusion 1227
x
49 Enumeration representation 1261
49.1 Literal positions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1261
49.2 Representation values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1262
49.3 Before Ada 2022 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1263
49.4 References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1264
53 Basics 1279
58 Packages 1307
58.1 Declaration Protection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1307
58.2 Hierarchical Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1308
58.3 Using Entities from Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1308
60 Generics 1321
60.1 Generic Subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1321
xi
60.2 Generic Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1322
60.3 Generic Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1323
61 Exceptions 1325
61.1 Standard Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1325
61.2 Custom Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1326
62 Concurrency 1327
62.1 Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1327
62.2 Rendezvous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1330
62.3 Selective Rendezvous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1332
62.4 Protected Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1333
64 Conclusion 1341
65 References 1343
xii
68.2 Tasks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1415
68.3 Rendezvous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1418
68.4 Selective Rendezvous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1420
68.5 Protected Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1422
68.6 Ravenscar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1426
xiii
73 Performance considerations 1555
73.1 Overall expectations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1555
73.2 Switches and optimizations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1555
73.2.1 Optimizations levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1555
73.2.2 Inlining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1556
73.3 Checks and assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1557
73.3.1 Checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1557
73.3.2 Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1561
73.4 Dynamic vs. static structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1561
73.5 Pointers vs. data copies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1563
73.5.1 Function returns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1566
75 Conclusion 1573
xiv
79.3.3 Avoiding Complex Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1626
79.3.4 Avoiding the Dangling Else Issue . . . . . . . . . . . . . . . . . . . . . . . 1627
85 Conclusion 1675
86 References 1677
86.1 About MISRA C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1677
86.2 About SPARK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1678
86.3 About MISRA C and SPARK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1678
88 GPRbuild 1687
88.1 Basic commands . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1687
88.2 Project files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1687
88.2.1 Basic structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1687
88.2.2 Customization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1688
88.3 Project dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1689
88.3.1 Simple dependency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1689
88.3.2 Dependencies to dynamic libraries . . . . . . . . . . . . . . . . . . . . . . 1691
88.4 Configuration pragma files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1691
88.5 Configuration packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1692
xv
89.2 Creating projects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1695
89.3 Building . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1696
89.4 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1696
89.4.1 Debug information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1696
89.4.2 Improving main application . . . . . . . . . . . . . . . . . . . . . . . . . . 1697
89.4.3 Debugging the application . . . . . . . . . . . . . . . . . . . . . . . . . . . 1698
89.5 Formal verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1698
92 Definitions 1715
92.1 Level . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1715
92.2 Remediation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1715
xvi
93.5 User-Defined Storage Pools Policy (DYN05) . . . . . . . . . . . . . . . . . . . . . . 1724
93.5.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1725
93.5.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1725
93.5.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1725
93.5.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1726
93.5.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1726
93.5.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1726
93.6 Statically Determine Maximum Stack Requirements (DYN06) . . . . . . . . . . 1726
93.6.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1727
93.6.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1727
93.6.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1727
93.6.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1727
93.6.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1727
93.6.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1727
xvii
95.3.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1742
95.3.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1742
xviii
96.8.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1756
96.8.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1756
96.9 Use Precondition and Postcondition Contracts (RPP09) . . . . . . . . . . . . . . 1756
96.9.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1757
96.9.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1757
96.9.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1757
96.9.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1757
96.9.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1758
96.9.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1758
96.10Do Not Re-Verify Preconditions in Subprogram Bodies (RPP10) . . . . . . . . . . 1758
96.10.1Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1759
96.10.2Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1759
96.10.3Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1759
96.10.4Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1759
96.10.5Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1759
96.10.6Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1760
96.11Always Use the Result of Function Calls (RPP11) . . . . . . . . . . . . . . . . . . 1760
96.11.1Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1760
96.11.2Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1760
96.11.3Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1761
96.11.4Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1761
96.11.5Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1761
96.11.6Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1761
96.12No Recursion (RPP12) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1761
96.12.1Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1762
96.12.2Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1762
96.12.3Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1762
96.12.4Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1762
96.12.5Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1762
96.12.6Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1762
96.13No Reuse of Standard Typemarks (RPP13) . . . . . . . . . . . . . . . . . . . . . . 1763
96.13.1Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1763
96.13.2Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1763
96.13.3Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1763
96.13.4Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1764
96.13.5Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1764
96.13.6Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1764
96.14Use Symbolic Constants for Literal Values (RPP14) . . . . . . . . . . . . . . . . . 1764
96.14.1Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1764
96.14.2Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1765
96.14.3Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1765
96.14.4Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1765
96.14.5Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1765
96.14.6Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1765
xix
97.2.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1771
97.2.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1771
97.3 No Exception Propagation Beyond Name Visibility (EXU03) . . . . . . . . . . . . 1771
97.3.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1772
97.3.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1772
97.3.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1772
97.3.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1772
97.3.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1773
97.3.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1773
97.4 Prove Absence of Run-time Exceptions (EXU04) . . . . . . . . . . . . . . . . . . . 1773
97.4.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1774
97.4.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1774
97.4.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1774
97.4.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1774
97.4.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1774
97.4.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1774
xx
98.6.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1785
98.6.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1786
98.7 Ensure Local Type Consistency (OOP07) . . . . . . . . . . . . . . . . . . . . . . . 1786
98.7.1 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1786
98.7.2 Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1786
98.7.3 Applicable Vulnerability within ISO TR 24772-2 . . . . . . . . . . . . . . . 1788
98.7.4 Noncompliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . 1788
98.7.5 Compliant Code Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1789
98.7.6 Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1790
100
References 1799
102
Subprograms 1809
102.1Subtract procedure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1809
102.2Subtract function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1810
102.3Equality function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1811
102.4States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1813
102.5States #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1814
102.6States #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1815
102.7States #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1816
xxi
103
Modular Programming 1819
103.1Months . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1819
103.2Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1820
104
Strongly typed language 1823
104.1Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1823
104.2Integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1826
104.3Temperatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1830
105
Records 1835
105.1Directions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1835
105.2Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1837
105.3Inventory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1841
106
Arrays 1845
106.1Constrained Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1845
106.2Colors: Lookup-Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1847
106.3Unconstrained Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1850
106.4Product info . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1853
106.5String_10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1856
106.6List of Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1858
107
More About Types 1863
107.1Aggregate Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1863
107.2Versioning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1865
107.3Simple todo list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1867
107.4Price list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1869
108
Privacy 1875
108.1Directions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1875
108.2Limited Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1877
108.3Bonus exercise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1881
108.3.1Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1882
108.3.2List of Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1882
108.3.3Price List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1882
109
Generics 1883
109.1Display Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1883
109.2Average of Array of Float . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1885
109.3Average of Array of Any Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1887
109.4Generic list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1890
110
Exceptions 1893
110.1Uninitialized Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1893
110.2Numerical Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1894
110.3Re-raising Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1896
111
Tasking 1899
111.1Display Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1899
111.2Event Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1900
111.3Generic Protected Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1902
112
Design by contracts 1905
112.1Price Range . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1905
112.2Pythagorean Theorem: Predicate . . . . . . . . . . . . . . . . . . . . . . . . . . . 1906
112.3Pythagorean Theorem: Precondition . . . . . . . . . . . . . . . . . . . . . . . . . 1908
112.4Pythagorean Theorem: Postcondition . . . . . . . . . . . . . . . . . . . . . . . . . 1910
112.5Pythagorean Theorem: Type Invariant . . . . . . . . . . . . . . . . . . . . . . . . 1912
112.6Primary Color . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1914
xxii
113
Object-oriented programming 1919
113.1Simple type extension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1919
113.2Online Store . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1921
114
Standard library: Containers 1927
114.1Simple todo list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1927
114.2List of unique integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1929
115
Standard library: Dates & Times 1933
115.1Holocene calendar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1933
115.2List of events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1934
116
Standard library: Strings 1939
116.1Concatenation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1939
116.2List of events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1941
117
Standard library: Numerics 1945
117.1Decibel Factor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1945
117.2Root-Mean-Square . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1947
117.3Rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1950
118
Solutions 1955
118.1Imperative Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1955
118.1.1Hello World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1955
118.1.2Greetings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1955
118.1.3Positive Or Negative . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1956
118.1.4Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1956
118.2Subprograms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1957
118.2.1Subtract Procedure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1957
118.2.2Subtract Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1958
118.2.3Equality function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1959
118.2.4States . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1960
118.2.5States #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1961
118.2.6States #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1962
118.2.7States #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1963
118.3Modular Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1964
118.3.1Months . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1964
118.3.2Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1965
118.4Strongly typed language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1967
118.4.1Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1967
118.4.2Integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1969
118.4.3Temperatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1972
118.5Records . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1974
118.5.1Directions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1974
118.5.2Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1976
118.5.3Inventory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1979
118.6Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1981
118.6.1Constrained Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1981
118.6.2Colors: Lookup-Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1983
118.6.3Unconstrained Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1985
118.6.4Product info . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1987
118.6.5String_10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1989
118.6.6List of Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1991
118.7More About Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1994
118.7.1Aggregate Initialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1994
118.7.2Versioning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1996
118.7.3Simple todo list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1997
118.7.4Price list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1999
118.8Privacy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2001
xxiii
118.8.1Directions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2001
118.8.2Limited Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2003
118.9Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2006
118.9.1Display Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2006
118.9.2Average of Array of Float . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2008
118.9.3Average of Array of Any Type . . . . . . . . . . . . . . . . . . . . . . . . . 2009
118.9.4Generic list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2011
118.10
Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2013
118.10.1
Uninitialized Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2013
118.10.2
Numerical Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2015
118.10.3
Re-raising Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2017
118.11
Tasking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2018
118.11.1
Display Service . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2018
118.11.2
Event Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2020
118.11.3
Generic Protected Queue . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2021
118.12
Design by contracts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2024
118.12.1
Price Range . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2024
118.12.2
Pythagorean Theorem: Predicate . . . . . . . . . . . . . . . . . . . . . . . 2025
118.12.3
Pythagorean Theorem: Precondition . . . . . . . . . . . . . . . . . . . . . 2027
118.12.4
Pythagorean Theorem: Postcondition . . . . . . . . . . . . . . . . . . . . 2028
118.12.5
Pythagorean Theorem: Type Invariant . . . . . . . . . . . . . . . . . . . . 2030
118.12.6
Primary Colors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2032
118.13
Object-oriented programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2034
118.13.1
Simple type extension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2034
118.13.2
Online Store . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2036
118.14
Standard library: Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2039
118.14.1
Simple todo list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2039
118.14.2
List of unique integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2040
118.15
Standard library: Dates & Times . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2042
118.15.1
Holocene calendar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2042
118.15.2
List of events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2043
118.16
Standard library: Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2045
118.16.1
Concatenation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2045
118.16.2
List of events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2047
118.17
Standard library: Numerics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2050
118.17.1
Decibel Factor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2050
118.17.2
Root-Mean-Square . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2051
118.17.3
Rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2053
Bibliography 2069
xxiv
Learning Ada
CONTENTS 1
Learning Ada
2 CONTENTS
Part I
Introduction to Ada
3
Learning Ada
This course will teach you the basics of the Ada programming language and is intended for
those who already have a basic understanding of programming techniques. You will learn
how to apply those techniques to programming in Ada.
This document was written by Raphaël Amiard and Gustavo A. Hoffmann, with review from
Richard Kenner.
Note: The code examples in this course use a 50-column limit, which greatly improves
the readability of the code on devices with a small screen size. This constraint, however,
leads to an unusual coding style. For instance, instead of calling Put_Line in a single line,
we have this:
Put_Line
(" is in the northeast quadrant");
or this:
Note that typical Ada code uses a limit of at least 79 columns. Therefore, please don't take
the coding style from this course as a reference!
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn website2 .
The directory structure in the zip file is based on the code block metadata. For example, if
you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
1 http://creativecommons.org/licenses/by-sa/4.0
2 https://learn.adacore.com/zip/learning-ada_code.zip
5
Learning Ada
6
CHAPTER
ONE
INTRODUCTION
1.1 History
In the 1970s the United States Department of Defense (DOD) suffered from an explosion
of the number of programming languages, with different projects using different and non-
standard dialects or language subsets / supersets. The DOD decided to solve this problem
by issuing a request for proposals for a common, modern programming language. The
winning proposal was one submitted by Jean Ichbiah from CII Honeywell-Bull.
The first Ada standard was issued in 1983; it was subsequently revised and enhanced in
1995, 2005 and 2012, with each revision bringing useful new features.
This tutorial will focus on Ada 2012 as a whole, rather than teaching different versions of
the language.
Today, Ada is heavily used in embedded real-time systems, many of which are safety critical.
While Ada is and can be used as a general-purpose language, it will really shine in low-level
applications:
• Embedded systems with low memory requirements (no garbage collector allowed).
• Direct interfacing with hardware.
• Soft or hard real-time systems.
• Low-level systems programming.
Specific domains seeing Ada usage include Aerospace & Defense, civil aviation, rail, and
many others. These applications require a high degree of safety: a software defect is not
just an annoyance, but may have severe consequences. Ada provides safety features that
detect defects at an early stage — usually at compilation time or using static analysis tools.
Ada can also be used to create applications in a variety of other areas, such as:
• Video game programming3
• Real-time audio4
• Kernel modules5
This is a non-comprehensive list that hopefully sheds light on which kind of programming
Ada is good at.
3 https://github.com/AdaDoom3/AdaDoom3
4 http://www.electronicdesign.com/embedded-revolution/assessing-ada-language-audio-applications
5 http://www.nihamkin.com/tag/kernel.html
7
Learning Ada
In terms of modern languages, the closest in terms of targets and level of abstraction are
probably C++6 and Rust7 .
1.3 Philosophy
Ada's philosophy is different from most other languages. Underlying Ada's design are prin-
ciples that include the following:
• Readability is more important than conciseness. Syntactically this shows through the
fact that keywords are preferred to symbols, that no keyword is an abbreviation, etc.
• Very strong typing. It is very easy to introduce new types in Ada, with the benefit of
preventing data usage errors.
– It is similar to many functional languages in that regard, except that the program-
mer has to be much more explicit about typing in Ada, because there is almost no
type inference.
• Explicit is better than implicit. Although this is a Python8 commandment, Ada takes it
way further than any language we know of:
– There is mostly no structural typing, and most types need to be explicitly named
by the programmer.
– As previously said, there is mostly no type inference.
– Semantics are very well defined, and undefined behavior is limited to an absolute
minimum.
– The programmer can generally give a lot of information about what their program
means to the compiler (and other programmers). This allows the compiler to be
extremely helpful (read: strict) with the programmer.
During this course, we will explain the individual language features that are building blocks
for that philosophy.
1.4 SPARK
While this class is solely about the Ada language, it is worth mentioning that another lan-
guage, extremely close to and interoperable with Ada, exists: the SPARK language.
SPARK is a subset of Ada, designed so that the code written in SPARK is amenable to auto-
matic proof. This provides a level of assurance with regard to the correctness of your code
that is much higher than with a regular programming language.
There is a dedicated course for the SPARK language (page 973) but keep in mind that every
time we speak about the specification power of Ada during this course, it is power that you
can leverage in SPARK to help proving the correctness of program properties ranging from
absence of run-time errors to compliance with formally specified functional requirements.
6 https://en.wikipedia.org/wiki/C%2B%2B
7 https://www.rust-lang.org/en-US/
8 https://www.python.org
8 Chapter 1. Introduction
CHAPTER
TWO
IMPERATIVE LANGUAGE
Ada is a multi-paradigm language with support for object orientation and some elements of
functional programming, but its core is a simple, coherent procedural/imperative language
akin to C or Pascal.
In other languages
One important distinction between Ada and a language like C is that statements and ex-
pressions are very clearly distinguished. In Ada, if you try to use an expression where a
statement is required then your program will fail to compile. This rule supports a useful
stylistic principle: expressions are intended to deliver values, not to have side effects. It
can also prevent some programming errors, such as mistakenly using the equality operator
= instead of the assignment operation := in an assignment statement.
Listing 1: greet.adb
1 with Ada.Text_IO;
2
3 procedure Greet is
4 begin
5 -- Print "Hello, World!" to the screen
6 Ada.Text_IO.Put_Line ("Hello, World!");
7 end Greet;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet
MD5: cba89a34b87c9dfa71533d982d05e6ab
Runtime output
Hello, World!
9
Learning Ada
We'll see later how they work in detail. Here, we are requesting a standard library
module, the Ada.Text_IO package, which contains a procedure to print text on the
screen: Put_Line.
• Greet is a procedure, and the main entry point for our first program. Unlike in C
or C++, it can be named anything you prefer. The builder will determine the entry
point. In our simple example, gprbuild, GNAT's builder, will use the file you passed
as parameter.
• Put_Line is a procedure, just like Greet, except it is declared in the Ada.Text_IO
module. It is the Ada equivalent of C's printf.
• Comments start with -- and go to the end of the line. There is no multi-line comment
syntax, that is, it is not possible to start a comment in one line and continue it in the
next line. The only way to create multiple lines of comments in Ada is by using -- on
each line. For example:
In other languages
Procedures are similar to functions in C or C++ that return void. We'll see later how to
declare functions in Ada.
Listing 2: greet.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet is
4 begin
5 -- Print "Hello, World!" to the screen
6 Put_Line ("Hello, World!");
7 end Greet;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_2
MD5: a58a1193207df44aa6edaa4fe1c14280
Runtime output
Hello, World!
This version utilizes an Ada feature known as a use clause, which has the form use package-
name. As illustrated by the call on Put_Line, the effect is that entities from the named
package can be referenced directly, without the package-name. prefix.
This section describes Ada's if statement and introduces several other fundamental lan-
guage facilities including integer I/O, data declarations, and subprogram parameter modes.
Ada's if statement is pretty unsurprising in form and function:
Listing 3: check_positive.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
3
4 procedure Check_Positive is
5 N : Integer;
6 begin
7 -- Put a String
8 Put ("Enter an integer value: ");
9
13 if N > 0 then
14 -- Put an Integer
15 Put (N);
16 Put_Line (" is a positive number");
17 end if;
18 end Check_Positive;
Project: Courses.Intro_To_Ada.Imperative_Language.Check_Positive
MD5: 2e8b4b2f3f258fd9e02c2d65846af101
The if statement minimally consists of the reserved word if, a condition (which must be
a Boolean value), the reserved word then and a non-empty sequence of statements (the
then part) which is executed if the condition evaluates to True, and a terminating end if.
This example declares an integer variable N, prompts the user for an integer, checks if the
value is positive and, if so, displays the integer's value followed by the string " is a positive
number". If the value is not positive, the procedure does not display any output.
The type Integer is a predefined signed type, and its range depends on the computer ar-
chitecture. On typical current processors Integer is 32-bit signed.
The example illustrates some of the basic functionality for integer input-output. The rel-
evant subprograms are in the predefined package Ada.Integer_Text_IO and include the
Get procedure (which reads in a number from the keyboard) and the Put procedure (which
displays an integer value).
Here's a slight variation on the example, which illustrates an if statement with an else
part:
Listing 4: check_positive.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
3
4 procedure Check_Positive is
5 N : Integer;
6 begin
7 -- Put a String
8 Put ("Enter an integer value: ");
(continues on next page)
13 -- Put an Integer
14 Put (N);
15
16 if N > 0 then
17 Put_Line (" is a positive number");
18 else
19 Put_Line (" is not a positive number");
20 end if;
21 end Check_Positive;
Project: Courses.Intro_To_Ada.Imperative_Language.Check_Positive_2
MD5: 28fca0d7840d06d478e5933e8182d1db
In this example, if the input value is not positive then the program displays the value fol-
lowed by the String " is not a positive number".
Our final variation illustrates an if statement with elsif sections:
Listing 5: check_direction.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
3
4 procedure Check_Direction is
5 N : Integer;
6 begin
7 Put ("Enter an integer value: ");
8 Get (N);
9 Put (N);
10
11 if N = 0 or N = 360 then
12 Put_Line (" is due north");
13 elsif N in 1 .. 89 then
14 Put_Line (" is in the northeast quadrant");
15 elsif N = 90 then
16 Put_Line (" is due east");
17 elsif N in 91 .. 179 then
18 Put_Line (" is in the southeast quadrant");
19 elsif N = 180 then
20 Put_Line (" is due south");
21 elsif N in 181 .. 269 then
22 Put_Line (" is in the southwest quadrant");
23 elsif N = 270 then
24 Put_Line (" is due west");
25 elsif N in 271 .. 359 then
26 Put_Line (" is in the northwest quadrant");
27 else
28 Put_Line (" is not in the range 0..360");
29 end if;
30 end Check_Direction;
Project: Courses.Intro_To_Ada.Imperative_Language.Check_Direction
MD5: 7759d30c9bb0bfb88efdf12128f9c382
This example expects the user to input an integer between 0 and 360 inclusive, and displays
which quadrant or axis the value corresponds to. The in operator in Ada tests whether a
scalar value is within a specified range and returns a Boolean result. The effect of the
program should be self-explanatory; later we'll see an alternative and more efficient style
to accomplish the same effect, through a case statement.
Ada's elsif keyword differs from C or C++, where nested else .. if blocks would be
used instead. And another difference is the presence of the end if in Ada, which avoids
the problem known as the "dangling else".
Ada has three ways of specifying loops. They differ from the C / Java / Javascript for-loop,
however, with simpler syntax and semantics in line with Ada's philosophy.
The first kind of loop is the for loop, which allows iteration through a discrete range.
Listing 6: greet_5a.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet_5a is
4 begin
5 for I in 1 .. 5 loop
6 -- Put_Line is a procedure call
7 Put_Line ("Hello, World!"
8 & Integer'Image (I));
9 -- ^ Procedure parameter
10 end loop;
11 end Greet_5a;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_5a
MD5: 7f588b67947126f789333adfaaf1b638
Runtime output
Hello, World! 1
Hello, World! 2
Hello, World! 3
Hello, World! 4
Hello, World! 5
Listing 7: greet_5a_reverse.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet_5a_Reverse is
4 begin
5 for I in reverse 1 .. 5 loop
6 Put_Line ("Hello, World!"
7 & Integer'Image (I));
8 end loop;
9 end Greet_5a_Reverse;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_5a_Reverse
MD5: a0d5dcfc471fb1a107477c934fa527c2
Runtime output
Hello, World! 5
Hello, World! 4
Hello, World! 3
Hello, World! 2
Hello, World! 1
The bounds of a for loop may be computed at run-time; they are evaluated once, before
the loop body is executed. If the value of the upper bound is less than the value of the
lower bound, then the loop is not executed at all. This is the case also for reverse loops.
Thus no output is produced in the following example:
Listing 8: greet_no_op.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet_No_Op is
4 begin
5 for I in reverse 5 .. 1 loop
6 Put_Line ("Hello, World!"
7 & Integer'Image (I));
8 end loop;
9 end Greet_No_Op;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_No_Op
MD5: 5070693fb0324d3e4e43a8c8c4f046e1
Build output
greet_no_op.adb:5:23: warning: loop range is null, loop will not execute [enabled␣
↪by default]
The for loop is more general than what we illustrated here; more on that later.
The simplest loop in Ada is the bare loop, which forms the foundation of the other kinds of
Ada loops.
Listing 9: greet_5b.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet_5b is
4 -- Variable declaration:
5 I : Integer := 1;
6 -- ^ Type
7 -- ^ Initial value
8 begin
9 loop
10 Put_Line ("Hello, World!"
11 & Integer'Image (I));
12
13 -- Exit statement:
14 exit when I = 5;
15 -- ^ Boolean condition
16
17 -- Assignment:
18 I := I + 1;
19 -- There is no I++ short form to
20 -- increment a variable
21 end loop;
22 end Greet_5b;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_5b
MD5: 5b218a64a07f64bd97774b574883c44a
Runtime output
Hello, World! 1
Hello, World! 2
Hello, World! 3
Hello, World! 4
Hello, World! 5
3 procedure Greet_5c is
4 I : Integer := 1;
5 begin
6 -- Condition must be a Boolean value
7 -- (no Integers).
8 -- Operator "<=" returns a Boolean
9 while I <= 5 loop
10 Put_Line ("Hello, World!"
11 & Integer'Image (I));
12
13 I := I + 1;
14 end loop;
15 end Greet_5c;
Runtime output
Hello, World! 1
Hello, World! 2
Hello, World! 3
Hello, World! 4
Hello, World! 5
The condition is evaluated before each iteration. If the result is false, then the loop is
terminated.
This program has the same effect as the previous examples.
In other languages
Note that Ada has different semantics than C-based languages with respect to the condition
in a while loop. In Ada the condition has to be a Boolean value or the compiler will reject the
program; the condition is not an integer that is treated as either True or False depending
on whether it is non-zero or zero.
Ada's case statement is similar to the C and C++ switch statement, but with some impor-
tant differences.
Here's an example, a variation of a program that was shown earlier with an if statement:
4 procedure Check_Direction is
5 N : Integer;
6 begin
7 loop
8 Put ("Enter an integer value: ");
9 Get (N);
10 Put (N);
11
12 case N is
13 when 0 | 360 =>
14 Put_Line
15 (" is due north");
16 when 1 .. 89 =>
17 Put_Line
18 (" is in the northeast quadrant");
19 when 90 =>
20 Put_Line
21 (" is due east");
22 when 91 .. 179 =>
23 Put_Line
24 (" is in the southeast quadrant");
25 when 180 =>
26 Put_Line
27 (" is due south");
28 when 181 .. 269 =>
29 Put_Line
30 (" is in the southwest quadrant");
31 when 270 =>
32 Put_Line
33 (" is due west");
34 when 271 .. 359 =>
35 Put_Line
36 (" is in the northwest quadrant");
37 when others =>
38 Put_Line
39 (" Au revoir");
40 exit;
41 end case;
42 end loop;
43 end Check_Direction;
Project: Courses.Intro_To_Ada.Imperative_Language.Check_Direction_2
MD5: 1c758b76a2c3991cb4e2a0cf5e172ac3
This program repeatedly prompts for an integer value and then, if the value is in the range
0 .. 360, displays the associated quadrant or axis. If the value is an Integer outside this
range, the loop (and the program) terminate after outputting a farewell message.
The effect of the case statement is similar to the if statement in an earlier example, but the
case statement can be more efficient because it does not involve multiple range tests.
Notable points about Ada's case statement:
• The case expression (here the variable N) must be of a discrete type, i.e. either an
integer type or an enumeration type. Discrete types will be covered in more detail
later discrete types (page 47).
• Every possible value for the case expression needs to be covered by a unique branch
of the case statement. This will be checked at compile time.
• A branch can specify a single value, such as 0; a range of values, such as 1 .. 89; or
any combination of the two (separated by a |).
• As a special case, an optional final branch can specify others, which covers all values
not included in the earlier branches.
• Execution consists of the evaluation of the case expression and then a transfer of
control to the statement sequence in the unique branch that covers that value.
• When execution of the statements in the selected branch has completed, control re-
sumes after the end case. Unlike C, execution does not fall through to the next branch.
So Ada doesn't need (and doesn't have) a break statement.
As mentioned earlier, Ada draws a clear syntactic separation between declarations, which
introduce names for entities that will be used in the program, and statements, which per-
form the processing. The areas in the program where declarations may appear are known
as declarative regions.
In any subprogram, the section between the is and the begin is a declarative region. You
can have variables, constants, types, inner subprograms, and other entities there.
We've briefly mentioned variable declarations in previous subsection. Let's look at a simple
example, where we declare an integer variable X in the declarative region and perform an
initialization and an addition on it:
3 procedure Main is
4 X : Integer;
5 begin
6 X := 0;
7 Put_Line ("The initial value of X is "
8 & Integer'Image (X));
9
Project: Courses.Intro_To_Ada.Imperative_Language.Variable_Declaration
MD5: cbb08d5e382fbfcc28e986bea80cd253
Runtime output
3 procedure Main is
4 procedure Nested is
5 begin
6 Put_Line ("Hello World");
7 end Nested;
8 begin
9 Nested;
10 -- Call to Nested
11 end Main;
Project: Courses.Intro_To_Ada.Imperative_Language.Nested_Procedure
MD5: 2e7fb267e31232196065febd5e35e6ef
Runtime output
Hello World
A declaration cannot appear as a statement. If you need to declare a local variable amidst
the statements, you can introduce a new declarative region with a block statement:
3 procedure Greet is
4 begin
5 loop
6 Put_Line ("Please enter your name: ");
7
8 declare
9 Name : String := Get_Line;
10 -- ^ Call to the
11 -- Get_Line function
12 begin
13 exit when Name = "";
14 Put_Line ("Hi " & Name & "!");
15 end;
16
20 Put_Line ("Bye!");
21 end Greet;
Project: Courses.Intro_To_Ada.Imperative_Language.Greet_6
MD5: a9c0c14a1b3e2ebe07cd88f442787e3a
Attention: The Get_Line function allows you to receive input from the user, and get
the result as a string. It is more or less equivalent to the scanf C function.
It returns a String, which, as we will see later, is an Unconstrained array type (page 78).
For now we simply note that, if you wish to declare a String variable and do not know
its size in advance, then you need to initialize the variable during its declaration.
Ada 2012 introduced an expression analog for conditional statements (if and case).
2.6.1 If expressions
Here's an alternative version of an example we saw earlier; the if statement has been
replaced by an if expression:
4 procedure Check_Positive is
5 N : Integer;
6 begin
7 Put ("Enter an integer value: ");
8 Get (N);
9 Put (N);
10
11 declare
12 S : constant String :=
13 (if N > 0
14 then " is a positive number"
15 else " is not a positive number");
16 begin
17 Put_Line (S);
18 end;
19 end Check_Positive;
Project: Courses.Intro_To_Ada.Imperative_Language.Check_Positive
MD5: 01f23463b14774f750dbb21f6c65ea09
The if expression evaluates to one of the two Strings depending on N, and assigns that
value to the local variable S.
Ada's if expressions are similar to if statements. However, there are a few differences
that stem from the fact that it is an expression:
• All branches' expressions must be of the same type
• It must be surrounded by parentheses if the surrounding expression does not already
contain them
• An else branch is mandatory unless the expression following then has a Boolean
value. In that case an else branch is optional and, if not present, defaults to else
True.
Here's another example:
3 procedure Main is
4 begin
5 for I in 1 .. 10 loop
6 Put_Line (if I mod 2 = 0
7 then "Even"
8 else "Odd");
9 end loop;
10 end Main;
Project: Courses.Intro_To_Ada.Imperative_Language.Even_Odd
MD5: c89c3233ab8822c828f7a7bba8fd3f1c
Runtime output
Odd
Even
Odd
Even
Odd
Even
Odd
Even
Odd
Even
This program produces 10 lines of output, alternating between "Odd" and "Even".
Analogous to if expressions, Ada also has case expressions. They work just as you would
expect.
3 procedure Main is
4 begin
5 for I in 1 .. 10 loop
6 Put_Line
7 (case I is
8 when 1 | 3 | 5 | 7 | 9 => "Odd",
9 when 2 | 4 | 6 | 8 | 10 => "Even");
10 end loop;
11 end Main;
Project: Courses.Intro_To_Ada.Imperative_Language.Case_Expression
MD5: 6ce40efc987c2665960b1f08d30d780d
Runtime output
Odd
Even
Odd
Even
Odd
Even
Odd
Even
Odd
Even
THREE
SUBPROGRAMS
3.1 Subprograms
So far, we have used procedures, mostly to have a main body of code to execute. Proce-
dures are one kind of subprogram.
There are two kinds of subprograms in Ada, functions and procedures. The distinction
between the two is that a function returns a value, and a procedure does not.
This example shows the declaration and definition of a function:
Listing 1: increment.ads
1 function Increment (I : Integer) return Integer;
Listing 2: increment.adb
1 -- We declare (but don't define) a function with
2 -- one parameter, returning an integer value
3
Subprograms in Ada can, of course, have parameters. One syntactically important note is
that a subprogram which has no parameters does not have a parameter section at all, for
example:
procedure Proc;
Listing 3: increment_by.ads
1 function Increment_By
2 (I : Integer := 0;
3 Incr : Integer := 1) return Integer;
4 -- ^ Default value for parameters
23
Learning Ada
Project: Courses.Intro_To_Ada.Subprograms.Increment_By
MD5: 5728b915789beee0b5546ea7b36a1cc2
In this example, we see that parameters can have default values. When calling the subpro-
gram, you can then omit parameters if they have a default value. Unlike C/C++, a call to a
subprogram without parameters does not include parentheses.
This is the implementation of the function above:
Listing 4: increment_by.adb
1 function Increment_By
2 (I : Integer := 0;
3 Incr : Integer := 1) return Integer is
4 begin
5 return I + Incr;
6 end Increment_By;
Project: Courses.Intro_To_Ada.Subprograms.Increment_By
MD5: 07c85e5c1272ea396bf4dbc0cefcdce7
Listing 5: show_increment.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Increment_By;
3
4 procedure Show_Increment is
5 A, B, C : Integer;
6 begin
7 C := Increment_By;
(continues on next page)
9 https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug.html
24 Chapter 3. Subprograms
Learning Ada
15 A := 10;
16 B := 3;
17 C := Increment_By (A, B);
18 -- ^ Regular parameter passing
19
27 A := 20;
28 B := 5;
29 C := Increment_By (I => A,
30 Incr => B);
31 -- ^ Named parameter passing
32
Project: Courses.Intro_To_Ada.Subprograms.Increment_By
MD5: dcb501c8c6815b03c6841fc8b80d6911
Runtime output
Ada allows you to name the parameters when you pass them, whether they have a default
or not. There are some rules:
• Positional parameters come first.
• A positional parameter cannot follow a named parameter.
As a convention, people usually name parameters at the call site if the function's corre-
sponding parameters has a default value. However, it is also perfectly acceptable to name
every parameter if it makes the code clearer.
3.1. Subprograms 25
Learning Ada
As briefly mentioned earlier, Ada allows you to declare one subprogram inside another.
This is useful for two reasons:
• It lets you organize your programs in a cleaner fashion. If you need a subprogram only
as a "helper" for another subprogram, then the principle of localization indicates that
the helper subprogram should be declared nested.
• It allows you to share state easily in a controlled fashion, because the nested subpro-
grams have access to the parameters, as well as any local variables, declared in the
outer scope.
For the previous example, we can move the duplicated code (call to Put_Line) to a separate
procedure. This is a shortened version with the nested Display_Result procedure.
Listing 6: show_increment.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Increment_By;
3
4 procedure Show_Increment is
5 A, B, C : Integer;
6
7 procedure Display_Result is
8 begin
9 Put_Line ("Increment of "
10 & Integer'Image (A)
11 & " with "
12 & Integer'Image (B)
13 & " is "
14 & Integer'Image (C));
15 end Display_Result;
16
17 begin
18 A := 10;
19 B := 3;
20 C := Increment_By (A, B);
21 Display_Result;
22 A := 20;
23 B := 5;
24 C := Increment_By (A, B);
25 Display_Result;
26 end Show_Increment;
Project: Courses.Intro_To_Ada.Subprograms.Increment_By
MD5: 23ec8ae3080c042123a9e82ee6b3d9e3
Runtime output
Increment of 10 with 3 is 13
Increment of 20 with 5 is 25
26 Chapter 3. Subprograms
Learning Ada
An important feature of function calls in Ada is that the return value at a call cannot be
ignored; that is, a function call cannot be used as a statement.
If you want to call a function and do not need its result, you will still need to explicitly store
it in a local variable.
Listing 7: quadruple.adb
1 function Quadruple (I : Integer)
2 return Integer is
3
18 return Res;
19 end Quadruple;
Project: Courses.Intro_To_Ada.Subprograms.Quadruple
MD5: 44326f12a9d797ea13ffe52ea48fc36f
Build output
function Read_Int
(Stream : Network_Stream;
Result : out Integer) return Boolean;
procedure Main is
Stream : Network_Stream := Get_Stream;
My_Int : Integer;
3.1. Subprograms 27
Learning Ada
• Or give the variable a name that contains any of the strings discard dummy ignore
junk unused (case insensitive)
So far we have seen that Ada is a safety-focused language. There are many ways this is
realized, but two important points are:
• Ada makes the user specify as much as possible about the behavior expected for the
program, so that the compiler can warn or reject if there is an inconsistency.
• Ada provides a variety of techniques for achieving the generality and flexibility of point-
ers and dynamic memory management, but without the latter's drawbacks (such as
memory leakage and dangling references).
Parameter modes are a feature that helps achieve the two design goals above. A subpro-
gram parameter can be specified with a mode, which is one of the following:
The default mode for parameters is in; so far, most of the examples have been using in
parameters.
Historically
Functions and procedures were originally more different in philosophy. Before Ada 2012,
functions could only take in parameters.
3.3.1 In parameters
The first mode for parameters is the one we have been implicitly using so far. Parameters
passed using this mode cannot be modified, so that the following program will cause an
error:
Listing 8: swap.adb
1 procedure Swap (A, B : Integer) is
2 Tmp : Integer;
3 begin
4 Tmp := A;
5
28 Chapter 3. Subprograms
Learning Ada
Project: Courses.Intro_To_Ada.Subprograms.Swap
MD5: 478ac23f878934aae820e4b9c056d939
Build output
The fact that in is the default mode is very important. It means that a parameter will not
be modified unless you explicitly specify a mode in which modification is allowed.
Listing 9: in_out_params.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure In_Out_Params is
4 procedure Swap (A, B : in out Integer) is
5 Tmp : Integer;
6 begin
7 Tmp := A;
8 A := B;
9 B := Tmp;
10 end Swap;
11
12 A : Integer := 12;
13 B : Integer := 44;
14 begin
15 Swap (A, B);
16
17 -- Prints 44
18 Put_Line (Integer'Image (A));
19 end In_Out_Params;
Project: Courses.Intro_To_Ada.Subprograms.In_Out_Params
MD5: 319358e479449c115cf2b3cbb4ff3a6b
Runtime output
44
An in out parameter will allow read and write access to the object passed as parameter,
so in the example above, we can see that A is modified after the call to Swap.
Attention: While in out parameters look a bit like references in C++, or regular
parameters in Java that are passed by-reference, the Ada language standard does not
mandate "by reference" passing for in out parameters except for certain categories of
types as will be explained later.
In general, it is better to think of modes as higher level than by-value versus by-reference
semantics. For the compiler, it means that an array passed as an in parameter might
be passed by reference, because it is more efficient (which does not change anything
for the user since the parameter is not assignable). However, a parameter of a discrete
type will always be passed by copy, regardless of its mode (which is more efficient on
most architectures).
The out mode applies when the subprogram needs to write to a parameter that might be
uninitialized at the point of call. Reading the value of an out parameter is permitted, but
it should only be done after the subprogram has assigned a value to the parameter. Out
parameters behave a bit like return values for functions. When the subprogram returns,
the actual parameter (a variable) will have the value of the out parameter at the point of
return.
In other languages
Ada doesn't have a tuple construct and does not allow returning multiple values from a sub-
program (except by declaring a full-fledged record type). Hence, a way to return multiple
values from a subprogram is to use out parameters.
For example, a procedure reading integers from the network could have one of the following
specifications:
procedure Read_Int
(Stream : Network_Stream;
Success : out Boolean;
Result : out Integer);
function Read_Int
(Stream : Network_Stream;
Result : out Integer) return Boolean;
While reading an out variable before writing to it should, ideally, trigger an error, imposing
that as a rule would cause either inefficient run-time checks or complex compile-time rules.
So from the user's perspective an out parameter acts like an uninitialized variable when the
subprogram is invoked.
30 Chapter 3. Subprograms
Learning Ada
Project: Courses.Intro_To_Ada.Subprograms.Out_Params
MD5: 36bdb4e541297d7fb0b075816cb6e73a
Build output
outp.adb:3:22: warning: "A" may be referenced before it has a value [enabled by␣
↪default]
As we saw earlier, a subprogram can be declared without being fully defined, This is possible
in general, and can be useful if you need subprograms to be mutually recursive, as in the
example below:
Project: Courses.Intro_To_Ada.Subprograms.Mutually_Recursive_Subprograms
MD5: 5ee030cdecc6c4aea8916cbb763e8526
3.4 Renaming
Subprograms can be renamed by using the renames keyword and declaring a new name
for a subprogram:
This can be useful, for example, to improve the readability of your application when you're
using code from external sources that cannot be changed in your system. Let's look at an
example:
3 procedure A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed
4 (A_Message : String) is
5 begin
6 Put_Line (A_Message);
7 end A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
Project: Courses.Intro_To_Ada.Subprograms.Proc_Renaming
MD5: 6d4952e9dee8ef69a9e3c3e185c635f1
As the wording in the name of procedure above implies, we cannot change its name. We
can, however, rename it to something like Show in our test application and use this shorter
name. Note that we also have to declare all parameters of the original subprogram — we
may rename them, too, in the declaration. For example:
3 procedure Show_Renaming is
4
8 begin
9 Show ("Hello World!");
10 end Show_Renaming;
Project: Courses.Intro_To_Ada.Subprograms.Proc_Renaming
MD5: 5b3b550f8a1cbeb7d9cfd3673f6d42b3
Runtime output
Hello World!
32 Chapter 3. Subprograms
Learning Ada
We may also rename subprograms from the standard library. For example, we may rename
Integer'Image to Img:
3 procedure Show_Image_Renaming is
4
8 begin
9 Put_Line (Img (2));
10 Put_Line (Img (3));
11 end Show_Image_Renaming;
Project: Courses.Intro_To_Ada.Subprograms.Integer_Image_Renaming
MD5: 9843b9d5967679c4fe8bd83a5213829f
Runtime output
2
3
Renaming also allows us to introduce default expressions that were not available in the
original declaration. For example, we may specify "Hello World!" as the default for the
String parameter of the Show procedure:
with A_Procedure_With_Very_Long_Name_That_Cannot_Be_Changed;
procedure Show_Renaming_Defaults is
begin
Show;
end Show_Renaming_Defaults;
3.4. Renaming 33
Learning Ada
34 Chapter 3. Subprograms
CHAPTER
FOUR
MODULAR PROGRAMMING
So far, our examples have been simple standalone subprograms. Ada is helpful in that
regard, since it allows arbitrary declarations in a declarative part. We were thus able to
declare our types and variables in the bodies of main procedures.
However, it is easy to see that this is not going to scale up for real-world applications. We
need a better way to structure our programs into modular and distinct units.
Ada encourages the separation of programs into multiple packages and sub-packages, pro-
viding many tools to a programmer on a quest for a perfectly organized code-base.
4.1 Packages
Listing 1: week.ads
1 package Week is
2
11 end Week;
Project: Courses.Intro_To_Ada.Modular_Programming.Week
MD5: 0fa033dc8fe2b9741483de273354e7ee
Listing 2: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Week;
3 -- References the Week package, and
4 -- adds a dependency from Main to Week
5
6 procedure Main is
7 begin
8 Put_Line ("First day of the week is "
(continues on next page)
35
Learning Ada
Project: Courses.Intro_To_Ada.Modular_Programming.Week
MD5: 03e17a75620de6a397b1d3c5a3e22f6a
Runtime output
Packages let you make your code modular, separating your programs into semantically
significant units. Additionally the separation of a package's specification from its body
(which we will see below) can reduce compilation time.
While the with clause indicates a dependency, you can see in the example above that you
still need to prefix the referencing of entities from the Week package by the name of the
package. (If we had included a use Week clause, then such a prefix would not have been
necessary.)
Accessing entities from a package uses the dot notation, A.B, which is the same notation
as the one used to access record fields.
A with clause can only appear in the prelude of a compilation unit (i.e., before the reserved
word, such as procedure, that marks the beginning of the unit). It is not allowed anywhere
else. This rule is only needed for methodological reasons: the person reading your code
should be able to see immediately which units the code depends on.
In other languages
Packages look similar to, but are semantically very different from, header files in C/C++.
• The first and most important distinction is that packages are a language-level mech-
anism. This is in contrast to a #include'd header file, which is a functionality of the C
preprocessor.
• An immediate consequence is that the with construct is a semantic inclusion mech-
anism, not a text inclusion mechanism. Hence, when you with a package, you are
saying to the compiler "I'm depending on this semantic unit", and not "include this
bunch of text in place here".
• The effect of a package thus does not vary depending on where it has been withed
from. Contrast this with C/C++, where the meaning of the included text depends on
the context in which the #include appears.
This allows compilation/recompilation to be more efficient. It also allows tools like IDEs
to have correct information about the semantics of a program. In turn, this allows
better tooling in general, and code that is more analyzable, even by humans.
An important benefit of Ada with clauses when compared to #include is that it is stateless.
The order of with and use clauses does not matter, and can be changed without side effects.
example above, the Week package will be in an .ads file (for Ada specification), and the
Main procedure will be in an .adb file (for Ada body).
As we have seen above, the with clause indicates a dependency on another package.
However, every reference to an entity coming from the Week package had to be prefixed
by the full name of the package. It is possible to make every entity of a package visible
directly in the current scope, using the use clause.
In fact, we have been using the use clause since almost the beginning of this tutorial.
Listing 3: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 -- ^ Make every entity of the
3 -- Ada.Text_IO package
4 -- directly visible.
5 with Week;
6
7 procedure Main is
8 use Week;
9 -- Make every entity of the Week
10 -- package directly visible.
11 begin
12 Put_Line ("First day of the week is " & Mon);
13 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Week
MD5: ea54077d4ae165b28ae8facfe8ba2db7
Runtime output
In the simple example above, the Week package only has declarations and no body. That's
not a mistake: in a package specification, which is what is illustrated above, you cannot
declare bodies. Those have to be in the package body.
Listing 4: operations.ads
1 package Operations is
2
3 -- Declaration
4 function Increment_By
5 (I : Integer;
6 Incr : Integer := 0) return Integer;
7
10 end Operations;
Listing 5: operations.adb
1 package body Operations is
2
3 Last_Increment : Integer := 1;
4
5 function Increment_By
6 (I : Integer;
7 Incr : Integer := 0) return Integer is
8 begin
9 if Incr /= 0 then
10 Last_Increment := Incr;
11 end if;
12
13 return I + Last_Increment;
14 end Increment_By;
15
21 end Operations;
Here we can see that the body of the Increment_By function has to be declared in the
body. Coincidentally, introducing a body allows us to put the Last_Increment variable in
the body, and make them inaccessible to the user of the Operations package, providing a
first form of encapsulation.
This works because entities declared in the body are only visible in the body.
This example shows how Last_Increment is used indirectly:
Listing 6: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Operations;
3
4 procedure Main is
5 use Operations;
6
7 I : Integer := 0;
8 R : Integer;
9
10 procedure Display_Update_Values is
(continues on next page)
Project: Courses.Intro_To_Ada.Modular_Programming.Operations
MD5: 76190b1261a9652cfb7986ecec191e37
Runtime output
0 incremented by 1 is 1
1 incremented by 1 is 2
2 incremented by 5 is 7
7 incremented by 5 is 12
12 incremented by 10 is 22
22 incremented by 10 is 32
Packages can be used to create hierarchies. We achieve this by using child packages, which
extend the functionality of their parent package. One example of a child package that
we've been using so far is the Ada.Text_IO package. Here, the parent package is called
Ada, while the child package is called Text_IO. In the previous examples, we've been using
the Put_Line procedure from the Text_IO child package.
Important
Ada also supports nested packages. However, since they can be more complicated to use,
the recommendation is to use child packages instead. Nested packages will be covered in
the advanced course.
Let's begin our discussion on child packages by taking our previous Week package:
Listing 7: week.ads
1 package Week is
2
11 end Week;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: 0fa033dc8fe2b9741483de273354e7ee
Listing 8: week-child.ads
1 package Week.Child is
2
5 end Week.Child;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: a7db38e772cf6153b5eb95069517e833
Here, Week is the parent package and Child is the child package. This is the corresponding
package body of Week.Child:
Listing 9: week-child.adb
1 package body Week.Child is
2
8 end Week.Child;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: 04dad82685ad9f0231c3084266b0af83
In the implementation of the Get_First_Of_Week function, we can use the Mon string di-
rectly, even though it was declared in the parent package Week. We don't write with Week
here because all elements from the specification of the Week package — such as Mon, Tue
and so on — are visible in the child package Week.Child.
Now that we've completed the implementation of the Week.Child package, we can use
elements from this child package in a subprogram by simply writing with Week.Child.
Similarly, if we want to use these elements directly, we write use Week.Child in addition.
For example:
4 procedure Main is
5 begin
6 Put_Line ("First day of the week is "
7 & Get_First_Of_Week);
8 end Main;
Runtime output
First day of the week is Monday
So far, we've seen a two-level package hierarchy. But the hierarchy that we can potentially
create isn't limited to that. For instance, we could extend the hierarchy of the previous
source code example by declaring a Week.Child.Grandchild package. In this case, Week.
Child would be the parent of the Grandchild package. Let's consider this implementation:
5 end Week.Child.Grandchild;
8 end Week.Child.Grandchild;
We can use this new Grandchild package in our test application in the same way as before:
we can reuse the previous test application and adapt the with and use, and the function
call. This is the updated code:
6 procedure Main is
7 begin
8 Put_Line ("Second day of the week is "
9 & Get_Second_Of_Week);
10 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: 29ee409c8131bd9529c6bf6e366bb390
Runtime output
Again, this isn't the limit for the package hierarchy. We could continue to extend
the hierarchy of the previous example by implementing a Week.Child.Grandchild.
Grand_grandchild package.
So far, we've seen a single child package of a parent package. However, a parent package
can also have multiple children. We could extend the example above and implement a
Week.Child_2 package. For example:
5 end Week.Child_2;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: bd3f63cacd142d9885600f4000b4573b
Here, Week is still the parent package of the Child package, but it's also the parent of the
Child_2 package. In the same way, Child_2 is obviously one of the child packages of Week.
This is the corresponding package body of Week.Child_2:
8 end Week.Child_2;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: c2c03e4cb1daff02dd6076c2956ef2aa
5 procedure Main is
6 begin
7 Put_Line ("First day of the week is "
8 & Get_First_Of_Week);
9 Put_Line ("Last day of the week is "
10 & Get_Last_Of_Week);
11 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Child_Packages
MD5: 6a91f239fb2a2d8c702409c22467a424
Runtime output
4.4.3 Visibility
In the previous section, we've seen that elements declared in a parent package specification
are visible in the child package. This is, however, not the case for elements declared in the
package body of a parent package.
Let's consider the package Book and its child Additional_Operations:
10 end Book;
7 end Book.Additional_Operations;
Project: Courses.Intro_To_Ada.Modular_Programming.Visibility
MD5: a0d67cff9aeff288709391d16306df00
16 end Book;
19 end Book.Additional_Operations;
Project: Courses.Intro_To_Ada.Modular_Programming.Visibility
MD5: 68b7490da12bafae0aa6fe0ab76c6b1c
In the implementation of the Get_Extended_Title, we're using the Title constant from the
parent package Book. However, as indicated in the comments of the Get_Extended_Author
function, the Author string — which we declared in the body of the Book package — isn't
visible in the Book.Additional_Operations package. Therefore, we cannot use it to im-
plement the Get_Extended_Author function.
We can, however, use the Get_Author function from Book in the implementation of the
Get_Extended_Author function to retrieve this string. Likewise, we can use this strategy
to implement the Get_Extended_Title function. This is the adapted code:
13 end Book.Additional_Operations;
Project: Courses.Intro_To_Ada.Modular_Programming.Visibility
MD5: b00c187cb54d3fcb9574726028c1efc6
3 with Book.Additional_Operations;
4 use Book.Additional_Operations;
5
6 procedure Main is
7 begin
8 Put_Line (Get_Extended_Title);
9 Put_Line (Get_Extended_Author);
10 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Visibility
MD5: bdc75987fe61e9401b400f8704890ebe
Runtime output
4.5 Renaming
Previously, we've mentioned that subprograms can be renamed (page 32). We can re-
name packages, too. Again, we use the renames keyword for that. The following example
renames the Ada.Text_IO package as TIO:
3 procedure Main is
4 package TIO renames Ada.Text_IO;
5 begin
6 TIO.Put_Line ("Hello");
7 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Rename_Text_IO
MD5: 33652dd004ef33d95c168ab8893cd412
Runtime output
Hello
We can use renaming to improve the readability of our code by using shorter package
names. In the example above, we write TIO.Put_Line instead of the longer version (Ada.
Text_IO.Put_Line). This approach is especially useful when we don't use packages and
want to avoid that the code becomes too verbose.
Note we can also rename subprograms and objects inside packages. For instance, we could
have just renamed the Put_Line procedure in the source code example above:
3 procedure Main is
4 procedure Say (Something : String)
5 renames Ada.Text_IO.Put_Line;
6 begin
7 Say ("Hello");
8 end Main;
Project: Courses.Intro_To_Ada.Modular_Programming.Rename_Put_Line
MD5: f30174ff29eb01f33bc95f1787f9f1dc
Runtime output
Hello
FIVE
Ada is a strongly typed language. It is interestingly modern in that respect: strong static
typing has been increasing in popularity in programming language design, owing to factors
such as the growth of statically typed functional programming, a big push from the research
community in the typing domain, and many practical languages with strong type systems.
In statically typed languages, a type is mainly (but not only) a compile time construct.
It is a construct to enforce invariants about the behavior of a program. Invariants are
unchangeable properties that hold for all variables of a given type. Enforcing them ensures,
for example, that variables of a data type never have invalid values.
A type is used to reason about the objects a program manipulates (an object is a variable
or a constant). The aim is to classify objects by what you can accomplish with them (i.e.,
the operations that are permitted), and this way you can reason about the correctness of
the objects' values.
5.2 Integers
A nice feature of Ada is that you can define your own integer types, based on the require-
ments of your program (i.e., the range of values that makes sense). In fact, the definitional
mechanism that Ada provides forms the semantic basis for the predefined integer types.
There is no "magical" built-in type in that regard, which is unlike most languages, and ar-
guably very elegant.
Listing 1: integer_type_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Integer_Type_Example is
4 -- Declare a signed integer type,
5 -- and give the bounds
6 type My_Int is range -1 .. 20;
7 -- ^ High bound
8 -- ^ Low bound
9
47
Learning Ada
Runtime output
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
This example illustrates the declaration of a signed integer type, and several things we can
do with them.
Every type declaration in Ada starts with the type keyword (except for task types
(page 161)). After the type, we can see a range that looks a lot like the ranges that we
use in for loops, that defines the low and high bound of the type. Every integer in the
inclusive range of the bounds is a valid value for the type.
Another point to note in the above example is the My_Int'Image (I) expression. The
Name'Attribute (optional params) notation is used for what is called an attribute in
Ada. An attribute is a built-in operation on a type, a value, or some other program entity.
It is accessed by using a ' symbol (the ASCII apostrophe).
Ada has several types available as "built-ins"; Integer is one of them. Here is how Integer
might be defined for a typical processor:
type Integer is
range -(2 ** 31) .. +(2 ** 31 - 1);
** is the exponent operator, which means that the first valid value for Integer is -231 , and
the last valid value is 231 - 1.
Ada does not mandate the range of the built-in type Integer. An implementation for a
16-bit target would likely choose the range -215 through 215 - 1.
Unlike some other languages, Ada requires that operations on integers should be checked
for overflow.
Listing 2: main.adb
1 procedure Main is
2 A : Integer := Integer'Last;
3 B : Integer;
4 begin
5 B := A + 5;
6 -- This operation will overflow, eg. it
7 -- will raise an exception at run time.
8 end Main;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Overflow_Check
MD5: bddd15b394f043442024899d12b982fb
Build output
Runtime output
Listing 3: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4 type My_Int is range 1 .. 20;
5 A : My_Int := 12;
6 B : My_Int := 15;
7 M : My_Int := (A + B) / 2;
8 -- No overflow here, overflow checks
9 -- are done at specific boundaries.
10 begin
11 for I in 1 .. M loop
(continues on next page)
5.2. Integers 49
Learning Ada
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Overflow_Check_2
MD5: d24283cbb42c0be5b5fa215eb16ad2e7
Runtime output
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Type-level overflow will only be checked at specific points in the execution. The result,
as we see above, is that you might have an operation that overflows in an intermediate
computation, but no exception will be raised because the final result does not overflow.
Ada also features unsigned Integer types. They're called modular types in Ada parlance.
The reason for this designation is due to their behavior in case of overflow: They simply
"wrap around", as if a modulo operation was applied.
For machine sized modular types, for example a modulus of 232 , this mimics the most
common implementation behavior of unsigned types. However, an advantage of Ada is
that the modulus is more general:
Listing 4: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4 type Mod_Int is mod 2 ** 5;
5 -- ^ Range is 0 .. 31
6
10 M : constant Mod_Int := A + B;
11 -- No overflow here,
12 -- M = (20 + 15) mod 32 = 3
13 begin
14 for I in 1 .. M loop
15 Put_Line ("Hello, World!");
(continues on next page)
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Unsigned_Types
MD5: df4efee4eb29e7ea15a0cf961b600dd5
Runtime output
Hello, World!
Hello, World!
Hello, World!
Unlike in C/C++, since this wraparound behavior is guaranteed by the Ada specification,
you can rely on it to implement portable code. Also, being able to leverage the wrapping
on arbitrary bounds is very useful — the modulus does not need to be a power of 2 — to
implement certain algorithms and data structures, such as ring buffers10 .
5.4 Enumerations
Enumeration types are another nicety of Ada's type system. Unlike C's enums, they are not
integers, and each new enumeration type is incompatible with other enumeration types.
Enumeration types are part of the bigger family of discrete types, which makes them usable
in certain situations that we will describe later but one context that we have already seen
is a case statement.
Listing 5: enumeration_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Enumeration_Example is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7 -- An enumeration type
8 begin
9 for I in Days loop
10 case I is
11 when Saturday .. Sunday =>
12 Put_Line ("Week end!");
13
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Enumeration_Example
MD5: 45d6c83992af4fb6d5015d5f22cb7113
Runtime output
10 https://en.wikipedia.org/wiki/Circular_buffer
5.4. Enumerations 51
Learning Ada
Hello on MONDAY
Hello on TUESDAY
Hello on WEDNESDAY
Hello on THURSDAY
Hello on FRIDAY
Week end!
Week end!
Enumeration types are powerful enough that, unlike in most languages, they're used to
define the standard Boolean type:
As mentioned previously, every "built-in" type in Ada is defined with facilities generally
available to the user.
Like most languages, Ada supports floating-point types. The most commonly used floating-
point type is Float:
Listing 6: floating_point_demo.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Floating_Point_Demo is
4 A : constant Float := 2.5;
5 begin
6 Put_Line ("The value of A is "
7 & Float'Image (A));
8 end Floating_Point_Demo;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Floating_Point_Demo
MD5: 06998775497b68b742700138faecbb6a
Runtime output
Listing 7: floating_point_operations.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Floating_Point_Operations is
4 A : Float := 2.5;
5 begin
(continues on next page)
10 A := A ** 2 + 1.0;
11 Put_Line ("The value of A is "
12 & Float'Image (A));
13 end Floating_Point_Operations;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Floating_Point_Operations
MD5: c280e0f23e020aaee1a8777e7fb4c242
Runtime output
The value of A is 2.0 after the first operation and 5.0 after the second operation.
In addition to Float, an Ada implementation may offer data types with higher precision such
as Long_Float and Long_Long_Float. Like Float, the standard does not indicate the exact
precision of these types: it only guarantees that the type Long_Float, for example, has at
least the precision of Float. In order to guarantee that a certain precision requirement is
met, we can define custom floating-point types, as we will see in the next section.
Ada allows the user to specify the precision for a floating-point type, expressed in terms
of decimal digits. Operations on these custom types will then have at least the specified
precision. The syntax for a simple floating-point type declaration is:
The compiler will choose a floating-point representation that supports the required preci-
sion. For example:
Listing 8: custom_floating_types.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Custom_Floating_Types is
4 type T3 is digits 3;
5 type T15 is digits 15;
6 type T18 is digits 18;
7 begin
8 Put_Line ("T3 requires "
9 & Integer'Image (T3'Size)
10 & " bits");
11 Put_Line ("T15 requires "
12 & Integer'Image (T15'Size)
13 & " bits");
14 Put_Line ("T18 requires "
15 & Integer'Image (T18'Size)
16 & " bits");
17 end Custom_Floating_Types;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Custom_Floating_Types
MD5: 3c23738f13e081038996c533da8fb723
Runtime output
T3 requires 32 bits
T15 requires 64 bits
T18 requires 128 bits
In this example, the attribute 'Size is used to retrieve the number of bits used for the
specified data type. As we can see by running this example, the compiler allocates 32
bits for T3, 64 bits for T15 and 128 bits for T18. This includes both the mantissa and the
exponent.
The number of digits specified in the data type is also used in the format when displaying
floating-point variables. For example:
Listing 9: display_custom_floating_types.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Display_Custom_Floating_Types is
4 type T3 is digits 3;
5 type T18 is digits 18;
6
7 C1 : constant := 1.0e-4;
8
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Display_Custom_Floating_Types
MD5: 58ec2660388a7f05e139f73e94303cf1
Runtime output
As expected, the application will display the variables according to specified precision
(1.00E+00 and 1.00010000000000000E+00).
In addition to the precision, a range can also be specified for a floating-point type. The
syntax is similar to the one used for integer data types — using the range keyword. This
simple example creates a new floating-point type based on the type Float, for a normalized
range between -1.0 and 1.0:
3 procedure Floating_Point_Range is
4 type T_Norm is new Float range -1.0 .. 1.0;
5 A : T_Norm;
6 begin
7 A := 1.0;
8 Put_Line ("The value of A is "
9 & T_Norm'Image (A));
10 end Floating_Point_Range;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Floating_Point_Range
MD5: b43d596682aa0fa11124a3a3d0596abc
Runtime output
The application is responsible for ensuring that variables of this type stay within this range;
otherwise an exception is raised. In this example, the exception Constraint_Error is
raised when assigning 2.0 to the variable A:
3 procedure Floating_Point_Range_Exception is
4 type T_Norm is new Float range -1.0 .. 1.0;
5 A : T_Norm;
6 begin
7 A := 2.0;
8 Put_Line ("The value of A is "
9 & T_Norm'Image (A));
10 end Floating_Point_Range_Exception;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Floating_Point_Range_
↪Exception
MD5: ecda66589ba28e453956dca159ea5f0d
Build output
Runtime output
Ranges can also be specified for custom floating-point types. For example:
4 procedure Custom_Range_Types is
5 type T6_Inv_Trig is
6 digits 6 range -Pi / 2.0 .. Pi / 2.0;
7 begin
8 null;
9 end Custom_Range_Types;
In this example, we are defining a type called T6_Inv_Trig, which has a range from -π /
2 to π / 2 with a minimum precision of 6 digits. (Pi is defined in the predefined package
Ada.Numerics.)
As noted earlier, Ada is strongly typed. As a result, different types of the same family are
incompatible with each other; a value of one type cannot be assigned to a variable from
the other type. For example:
3 procedure Illegal_Example is
4 -- Declare two different floating point types
5 type Meters is new Float;
6 type Miles is new Float;
7
8 Dist_Imperial : Miles;
9
10 -- Declare a constant
11 Dist_Metric : constant Meters := 1000.0;
12 begin
13 -- Not correct: types mismatch
14 Dist_Imperial := Dist_Metric * 621.371e-6;
15 Put_Line (Miles'Image (Dist_Imperial));
16 end Illegal_Example;
Build output
illegal_example.adb:14:33: error: expected type "Miles" defined at line 6
illegal_example.adb:14:33: error: found type "Meters" defined at line 5
gprbuild: *** compilation phase failed
A consequence of these rules is that, in the general case, a "mixed mode" expression like
2 * 3.0 will trigger a compilation error. In a language like C or Python, such expressions
are made valid by implicit conversions. In Ada, such conversions must be made explicit:
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Imperial_Metric
MD5: e455641e86227e80e5f920b5af6315d4
Runtime output
6.21371E-01
Of course, we probably do not want to write the conversion code every time we convert
from meters to miles. The idiomatic Ada way in that case would be to introduce conversion
functions along with the types.
3 procedure Conv is
4 type Meters is new Float;
5 type Miles is new Float;
6
15 Dist_Imperial : Miles;
16 Dist_Metric : constant Meters := 1000.0;
17 begin
18 Dist_Imperial := To_Miles (Dist_Metric);
19 Put_Line (Miles'Image (Dist_Imperial));
20 end Conv;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Imperial_Metric_Func
MD5: 661737fa9f130ac3070210bbf6f08214
Runtime output
6.21371E-01
If you write a lot of numeric code, having to explicitly provide such conversions might seem
painful at first. However, this approach brings some advantages. Notably, you can rely on
the absence of implicit conversions, which will in turn prevent some subtle errors.
In other languages
In C, for example, the rules for implicit conversions may not always be completely
obvious. In Ada, however, the code will always do exactly what it seems to do.
For example:
int a = 3, b = 2;
float f = a / b;
This code will compile fine, but the result of f will be 1.0 instead of 1.5, because
the compiler will generate an integer division (three divided by two) that results
in one. The software developer must be aware of data conversion issues and use
an appropriate casting:
int a = 3, b = 2;
float f = (float)a / b;
In the corrected example, the compiler will convert both variables to their corre-
sponding floating-point representation before performing the division. This will
produce the expected result.
This example is very simple, and experienced C developers will probably notice
and correct it before it creates bigger problems. However, in more complex ap-
plications where the type declaration is not always visible — e.g. when referring
to elements of a struct — this situation might not always be evident and quickly
lead to software defects that can be harder to find.
The Ada compiler, in contrast, will always reject code that mixes floating-point
and integer variables without explicit conversion. The following Ada code, based
on the erroneous example in C, will not compile:
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Implicit_Cast
MD5: 38a8fcc6608c22e22940052ab8dd62f4
Build output
The offending line must be changed to F := Float (A) / Float (B); in order
to be accepted by the compiler.
You can use Ada's strong typing to help enforce invariants in your code, as in the example
above: Since Miles and Meters are two different types, you cannot mistakenly convert an
instance of one to an instance of the other.
In Ada you can create new types based on existing ones. This is very useful: you get a type
that has the same properties as some existing type but is treated as a distinct type in the
interest of strong typing.
11 SSN : Social_Security_Number :=
12 555_55_5555;
13 -- ^ You can put underscores as
14 -- formatting in any number.
15
16 I : Integer;
17
26 -- Likewise illegal:
27 SSN := I;
28
32 -- Likewise OK:
33 SSN := Social_Security_Number (I);
34 end Main;
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Derived_Types
MD5: 63445601ddb5e52dceab095d3305623a
Build output
The type Social_Security is said to be a derived type; its parent type is Integer.
As illustrated in this example, you can refine the valid range when defining a derived scalar
type (such as integer, floating-point and enumeration).
The syntax for enumerations uses the range <range> syntax:
3 procedure Greet is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Days
MD5: 853b5c1576961c7c20d4306275122364
5.8 Subtypes
As we are starting to see, types may be used in Ada to enforce constraints on the valid
range of values. However, we sometimes want to enforce constraints on some values while
staying within a single type. This is where subtypes come into play. A subtype does not
introduce a new type.
3 procedure Greet is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7
8 -- Declaration of a subtype
9 subtype Weekend_Days is
10 Days range Saturday .. Sunday;
11 -- ^ Constraint of the subtype
12
13 M : Days := Sunday;
14
Runtime output
Hello on MONDAY
Hello on TUESDAY
Hello on WEDNESDAY
Hello on THURSDAY
Hello on FRIDAY
Week end!
Week end!
Several subtypes are predefined in the standard package in Ada, and are automatically
available to you:
subtype Natural is Integer range 0 .. Integer'Last;
subtype Positive is Integer range 1 .. Integer'Last;
While subtypes of a type are statically compatible with each other, constraints are enforced
at run time: if you violate a subtype constraint, an exception will be raised.
3 procedure Greet is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7
8 subtype Weekend_Days is
9 Days range Saturday .. Sunday;
10
5.8. Subtypes 61
Learning Ada
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Days_Subtype_Error
MD5: 84d42d276d26544f35edab5870459378
Build output
Runtime output
Previously, we've seen that we can create new types by declaring e.g. type Miles is new
Float. We could also create type aliases, which generate alternative names — aliases —
for known types. Note that type aliases are sometimes called type synonyms.
We achieve this in Ada by using subtypes without new constraints. In this case, however,
we don't get all of the benefits of Ada's strong type checking. Let's rewrite an example
using type aliases:
3 procedure Undetected_Imperial_Metric_Error is
4 -- Declare two type aliases
5 subtype Meters is Float;
6 subtype Miles is Float;
7
8 Dist_Imperial : Miles;
9
10 -- Declare a constant
11 Dist_Metric : constant Meters := 100.0;
12 begin
13 -- No conversion to Miles type required:
14 Dist_Imperial := (Dist_Metric * 1609.0)
15 / 1000.0;
16
Project: Courses.Intro_To_Ada.Strongly_Typed_Language.Undetected_Imperial_Metric_
↪Error
MD5: cdb8f949c69f3c480502b859dac298ee
Runtime output
1.00000E+02
In the example above, the fact that both Meters and Miles are subtypes of Float allows
us to mix variables of both types without type conversion. This, however, can lead to all
sorts of programming mistakes that we'd like to avoid, as we can see in the undetected
error highlighted in the code above. In that example, the error in the assignment of a value
in meters to a variable meant to store values in miles remains undetected because both
Meters and Miles are subtypes of Float. Therefore, the recommendation is to use strong
typing — via type X is new Y — for cases such as the one above.
There are, however, many situations where type aliases are useful. For example, in an
application that uses floating-point types in multiple contexts, we could use type aliases
to indicate additional meaning to the types or to avoid long variable names. For example,
instead of writing:
We could write:
In other languages
In C, for example, we can use a typedef declaration to create a type alias. For example:
This corresponds to the declaration that we've seen above using subtypes. Other program-
ming languages include this concept in similar ways. For example:
• C++: using meters = float;
• Swift: typealias Meters = Double
• Kotlin: typealias Meters = Double
• Haskell: type Meters = Float
Note, however, that subtypes in Ada correspond to type aliases if, and only if, they don't
have new constraints. Thus, if we add a new constraint to a subtype declaration, we don't
have a type alias anymore. For example, the following declaration can't be considered a
type alias of Float:
subtype Liquid_Water_Temperature is
Degree_Celsius range 0.0 .. 100.0;
subtype Running_Water_Temperature is
Liquid_Water_Temperature;
5.8. Subtypes 63
Learning Ada
SIX
RECORDS
So far, all the types we have encountered have values that are not decomposable: each
instance represents a single piece of data. Now we are going to see our first class of com-
posite types: records.
Records allow composing a value out of instances of other types. Each of those instances
will be given a name. The pair consisting of a name and an instance of a specific type is
called a field, or a component.
Fields look a lot like variable declarations, except that they are inside of a record definition.
And as with variable declarations, you can specify additional constraints when supplying
the subtype of the field.
Record components can have default values. When a variable having the record type is
declared, a field with a default initialization will be automatically set to this value. The
value can be any expression of the component type, and may be run-time computable.
In the remaining sections of this chapter, we see how to use record types. In addition to
that, we discuss more about records in another chapter (page 101).
65
Learning Ada
6.2 Aggregates
-- Positional components
Ada_Birthday : Date := (10, December, 1815);
-- Named components
Leap_Day_2020 : Date := (Day => 29,
Month => February,
Year => 2020);
-- ^ By name
Records have a convenient notation for expressing values, illustrated above. This notation
is called aggregate notation, and the literals are called aggregates. They can be used in
a variety of contexts that we will see throughout the course, one of which is to initialize
records.
An aggregate is a list of values separated by commas and enclosed in parentheses. It is
allowed in any context where a value of the record is expected.
Values for the components can be specified positionally, as in Ada_Birthday example, or
by name, as in Leap_Day_2020. A mixture of positional and named values is permitted, but
you cannot use a positional notation after a named one.
To access components of a record instance, you use an operation that is called component
selection. This is achieved by using the dot notation. For example, if we declare a variable
Some_Day of the Date record type mentioned above, we can access the Year component
by writing Some_Day.Year.
Let's look at an example:
Listing 1: record_selection.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Record_Selection is
4
5 type Months is
6 (January, February, March, April,
7 May, June, July, August, September,
8 October, November, December);
9
66 Chapter 6. Records
Learning Ada
27 begin
28 Display_Date (Some_Day);
29
33 Display_Date (Some_Day);
34 end Record_Selection;
Project: Courses.Intro_To_Ada.Records.Record_Selection
MD5: 79602cf4d011ba7423d07772b13e2b5a
Runtime output
As you can see in this example, we can use the dot notation in the expression D.Year or
Some_Day.Year to access the information stored in that component, as well as to mod-
ify this information in assignments. To be more specific, when we use D.Year in the call
to Put_Line, we're retrieving the information stored in that component. When we write
Some_Day.Year := 2001, we're overwriting the information that was previously stored in
the Year component of Some_Day.
6.4 Renaming
In previous chapters, we've discussed subprogram (page 32) and package (page 46) re-
naming. We can rename record components as well. Instead of writing the full component
selection using the dot notation, we can declare an alias that allows us to access the same
component. This is useful to simplify the implementation of a subprogram, for example.
We can rename record components by using the renames keyword in a variable declaration.
For example:
Some_Day : Date;
Y : Integer renames Some_Day.Year;
Here, Y is an alias, so that every time we using Y, we are really using the Year component
of Some_Day.
Let's look at a complete example:
Listing 2: dates.ads
1 package Dates is
2
3 type Months is
4 (January, February, March, April,
5 May, June, July, August, September,
6 October, November, December);
7
6.4. Renaming 67
Learning Ada
14 procedure Increase_Month
15 (Some_Day : in out Date);
16
17 procedure Display_Month
18 (Some_Day : Date);
19
20 end Dates;
Listing 3: dates.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Increase_Month
6 (Some_Day : in out Date)
7 is
8 -- Renaming components from
9 -- the Date record
10 M : Months renames Some_Day.Month;
11 Y : Integer renames Some_Day.Year;
12
27 procedure Display_Month
28 (Some_Day : Date)
29 is
30 -- Renaming components from
31 -- the Date record
32 M : Months renames Some_Day.Month;
33 Y : Integer renames Some_Day.Year;
34 begin
35 Put_Line ("Month: "
36 & Months'Image (M)
37 & ", Year:"
38 & Integer'Image (Y));
39 end Display_Month;
40
41 end Dates;
Listing 4: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Dates; use Dates;
3
68 Chapter 6. Records
Learning Ada
12 Display_Month (D);
13 end Main;
Project: Courses.Intro_To_Ada.Arrays.Record_Component_Renaming
MD5: 905390bd02b8417039052218800975a3
Runtime output
We apply renaming to two components of the Date record in the implementation of the In-
crease_Month procedure. Then, instead of directly using Some_Day.Month and Some_Day.
Year in the next operations, we simply use the renamed versions M and Y.
Note that, in the example above, we also rename Months'Succ — which is the function that
gives us the next month — to Next.
6.4. Renaming 69
Learning Ada
70 Chapter 6. Records
CHAPTER
SEVEN
ARRAYS
Arrays in Ada are used to define contiguous collections of elements that can be selected by
indexing. Here's a simple example:
Listing 1: greet.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet is
4 type My_Int is range 0 .. 1000;
5 type Index is range 1 .. 5;
6
7 type My_Int_Array is
8 array (Index) of My_Int;
9 -- ^ Type of elements
10 -- ^ Bounds of the array
11 Arr : My_Int_Array := (2, 3, 5, 7, 11);
12 -- ^ Array literal
13 -- (aggregate)
14
15 V : My_Int;
16 begin
17 for I in Index loop
18 V := Arr (I);
19 -- ^ Take the Ith element
20 Put (My_Int'Image (V));
21 end loop;
22 New_Line;
23 end Greet;
Project: Courses.Intro_To_Ada.Arrays.Greet
MD5: ffdd2ba2322b0946dfcac3a55bce5270
Runtime output
2 3 5 7 11
The first point to note is that we specify the index type for the array, rather than its size.
Here we declared an integer type named Index ranging from 1 to 5, so each array instance
will have 5 elements, with the initial element at index 1 and the last element at index 5.
71
Learning Ada
Although this example used an integer type for the index, Ada is more general: any discrete
type is permitted to index an array, including Enum types (page 51). We will soon see what
that means.
Another point to note is that querying an element of the array at a given index uses the same
syntax as for function calls: that is, the array object followed by the index in parentheses.
Thus when you see an expression such as A (B), whether it is a function call or an array
subscript depends on what A refers to.
Finally, notice how we initialize the array with the (2, 3, 5, 7, 11) expression. This
is another kind of aggregate in Ada, and is in a sense a literal expression for an array, in
the same way that 3 is a literal expression for an integer. The notation is very powerful,
with a number of properties that we will introduce later. A detailed overview appears in the
notation of aggregate types (page 89).
Unrelated to arrays, the example also illustrated two procedures from Ada.Text_IO:
• Put, which displays a string without a terminating end of line
• New_Line, which outputs an end of line
Let's now delve into what it means to be able to use any discrete type to index into the
array.
In other languages
Semantically, an array object in Ada is the entire data structure, and not simply a handle or
pointer. Unlike C and C++, there is no implicit equivalence between an array and a pointer
to its initial element.
Listing 2: array_bounds_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Array_Bounds_Example is
4 type My_Int is range 0 .. 1000;
5
10 type My_Int_Array is
11 array (Index) of My_Int;
12
Project: Courses.Intro_To_Ada.Arrays.Array_Bounds_Example
MD5: e5fe9e7b83055f3ae23dd890e29c22de
Runtime output
2 3 5 7 11
72 Chapter 7. Arrays
Learning Ada
One effect is that the bounds of an array can be any values. In the first example we con-
structed an array type whose first index is 1, but in the example above we declare an array
type whose first index is 11.
That's perfectly fine in Ada, and moreover since we use the index type as a range to iterate
over the array indices, the code using the array does not need to change.
That leads us to an important consequence with regard to code dealing with arrays. Since
the bounds can vary, you should not assume / hard-code specific bounds when iterating /
using arrays. That means the code above is good, because it uses the index type, but a for
loop as shown below is bad practice even though it works correctly:
for I in 11 .. 15 loop
Tab (I) := Tab (I) * 2;
end loop;
Since you can use any discrete type to index an array, enumeration types are permitted.
Listing 3: month_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Month_Example is
4 type Month_Duration is range 1 .. 31;
5 type Month is (Jan, Feb, Mar, Apr,
6 May, Jun, Jul, Aug,
7 Sep, Oct, Nov, Dec);
8
9 type My_Int_Array is
10 array (Month) of Month_Duration;
11 -- ^ Can use an enumeration type
12 -- as the index
13
Runtime output
JAN has 31 days.
FEB has 28 days.
MAR has 31 days.
(continues on next page)
7.2 Indexing
We have already seen the syntax for selecting elements of an array. There are however a
few more points to note.
First, as is true in general in Ada, the indexing operation is strongly typed. If you use a
value of the wrong type to index the array, you will get a compile-time error.
Listing 4: greet.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet is
4 type My_Int is range 0 .. 1000;
5
9 type My_Int_Array is
10 array (My_Index) of My_Int;
11
Project: Courses.Intro_To_Ada.Arrays.Greet_2
MD5: 54543017e4ec69d24bf9e43d507b50e6
Build output
74 Chapter 7. Arrays
Learning Ada
Second, arrays in Ada are bounds checked. This means that if you try to access an element
outside of the bounds of the array, you will get a run-time error instead of accessing random
memory as in unsafe languages.
Listing 5: greet.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Greet is
4 type My_Int is range 0 .. 1000;
5 type Index is range 1 .. 5;
6
7 type My_Int_Array is
8 array (Index) of My_Int;
9
Project: Courses.Intro_To_Ada.Arrays.Greet_3
MD5: 0102674d089be838f1dfbf0791d99fce
Build output
greet.adb:12:30: warning: static value out of range of type "Index" defined at␣
↪line 5 [enabled by default]
Runtime output
7.2. Indexing 75
Learning Ada
In the previous examples, we have always explicitly created an index type for the array.
While this can be useful for typing and readability purposes, sometimes you simply want
to express a range of values. Ada allows you to do that, too.
Listing 6: simple_array_bounds.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Simple_Array_Bounds is
4 type My_Int is range 0 .. 1000;
5
6 type My_Int_Array is
7 array (1 .. 5) of My_Int;
8 -- ^ Subtype of Integer
9
Project: Courses.Intro_To_Ada.Arrays.Simple_Array_Bounds
MD5: c337a7fe0dacccc5f60f7b234aa96d39
Runtime output
2 3 5 7 11
This example defines the range of the array via the range syntax, which specifies an anony-
mous subtype of Integer and uses it to index the array.
This means that the type of the index is Integer. Similarly, when you use an anonymous
range in a for loop as in the example above, the type of the iteration variable is also Integer,
so you can use I to index Tab.
You can also use a named subtype for the bounds for an array.
We noted earlier that hard coding bounds when iterating over an array is a bad idea, and
showed how to use the array's index type/subtype to iterate over its range in a for loop.
That raises the question of how to write an iteration when the array has an anonymous
range for its bounds, since there is no name to refer to the range. Ada solves that via
several attributes of array objects:
Listing 7: range_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Range_Example is
(continues on next page)
76 Chapter 7. Arrays
Learning Ada
6 type My_Int_Array is
7 array (1 .. 5) of My_Int;
8
Project: Courses.Intro_To_Ada.Arrays.Range_Example
MD5: 8b0d7bf346cb59999dfd12dbaaf3e2a6
Runtime output
2 3 5 7 11
If you want more fine grained control, you can use the separate attributes 'First and
'Last.
Listing 8: array_attributes_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Array_Attributes_Example is
4 type My_Int is range 0 .. 1000;
5
6 type My_Int_Array is
7 array (1 .. 5) of My_Int;
8
9 Tab : My_Int_Array :=
10 (2, 3, 5, 7, 11);
11 begin
12 for I in Tab'First .. Tab'Last - 1 loop
13 -- ^ Iterate on every index
14 -- except the last
15 Put (My_Int'Image (Tab (I)));
16 end loop;
17 New_Line;
18 end Array_Attributes_Example;
Project: Courses.Intro_To_Ada.Arrays.Array_Attributes_Example
MD5: 95cc407c8aadd936e050fe3505e8fb46
Runtime output
2 3 5 7
The 'Range, 'First and 'Last attributes in these examples could also have been applied
to the array type name, and not just the array instances.
Although not illustrated in the above examples, another useful attribute for an array in-
stance A is A'Length, which is the number of elements that A contains.
It is legal and sometimes useful to have a "null array", which contains no elements. To get
this effect, define an index range whose upper bound is less than the lower bound.
Let's now consider one of the most powerful aspects of Ada's array facility.
Every array type we have defined so far has a fixed size: every instance of this type will
have the same bounds and therefore the same number of elements and the same size.
However, Ada also allows you to declare array types whose bounds are not fixed: in that
case, the bounds will need to be provided when creating instances of the type.
Listing 9: unconstrained_array_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Unconstrained_Array_Example is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7
8 type Workload_Type is
9 array (Days range <>) of Natural;
10 -- Indefinite array type
11 -- ^ Bounds are of type Days,
12 -- but not known
13
14 Workload : constant
15 Workload_Type (Monday .. Friday) :=
16 -- ^ Specify the bounds
17 -- when declaring
18 (Friday => 7, others => 8);
19 -- ^ Default value
20 -- ^ Specify element by name of index
21 begin
22 for I in Workload'Range loop
23 Put_Line (Integer'Image (Workload (I)));
24 end loop;
25 end Unconstrained_Array_Example;
Project: Courses.Intro_To_Ada.Arrays.Unconstrained_Array_Example
MD5: c84910e9b424cfabbbbe018ba0a6de59
Runtime output
8
8
8
8
7
The fact that the bounds of the array are not known is indicated by the Days range <>
syntax. Given a discrete type Discrete_Type, if we use Discrete_Type for the index in an
array type then Discrete_Type serves as the type of the index and comprises the range
of index values for each array instance.
If we define the index as Discrete_Type range <> then Discrete_Type serves as the type
of the index, but different array instances may have different bounds from this type.
78 Chapter 7. Arrays
Learning Ada
An array type that is defined with the Discrete_Type range <> syntax for its index is
referred to as an unconstrained array type, and, as illustrated above, the bounds need to
be provided when an instance is created.
The above example also shows other forms of the aggregate syntax. You can specify asso-
ciations by name, by giving the value of the index on the left side of an arrow association.
1 => 2 thus means "assign value 2 to the element at index 1 in my array". others => 8
means "assign value 8 to every element that wasn't previously assigned in this aggregate".
Attention: The so-called "box" notation (<>) is commonly used as a wildcard or place-
holder in Ada. You will often see it when the meaning is "what is expected here can be
anything".
In other languages
While unconstrained arrays in Ada might seem similar to variable length arrays in C, they
are in reality much more powerful, because they're truly first-class values in the language.
You can pass them as parameters to subprograms or return them from functions, and they
implicitly contain their bounds as part of their value. This means that it is useless to pass
the bounds or length of an array explicitly along with the array, because they are accessible
via the 'First, 'Last, 'Range and 'Length attributes explained earlier.
Although different instances of the same unconstrained array type can have different
bounds, a specific instance has the same bounds throughout its lifetime. This allows Ada
to implement unconstrained arrays efficiently; instances can be stored on the stack and do
not require heap allocation as in languages like Java.
A recurring theme in our introduction to Ada types has been the way important built-in
types like Boolean or Integer are defined through the same facilities that are available to
the user. This is also true for strings: The String type in Ada is a simple array.
Here is how the string type is defined in Ada:
type String is
array (Positive range <>) of Character;
The only built-in feature Ada adds to make strings more ergonomic is custom literals, as we
can see in the example below.
Hint: String literals are a syntactic sugar for aggregates, so that in the following example,
A and B have the same value.
Project: Courses.Intro_To_Ada.Arrays.String_Literals
MD5: 8e5871c8ead4ff8da643539857e23b30
3 procedure Greet is
4 Message : String (1 .. 11) := "dlroW olleH";
5 -- ^ Pre-defined array type.
6 -- Component type is Character
7 begin
8 for I in reverse Message'Range loop
9 -- ^ Iterate in reverse order
10 Put (Message (I));
11 end loop;
12 New_Line;
13 end Greet;
However, specifying the bounds of the object explicitly is a bit of a hassle; you have to
manually count the number of characters in the literal. Fortunately, Ada gives you an
easier way.
You can omit the bounds when creating an instance of an unconstrained array type if you
supply an initialization, since the bounds can be deduced from the initialization expression.
3 procedure Greet is
4 Message : constant String := "dlroW olleH";
5 -- ^ Bounds are automatically
6 -- computed from
7 -- initialization value
8 begin
9 for I in reverse Message'Range loop
10 Put (Message (I));
11 end loop;
12 New_Line;
13 end Greet;
Project: Courses.Intro_To_Ada.Arrays.Greet_5
MD5: 21448a1007a07ec9d434880628625c3f
Runtime output
Hello World
3 procedure Main is
4 type Integer_Array is
5 array (Natural range <>) of Integer;
6
80 Chapter 7. Arrays
Learning Ada
Attention: As you can see above, the standard String type in Ada is an array. As such,
it shares the advantages and drawbacks of arrays: a String value is stack allocated, it
is accessed efficiently, and its bounds are immutable.
If you want something akin to C++'s std::string, you can use Unbounded Strings
(page 246) from Ada's standard library. This type is more like a mutable, automatically
managed string buffer to which you can add content.
7.7 Restrictions
A very important point about arrays: bounds have to be known when instances are created.
It is for example illegal to do the following.
declare
A : String;
begin
A := "World";
end;
Also, while you of course can change the values of elements in an array, you cannot change
the array's bounds (and therefore its size) after it has been initialized. So this is also illegal:
declare
A : String := "Hello";
begin
A := "World"; -- OK: Same size
A := "Hello World"; -- Not OK: Different size
end;
Also, while you can expect a warning for this kind of error in very simple cases like this one,
it is impossible for a compiler to know in the general case if you are assigning a value of
the correct length, so this violation will generally result in a run-time error.
Attention
While we will learn more about this later, it is important to know that arrays are
not the only types whose instances might be of unknown size at compile-time.
Such objects are said to be of an indefinite subtype, which means that the subtype
size is not known at compile time, but is dynamically computed (at run time).
3 procedure Indefinite_Subtypes is
(continues on next page)
7.7. Restrictions 81
Learning Ada
9 A : String := "Hello";
10 -- Indefinite subtype
11
12 B : String (1 .. 5) := "Hello";
13 -- Definite subtype
14
15 C : String (1 .. Get_Number);
16 -- Indefinite subtype
17 -- (Get_Number's value is computed at
18 -- run-time)
19 begin
20 null;
21 end Indefinite_Subtypes;
Project: Courses.Intro_To_Ada.Arrays.Indefinite_Subtypes
MD5: a24235838511a94879f74757421a28f0
The return type of a function can be any type; a function can return a value whose size is
unknown at compile time. Likewise, the parameters can be of any type.
For example, this is a function that returns an unconstrained String:
3 procedure Main is
4
82 Chapter 7. Arrays
Learning Ada
Runtime output
First day is Monday
(This example is for illustrative purposes only. There is a built-in mechanism, the 'Image
attribute for scalar types, that returns the name (as a String) of any element of an enu-
meration type. For example Days'Image(Monday) is "MONDAY".)
In other languages
Returning variable size objects in languages lacking a garbage collector is a bit complicated
implementation-wise, which is why C and C++ don't allow it, preferring to depend on explicit
dynamic allocation / free from the user.
The problem is that explicit storage management is unsafe as soon as you want to collect
unused memory. Ada's ability to return variable size objects will remove one use case for
dynamic allocation, and hence, remove one potential source of bugs from your programs.
Rust follows the C/C++ model, but with safe pointer semantics. However, dynamic alloca-
tion is still used. Ada can benefit from a possible performance edge because it can use any
model.
While we can have array types whose size and bounds are determined at run time, the
array's component type needs to be of a definite and constrained type.
Thus, if you need to declare, for example, an array of strings, the String subtype used as
component will need to have a fixed size.
3 procedure Show_Days is
4 type Days is (Monday, Tuesday, Wednesday,
5 Thursday, Friday,
6 Saturday, Sunday);
7
11 type Days_Name_Type is
12 array (Days) of Day_Name;
13 -- ^ Type of the index
14 -- ^ Type of the element.
15 -- Must be definite
(continues on next page)
Project: Courses.Intro_To_Ada.Arrays.Day_Name_2
MD5: bc66303091c084f66abde72ae59f55a9
Runtime output
Mo
Tu
We
Th
Fr
Sa
Su
One last feature of Ada arrays that we're going to cover is array slices. It is possible to take
and use a slice of an array (a contiguous sequence of elements) as a name or a value.
3 procedure Main is
4 Buf : String := "Hello ...";
5
Project: Courses.Intro_To_Ada.Arrays.Slices
MD5: cdf582c6c9089658236f5c79b7be4c3f
Runtime output
84 Chapter 7. Arrays
Learning Ada
Hello Bob
Hi John
As we can see above, you can use a slice on the left side of an assignment, to replace only
part of an array.
A slice of an array is of the same type as the array, but has a different subtype, constrained
by the bounds of the slice.
Attention: Ada has multidimensional arrays11 , which are not covered in this course.
Slices will only work on one dimensional arrays.
7.11 Renaming
So far, we've seen that the following elements can be renamed: subprograms (page 32),
packages (page 46), and record components (page 67). We can also rename objects by
using the renames keyword. This allows for creating alternative names for these objects.
Let's look at an example:
5 Current_Temperature : Degree_Celsius;
6
7 end Measurements;
4 procedure Main is
5 subtype Degrees is
6 Measurements.Degree_Celsius;
7
8 T : Degrees
9 renames Measurements.Current_Temperature;
10 begin
11 T := 5.0;
12
17 T := T + 2.5;
18
7.11. Renaming 85
Learning Ada
Project: Courses.Intro_To_Ada.Arrays.Variable_Renaming
MD5: 4426aeaa364cb5cf10ff40e1bccb9757
Runtime output
5.00000E+00
5.00000E+00
7.50000E+00
7.50000E+00
9 type Color_Array is
10 array (Positive range <>) of Color;
11
14 end Colors;
86 Chapter 7. Arrays
Learning Ada
23 end Colors;
5 procedure Test_Reverse_Colors is
6
7 My_Colors : Color_Array (1 .. 5) :=
8 (Black, Red, Green, Blue, White);
9
10 begin
11 for C of My_Colors loop
12 Put_Line ("My_Color: "
13 & Color'Image (C));
14 end loop;
15
16 New_Line;
17 Put_Line ("Reversing My_Color...");
18 New_Line;
19 Reverse_It (My_Colors);
20
26 end Test_Reverse_Colors;
Project: Courses.Intro_To_Ada.Arrays.Reverse_Colors
MD5: cd9fd7f64d1ec8967e340d57fd7afc0a
Runtime output
My_Color: BLACK
My_Color: RED
My_Color: GREEN
My_Color: BLUE
My_Color: WHITE
Reversing My_Color...
My_Color: WHITE
My_Color: BLUE
My_Color: GREEN
My_Color: RED
My_Color: BLACK
In the example above, package Colors implements the procedure Reverse_It by declaring
new names for two positions of the array. The actual implementation becomes easy to read:
7.11. Renaming 87
Learning Ada
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
begin
Tmp := X (I);
X (I) := X (X'Last +
X'First - I);
X (X'Last + X'First - I) := Tmp;
end;
88 Chapter 7. Arrays
CHAPTER
EIGHT
So far, we have talked about aggregates quite a bit and have seen a number of examples.
Now we will revisit this feature in some more detail.
An Ada aggregate is, in effect, a literal value for a composite type. It's a very powerful
notation that helps you to avoid writing procedural code for the initialization of your data
structures in many cases.
A basic rule when writing aggregates is that every component of the array or record has to
be specified, even components that have a default value.
This means that the following code is incorrect:
Listing 1: incorrect.ads
1 package Incorrect is
2 type Point is record
3 X, Y : Integer := 0;
4 end record;
5
Project: Courses.Intro_To_Ada.More_About_Types.Incorrect_Aggregate
MD5: 80a3475dece1c42cfb67b1d57b5bd464
Build output
There are a few shortcuts that you can use to make the notation more convenient:
• To specify the default value for a component, you can use the <> notation.
• You can use the | symbol to give several components the same value.
• You can use the others choice to refer to every component that has not yet been
specified, provided all those fields have the same type.
• You can use the range notation .. to refer to specify a contiguous sequence of indices
in an array.
However, note that as soon as you used a named association, all subsequent components
likewise need to be specified with named associations.
89
Learning Ada
Listing 2: points.ads
1 package Points is
2 type Point is record
3 X, Y : Integer := 0;
4 end record;
5
6 type Point_Array is
7 array (Positive range <>) of Point;
8
Project: Courses.Intro_To_Ada.More_About_Types.Points
MD5: 48ea183a42f203325ed6190fbd8493d9
Ada has a general concept of name overloading, which we saw earlier in the section on
enumeration types (page 51).
Let's take a simple example: it is possible in Ada to have functions that have the same
name, but different types for their parameters.
Listing 3: pkg.ads
1 package Pkg is
2 function F (A : Integer) return Integer;
3 function F (A : Character) return Integer;
4 end Pkg;
Project: Courses.Intro_To_Ada.More_About_Types.Overloading
MD5: defae85228ee183b536af395d077e71e
Listing 4: pkg.ads
1 package Pkg is
2 type SSID is new Integer;
(continues on next page)
12 https://en.wikipedia.org/wiki/Function_overloading
Listing 5: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Pkg; use Pkg;
3
4 procedure Main is
5 S : String := Convert (123_145_299);
6 -- ^ Valid, will choose the
7 -- proper Convert
8 begin
9 Put_Line (S);
10 end Main;
Attention: Note that overload resolution based on the type is allowed for both functions
and enumeration literals in Ada - which is why you can have multiple enumeration literals
with the same name. Semantically, an enumeration literal is treated like a function that
has no parameters.
Listing 6: pkg.ads
1 package Pkg is
2 type SSID is new Integer;
3
Listing 7: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Pkg; use Pkg;
3
4 procedure Main is
5 S : String := Convert (123_145_299);
6 -- ^ Invalid, which convert
7 -- should we call?
8
16 I : SSID := 123_145_299;
17
Project: Courses.Intro_To_Ada.More_About_Types.Overloading_Error
MD5: 722660d8b692cde65a1c2b7800dd78c4
Syntactically the target of a qualified expression can be either any expression in parenthe-
ses, or an aggregate:
Listing 8: qual_expr.ads
1 package Qual_Expr is
2 type Point is record
3 A, B : Integer;
4 end record;
5
8 A : Integer := Integer'(12);
9 end Qual_Expr;
Project: Courses.Intro_To_Ada.More_About_Types.Qual_Expr
MD5: e71523eb441a28a4f6549d5f0418620a
This illustrates that qualified expressions are a convenient (and sometimes necessary) way
for the programmer to make the type of an expression explicit, for the compiler of course,
but also for other programmers.
Attention: While they look and feel similar, type conversions and qualified expressions
are not the same.
A qualified expression specifies the exact type that the target expression will be resolved
to, whereas a type conversion will try to convert the target and issue a run-time error if
the target value cannot be so converted.
Note that you can use a qualified expression to convert from one subtype to another,
with an exception raised if a constraint is violated.
X : Integer := Natural'(1);
As noted earlier, each enumeration type is distinct and incompatible with every other enu-
meration type. However, what we did not mention previously is that character literals are
permitted as enumeration literals. This means that in addition to the language's strongly
typed character types, user-defined character types are also permitted:
Listing 9: character_example.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Character_Example is
4 type My_Char is ('a', 'b', 'c');
5 -- Our custom character type, an
6 -- enumeration type with 3 valid values.
7
8 C : Character;
9 -- ^ Built-in character type
10 -- (it's an enumeration type)
11
12 M : My_Char;
13 begin
14 C := '?';
15 -- ^ Character literal
16 -- (enumeration literal)
17
18 M := 'a';
19
20 C := 65;
21 -- ^ Invalid: 65 is not a
22 -- Character value
23
24 C := Character'Val (65);
25 -- Assign the character at
26 -- position 65 in the
27 -- enumeration (which is 'A')
28
29 M := C;
30 -- ^ Invalid: C is of type Character,
31 -- and M is a My_Char
32
33 M := 'd';
34 -- ^ Invalid: 'd' is not a valid
35 -- literal for type My_Char
36 end Character_Example;
Project: Courses.Intro_To_Ada.More_About_Types.Character_Example
MD5: e4c5a07dbe8745749056f8c110d69fa3
Build output
NINE
9.1 Overview
Pointers are a potentially dangerous construct, which conflicts with Ada's underlying phi-
losophy.
There are two ways in which Ada helps shield programmers from the dangers of pointers:
1. One approach, which we have already seen, is to provide alternative features so that
the programmer does not need to use pointers. Parameter modes, arrays, and varying
size types are all constructs that can replace typical pointer usages in C.
2. Second, Ada has made pointers as safe and restricted as possible, but allows "escape
hatches" when the programmer explicitly requests them and presumably will be exer-
cising such features with appropriate care.
Here is how you declare a simple pointer type, or access type, in Ada:
Listing 1: dates.ads
1 package Dates is
2 type Months is
3 (January, February, March, April,
4 May, June, July, August, September,
5 October, November, December);
6
Listing 2: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
4 -- Declare an access type
5 type Date_Acc is access Date;
6 -- ^ "Designated type"
7 -- ^ Date_Acc values
8 -- point to Date
9 -- objects
10
11 D : Date_Acc := null;
12 -- ^ Literal for
13 -- "access to nothing"
(continues on next page)
95
Learning Ada
Project: Courses.Intro_To_Ada.Access_Types.Access_Types
MD5: d3421918c48c221836bdf03b9e68bfb5
Listing 3: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
4 -- Declare an access type
5 type Date_Acc is access Date;
6 type Date_Acc_2 is access Date;
7
8 D : Date_Acc := null;
9 D2 : Date_Acc_2 := D;
10 -- ^ Invalid! Different types
11 end Access_Types;
Project: Courses.Intro_To_Ada.Access_Types.Access_Types
MD5: af0dff5a26cb16f0fe15c84286557a44
Build output
In other languages
In most other languages, pointer types are structurally, not nominally typed, like they are
in Ada, which means that two pointer types will be the same as long as they share the same
target type and accessibility rules.
Not so in Ada, which takes some time getting used to. A seemingly simple problem is, if you
want to have a canonical access to a type, where should it be declared? A commonly used
pattern is that if you need an access type to a specific type you "own", you will declare it
along with the type:
package Access_Types is
type Point is record
X, Y : Natural;
end record;
Once we have declared an access type, we need a way to give variables of the types a
meaningful value! You can allocate a value of an access type with the new keyword in Ada.
Listing 4: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
4 type Date_Acc is access Date;
5
Project: Courses.Intro_To_Ada.Access_Types.Access_Types
MD5: e0be95b966e4aebaaf25db646d60c35c
If the type you want to allocate needs constraints, you can put them in the subtype indica-
tion, just as you would do in a variable declaration:
Listing 5: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
4 type String_Acc is access String;
5 -- ^
6 -- Access to unconstrained array type
7 Msg : String_Acc;
8 -- ^ Default value is null
9
10 Buffer : String_Acc :=
11 new String (1 .. 10);
12 -- ^ Constraint required
13 end Access_Types;
Project: Courses.Intro_To_Ada.Access_Types.Access_Types
MD5: 83cf7a1074ff1b739658508098aa8208
In some cases, though, allocating just by specifying the type is not ideal, so Ada also allows
you to initialize along with the allocation. This is done via the qualified expression syntax:
Listing 6: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
(continues on next page)
7 D : Date_Acc :=
8 new Date'(30, November, 2011);
9 Msg : String_Acc := new String'("Hello");
10 end Access_Types;
9.3 Dereferencing
The last important piece of Ada's access type facility is how to get from an access value
to the object that is pointed to, that is, how to dereference the pointer. Dereferencing a
pointer uses the .all syntax in Ada, but is often not needed — in many cases, the access
value will be implicitly dereferenced for you:
Listing 7: access_types.ads
1 with Dates; use Dates;
2
3 package Access_Types is
4 type Date_Acc is access Date;
5
6 D : Date_Acc :=
7 new Date'(30, November, 2011);
8
Project: Courses.Intro_To_Ada.Access_Types.Access_Types
MD5: 5cd1c259da04010b0dc1b43e9bd93b55
As you might know if you have used pointers in C or C++, we are still missing features that
are considered fundamental to the use of pointers, such as:
• Pointer arithmetic (being able to increment or decrement a pointer in order to point to
the next or previous object)
• Manual deallocation - what is called free or delete in C. This is a potentially unsafe
operation. To keep within the realm of safe Ada, you need to never deallocate manually.
Those features exist in Ada, but are only available through specific standard library APIs.
Attention: The guideline in Ada is that most of the time you can avoid manual alloca-
tion, and you should.
There are many ways to avoid manual allocation, some of which have been covered
(such as parameter modes). The language also provides library abstractions to avoid
pointers:
1. One is the use of containers (page 203). Containers help users avoid pointers,
because container memory is automatically managed.
2. A container to note in this context is the Indefinite holder13 . This container allows
you to store a value of an indefinite type such as String.
3. GNATCOLL has a library for smart pointers, called Refcount14 Those pointers' mem-
ory is automatically managed, so that when an allocated object has no more refer-
ences to it, the memory is automatically deallocated.
The linked list is a common idiom in data structures; in Ada this would be most naturally
defined through two types, a record type and an access type, that are mutually dependent.
To declare mutually dependent types, you can use an incomplete type declaration:
Listing 8: simple_list.ads
1 package Simple_List is
2 type Node;
3 -- This is an incomplete type declaration,
4 -- which is completed in the same
5 -- declarative region.
6
Project: Courses.Intro_To_Ada.Access_Types.Simple_List
MD5: 4929b89c1fc913da635fa02e48248271
In this example, the Node and Node_Acc types are mutually dependent.
13 http://www.ada-auth.org/standards/12rat/html/Rat12-8-5.html
14 https://github.com/AdaCore/gnatcoll-core/blob/master/src/gnatcoll-refcount.ads
TEN
We have previously seen some simple examples of record types (page 65). Let's now look
at some of the more advanced properties of this fundamental language feature.
One point to note is that object size for a record type does not need to be known at compile
time. This is illustrated in the example below:
Listing 1: runtime_length.ads
1 package Runtime_Length is
2 function Compute_Max_Len return Natural;
3 end Runtime_Length;
Listing 2: var_size_record.ads
1 with Runtime_Length; use Runtime_Length;
2
3 package Var_Size_Record is
4 Max_Len : constant Natural :=
5 Compute_Max_Len;
6 -- ^ Not known at compile time
7
8 type Items_Array is
9 array (Positive range <>) of Integer;
10
18 G : Growable_Stack;
19 end Var_Size_Record;
Project: Courses.Intro_To_Ada.More_About_Records.Var_Size_Record
MD5: 6fb0b3f2b685a72ec694640ce378f77c
It is completely fine to determine the size of your records at run time, but note that all
objects of this type will have the same size.
101
Learning Ada
In the example above, the size of the Items field is determined once, at run-time, but every
Growable_Stack instance will be exactly the same size. But maybe that's not what you
want to do. We saw that arrays in general offer this flexibility: for an unconstrained array
type, different objects can have different sizes.
You can get analogous functionality for records, too, using a special kind of field that is
called a discriminant:
Listing 3: var_size_record_2.ads
1 package Var_Size_Record_2 is
2 type Items_Array is
3 array (Positive range <>) of Integer;
4
Project: Courses.Intro_To_Ada.More_About_Records.Var_Size_Record_2
MD5: 0c2ffe41b7553984e1ef48a50386559f
Discriminants, in their simple forms, are constant: You cannot modify them once you have
initialized the object. This intuitively makes sense since they determine the size of the
object.
Also, they make a type indefinite: Whether or not the discriminant is used to specify the size
of an object, a type with a discriminant will be indefinite if the discriminant is not declared
with an initialization:
Listing 4: test_discriminants.ads
1 package Test_Discriminants is
2 type Point (X, Y : Natural) is record
3 null;
4 end record;
5
6 P : Point;
7 -- ERROR: Point is indefinite, so you
8 -- need to specify the discriminants
9 -- or give a default value
10
15 end Test_Discriminants;
Project: Courses.Intro_To_Ada.More_About_Records.Test_Discriminants
MD5: c3ec81ccae0d4144fe952ad99482be81
Build output
This also means that, in the example above, you cannot declare an array of Point values,
because the size of a Point is not known.
As mentioned in the example above, we could provide a default value for the discriminants,
so that we could legally declare Point values without specifying the discriminants. For the
example above, this is how it would look:
Listing 5: test_discriminants.ads
1 package Test_Discriminants is
2 type Point (X, Y : Natural := 0) is record
3 null;
4 end record;
5
6 P : Point;
7 -- We can now simply declare a "Point"
8 -- without further ado. In this case,
9 -- we're using the default values (0)
10 -- for X and Y.
11
16 end Test_Discriminants;
Project: Courses.Intro_To_Ada.More_About_Records.Test_Discriminants
MD5: 259f6cdf7fa857cc006dac6d1daedd73
Also note that, even though the Point type now has default discriminants, we can still
specify discriminants, as we're doing in the declarations of P2 and P3.
In most other respects discriminants behave like regular fields: You have to specify their
values in aggregates, as seen above, and you can access their values via the dot notation.
Listing 6: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Main is
6 procedure Print_Stack (G : Growable_Stack) is
7 begin
8 Put ("<Stack, items: [");
9 for I in G.Items'Range loop
10 exit when I > G.Len;
(continues on next page)
16 S : Growable_Stack :=
17 (Max_Len => 128,
18 Items => (1, 2, 3, 4, others => <>),
19 Len => 4);
20 begin
21 Print_Stack (S);
22 end Main;
Project: Courses.Intro_To_Ada.More_About_Records.Var_Size_Record_2
MD5: 4e8c102cd93dc5d8aa1b402589c5239b
Runtime output
Note: In the examples above, we used a discriminant to determine the size of an array,
but it is not limited to that, and could be used, for example, to determine the size of a
nested discriminated record.
The examples of discriminants thus far have illustrated the declaration of records of varying
size, by having components whose size depends on the discriminant.
However, discriminants can also be used to obtain the functionality of what are sometimes
called "variant records": records that can contain different sets of fields.
Listing 7: variant_record.ads
1 package Variant_Record is
2 -- Forward declaration of Expr
3 type Expr;
4
5 -- Access to a Expr
6 type Expr_Access is access Expr;
7
Project: Courses.Intro_To_Ada.More_About_Records.Variant_Record
MD5: af9c1edca3ed6b2d938249c7258806b1
The fields that are in a when branch will be only available when the value of the discriminant
is covered by the branch. In the example above, you will only be able to access the fields
Left and Right when the Kind is Bin_Op_Plus or Bin_Op_Minus.
If you try to access a field that is not valid for your record, a Constraint_Error will be
raised.
Listing 8: main.adb
1 with Variant_Record; use Variant_Record;
2
3 procedure Main is
4 E : Expr := (Num, 12);
5 begin
6 E.Left := new Expr'(Num, 15);
7 -- Will compile but fail at runtime
8 end Main;
Project: Courses.Intro_To_Ada.More_About_Records.Variant_Record
MD5: d157d5f96db0825b9376ba7fca9613ed
Build output
Runtime output
Listing 9: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Main is
6 function Eval_Expr (E : Expr) return Integer is
7 (case E.Kind is
8 when Bin_Op_Plus =>
9 Eval_Expr (E.Left.all)
10 + Eval_Expr (E.Right.all),
(continues on next page)
16 E : Expr := (Bin_Op_Plus,
17 new Expr'(Bin_Op_Minus,
18 new Expr'(Num, 12),
19 new Expr'(Num, 15)),
20 new Expr'(Num, 3));
21 begin
22 Put_Line (Integer'Image (Eval_Expr (E)));
23 end Main;
Project: Courses.Intro_To_Ada.More_About_Records.Variant_Record
MD5: 807dbb921b44b3eaeaf1baf6ffe1afaa
Runtime output
In other languages
Ada's variant records are very similar to Sum types in functional languages such as OCaml
or Haskell. A major difference is that the discriminant is a separate field in Ada, whereas
the 'tag' of a Sum type is kind of built in, and only accessible with pattern matching.
There are other differences (you can have several discriminants in a variant record in Ada).
Nevertheless, they allow the same kind of type modeling as sum types in functional lan-
guages.
Compared to C/C++ unions, Ada variant records are more powerful in what they allow, and
are also checked at run time, which makes them safer.
ELEVEN
FIXED-POINT TYPES
We have already seen how to specify floating-point types. However, in some applications
floating-point is not appropriate since, for example, the roundoff error from binary arith-
metic may be unacceptable or perhaps the hardware does not support floating-point in-
structions. Ada provides a category of types, the decimal fixed-point types, that allows the
programmer to specify the required decimal precision (number of digits) as well as the scal-
ing factor (a power of ten) and, optionally, a range. In effect the values will be represented
as integers implicitly scaled by the specified power of 10. This is useful, for example, for
financial applications.
The syntax for a simple decimal fixed-point type is
type <type-name> is delta <delta-value> digits <digits-value>;
In this case, the delta and the digits will be used by the compiler to derive a range.
Several attributes are useful for dealing with decimal types:
In the example below, we declare two data types: T3_D3 and T6_D3. For both types, the
delta value is the same: 0.001.
Listing 1: decimal_fixed_point_types.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Decimal_Fixed_Point_Types is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5 type T6_D3 is delta 10.0 ** (-3) digits 6;
6 begin
7 Put_Line ("The delta value of T3_D3 is "
8 & T3_D3'Image (T3_D3'Delta));
9 Put_Line ("The minimum value of T3_D3 is "
10 & T3_D3'Image (T3_D3'First));
11 Put_Line ("The maximum value of T3_D3 is "
12 & T3_D3'Image (T3_D3'Last));
13 New_Line;
14
107
Learning Ada
Project: Courses.Intro_To_Ada.Fixed_Point_Types.Decimal_Fixed_Point_Types
MD5: 6b1f6bfa555031b831aa872187c8bee9
Runtime output
When running the application, we see that the delta value of both types is indeed the same:
0.001. However, because T3_D3 is restricted to 3 digits, its range is -0.999 to 0.999. For
the T6_D3, we have defined a precision of 6 digits, so the range is -999.999 to 999.999.
Similar to the type definition using the range syntax, because we have an implicit range,
the compiled code will check that the variables contain values that are not out-of-range.
Also, if the result of a multiplication or division on decimal fixed-point types is smaller than
the delta value required for the context, the actual result will be zero. For example:
Listing 2: decimal_fixed_point_smaller.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Decimal_Fixed_Point_Smaller is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5 type T6_D6 is delta 10.0 ** (-6) digits 6;
6 A : T3_D3 := T3_D3'Delta;
7 B : T3_D3 := 0.5;
8 C : T6_D6;
9 begin
10 Put_Line ("The value of A is "
11 & T3_D3'Image (A));
12
13 A := A * B;
14 Put_Line ("The value of A * B is "
15 & T3_D3'Image (A));
16
17 A := T3_D3'Delta;
18 C := A * B;
19 Put_Line ("The value of A * B is "
20 & T6_D6'Image (C));
21 end Decimal_Fixed_Point_Smaller;
Project: Courses.Intro_To_Ada.Fixed_Point_Types.Decimal_Fixed_Point_Smaller
MD5: 6b0242caa4a79f9b3447a304002e6a3b
Runtime output
In this example, the result of the operation 0.001 * 0.5 is 0.0005. Since this value is not
representable for the T3_D3 type because the delta value is 0.001, the actual value stored
in variable A is zero. However, accuracy is preserved during the arithmetic operations if the
target has sufficient precision, and the value displayed for C is 0.000500.
Ordinary fixed-point types are similar to decimal fixed-point types in that the values are, in
effect, scaled integers. The difference between them is in the scale factor: for a decimal
fixed-point type, the scaling, given explicitly by the type's delta, is always a power of ten.
In contrast, for an ordinary fixed-point type, the scaling is defined by the type's small,
which is derived from the specified delta and, by default, is a power of two. Therefore,
ordinary fixed-point types are sometimes called binary fixed-point types.
Note: Ordinary fixed-point types can be thought of being closer to the actual represen-
tation on the machine, since hardware support for decimal fixed-point arithmetic is not
widespread (rescalings by a power of ten), while ordinary fixed-point types make use of the
available integer shift instructions.
By default the compiler will choose a scale factor, or small, that is a power of 2 no greater
than <delta-value>.
For example, we may define a normalized range between -1.0 and 1.0 as following:
Listing 3: normalized_fixed_point_type.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Normalized_Fixed_Point_Type is
4 D : constant := 2.0 ** (-31);
5 type TQ31 is delta D range -1.0 .. 1.0 - D;
6 begin
7 Put_Line ("TQ31 requires "
8 & Integer'Image (TQ31'Size)
9 & " bits");
10 Put_Line ("The delta value of TQ31 is "
11 & TQ31'Image (TQ31'Delta));
12 Put_Line ("The minimum value of TQ31 is "
13 & TQ31'Image (TQ31'First));
14 Put_Line ("The maximum value of TQ31 is "
15 & TQ31'Image (TQ31'Last));
16 end Normalized_Fixed_Point_Type;
Runtime output
In this example, we are defining a 32-bit fixed-point data type for our normalized range.
When running the application, we notice that the upper bound is close to one, but not
exact one. This is a typical effect of fixed-point data types — you can find more details in
this discussion about the Q format15 .
We may also rewrite this code with an exact type definition:
Listing 4: normalized_adapted_fixed_point_type.adb
1 procedure Normalized_Adapted_Fixed_Point_Type is
2 type TQ31 is
3 delta 2.0 ** (-31)
4 range -1.0 .. 1.0 - 2.0 ** (-31);
5 begin
6 null;
7 end Normalized_Adapted_Fixed_Point_Type;
Project: Courses.Intro_To_Ada.Fixed_Point_Types.Normalized_Adapted_Fixed_Point_Type
MD5: 3421800bb47b282d601a51d276944f62
Listing 5: custom_fixed_point_range.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics; use Ada.Numerics;
3
4 procedure Custom_Fixed_Point_Range is
5 type T_Inv_Trig is
6 delta 2.0 ** (-15) * Pi
7 range -Pi / 2.0 .. Pi / 2.0;
8 begin
9 Put_Line ("T_Inv_Trig requires "
10 & Integer'Image (T_Inv_Trig'Size)
11 & " bits");
12 Put_Line ("Delta value of T_Inv_Trig: "
13 & T_Inv_Trig'Image
14 (T_Inv_Trig'Delta));
15 Put_Line ("Minimum value of T_Inv_Trig: "
16 & T_Inv_Trig'Image
17 (T_Inv_Trig'First));
18 Put_Line ("Maximum value of T_Inv_Trig: "
19 & T_Inv_Trig'Image
20 (T_Inv_Trig'Last));
21 end Custom_Fixed_Point_Range;
Project: Courses.Intro_To_Ada.Fixed_Point_Types.Custom_Fixed_Point_Range
MD5: a3e6c549cb1070aa285857ae8813de27
Runtime output
15 https://en.wikipedia.org/wiki/Q_(number_format)
In this example, we are defining a 16-bit type called T_Inv_Trig, which has a range from
-π/2 to π/2.
All standard operations are available for fixed-point types. For example:
Listing 6: fixed_point_op.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Fixed_Point_Op is
4 type TQ31 is
5 delta 2.0 ** (-31)
6 range -1.0 .. 1.0 - 2.0 ** (-31);
7
8 A, B, R : TQ31;
9 begin
10 A := 0.25;
11 B := 0.50;
12 R := A + B;
13 Put_Line ("R is " & TQ31'Image (R));
14 end Fixed_Point_Op;
Project: Courses.Intro_To_Ada.Fixed_Point_Types.Fixed_Point_Op
MD5: cad218b70b7fb0621468027a807431b1
Runtime output
R is 0.7500000000
type Angle is
delta 1.0/3600.0
range 0.0 .. 360.0 - 1.0 / 3600.0;
for Angle'Small use Angle'Delta;
TWELVE
PRIVACY
One of the main principles of modular programming, as well as object oriented program-
ming, is encapsulation16 .
Encapsulation, briefly, is the concept that the implementer of a piece of software will dis-
tinguish between the code's public interface and its private implementation.
This is not only applicable to software libraries but wherever abstraction is used.
In Ada, the granularity of encapsulation is a bit different from most object-oriented lan-
guages, because privacy is generally specified at the package level.
Listing 1: encapsulate.ads
1 package Encapsulate is
2 procedure Hello;
3
4 private
5
6 procedure Hello2;
7 -- Not visible from external units
8 end Encapsulate;
Listing 2: encapsulate.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Hello is
6 begin
7 Put_Line ("Hello");
8 end Hello;
9
10 procedure Hello2 is
11 begin
12 Put_Line ("Hello #2");
13 end Hello2;
14
15 end Encapsulate;
16 https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
113
Learning Ada
Listing 3: main.adb
1 with Encapsulate;
2
3 procedure Main is
4 begin
5 Encapsulate.Hello;
6 Encapsulate.Hello2;
7 -- Invalid: Hello2 is not visible
8 end Main;
Build output
main.adb:6:15: error: "Hello2" is not a visible entity of "Encapsulate"
gprbuild: *** compilation phase failed
With this high-level granularity, it might not seem obvious how to hide the implementation
details of a type. Here is how it can be done in Ada:
Listing 4: stacks.ads
1 package Stacks is
2 type Stack is private;
3 -- Declare a private type: You cannot depend
4 -- on its implementation. You can only assign
5 -- and test for equality.
6
13 subtype Stack_Index is
14 Natural range 1 .. 10;
15
16 type Content_Type is
17 array (Stack_Index) of Natural;
18
Listing 5: stacks.adb
1 package body Stacks is
2
17 end Stacks;
In the above example, we define a stack type in the public part (known as the visible part
of the package spec in Ada), but the exact representation of that type is private.
Then, in the private part, we define the representation of that type. We can also declare
other types that will be used as helpers for our main public type. This is useful since declar-
ing helper types is common in Ada.
A few words about terminology:
• The Stack type as viewed from the public part is called the partial view of the type.
This is what clients have access to.
• The Stack type as viewed from the private part or the body of the package is called
the full view of the type. This is what implementers have access to.
From the point of view of the client (the with'ing unit), only the public (visible) part is im-
portant, and the private part could as well not exist. It makes it very easy to read linearly
the part of the package that is important for you.
-- No need to read the private part to use the package
package Stacks is
type Stack is private;
procedure Test_Stack is
S : Stack;
Res : Integer;
begin
Push (S, 5);
Push (S, 7);
Pop (S, Res);
end Test_Stack;
Ada's limited type facility allows you to declare a type for which assignment and comparison
operations are not automatically provided.
Listing 6: stacks.ads
1 package Stacks is
2 type Stack is limited private;
3 -- Limited type. Cannot assign nor compare.
4
13 type Content_Type is
14 array (Stack_Index) of Natural;
15
Listing 7: stacks.adb
1 package body Stacks is
2
17 end Stacks;
Listing 8: main.adb
1 with Stacks; use Stacks;
2
3 procedure Main is
4 S, S2 : Stack;
5 begin
6 S := S2;
7 -- Illegal: S is limited.
8 end Main;
Build output
main.adb:6:04: error: left hand of assignment must not be limited type
gprbuild: *** compilation phase failed
This is useful because, for example, for some data types the built-in assignment operation
might be incorrect (for example when a deep copy is required).
Ada does allow you to overload the comparison operators = and /= for limited types (and
to override the built-in declarations for non-limited types).
Ada also allows you to implement special semantics for assignment via controlled types17 .
However, in some cases assignment is simply inappropriate; one example is the File_Type
from the Ada.Text_IO package, which is declared as a limited type and thus attempts to
assign one file to another would be detected as illegal.
We've seen previously (in the child packages section (page 39)) that packages can have
child packages. Privacy plays an important role in child packages. This section discusses
some of the privacy rules that apply to child packages.
Although the private part of a package P is meant to encapsulate information, certain parts
of a child package P.C can have access to this private part of P. In those cases, information
from the private part of P can then be used as if it were declared in the public part of its
specification. To be more specific, the body of P.C and the private part of the specification
of P.C have access to the private part of P. However, the public part of the specification of
P.C only has access to the public part of P's specification. The following table summarizes
this:
Part of a child package Access to the private part of its parent's specification
Specification: public part
Specification: private part ✓
Body ✓
The rest of this section shows examples of how this access to private information actually
works for child packages.
Let's first look at an example where the body of a child package P.C has access to the private
part of the specification of its parent P. We've seen, in a previous source-code example,
17 http://www.ada-auth.org/standards/12rm/html/RM-7-6.html
that the Hello2 procedure declared in the private part of the Encapsulate package cannot
be used in the Main procedure, since it's not visible there. This limitation doesn't apply,
however, for parts of the child packages of the Encapsulate package. In fact, the body of
its child package Encapsulate.Child has access to the Hello2 procedure and can call it
there, as you can see in the implementation of the Hello3 procedure of the Child package:
Listing 9: encapsulate.ads
1 package Encapsulate is
2 procedure Hello;
3
4 private
5
6 procedure Hello2;
7 -- Not visible from external units
8 -- But visible in child packages
9 end Encapsulate;
5 procedure Hello is
6 begin
7 Put_Line ("Hello");
8 end Hello;
9
10 procedure Hello2 is
11 begin
12 Put_Line ("Hello #2");
13 end Hello2;
14
15 end Encapsulate;
3 procedure Hello3;
4
5 end Encapsulate.Child;
5 procedure Hello3 is
6 begin
7 -- Using private procedure Hello2
8 -- from the parent package
9 Hello2;
10 Put_Line ("Hello #3");
11 end Hello3;
12
13 end Encapsulate.Child;
3 procedure Main is
4 begin
5 Encapsulate.Child.Hello3;
6 end Main;
Project: Courses.Intro_To_Ada.Privacy.Encapsulate_Child
MD5: 1533f43eee8f8b4d14c9b2101f42f13a
Runtime output
Hello #2
Hello #3
The same mechanism applies to types declared in the private part of a parent package. For
instance, the body of a child package can access components of a record declared in the
private part of its parent package. Let's look at an example:
5 private
6
11 end My_Types;
5 end My_Types.Ops;
11 end My_Types.Ops;
6 procedure Main is
7 E : Priv_Rec;
8 begin
9 Put_Line ("Presenting information:");
10
17 Display (E);
18 end Main;
Project: Courses.Intro_To_Ada.Privacy.Private_Type_Child
MD5: 9960611460bc1190b30949eca08fc02b
Runtime output
Presenting information:
Priv_Rec.Number: 42
In this example, we don't have access to the Number component of the record type Priv_Rec
in the Main procedure. You can see this in the call to Put_Line that has been commented-
out in the implementation of Main. Trying to access the Number component there would
trigger a compilation error. But we do have access to this component in the body of the
My_Types.Ops package, since it's a child package of the My_Types package. Therefore,
Ops's body has access to the declaration of the Priv_Rec type — which is in the private
part of its parent, the My_Types package. For this reason, the same call to Put_Line that
would trigger a compilation error in the Main procedure works fine in the Display procedure
of the My_Types.Ops package.
This kind of privacy rules for child packages allows for extending the functionality of a parent
package and, at the same time, retain its encapsulation.
As we mentioned previously, in addition to the package body, the private part of the speci-
fication of a child package P.C also has access to the private part of the specification of its
parent P. Let's look at an example where we declare an object of private type Priv_Rec in
the private part of the child package My_Types.Child and initialize the Number component
of the Priv_Rec record directly:
package My_Types.Child is
private
end My_Types.Ops;
package My_Types.Child is
end My_Types.Ops;
The declaration above triggers a compilation error, since type Priv_Rec is private. Because
the public part of My_Types.Child is also visible outside the child package, Ada cannot
allow accessing private information in this part of the specification.
THIRTEEN
GENERICS
13.1 Introduction
Generics are used for metaprogramming in Ada. They are useful for abstract algorithms
that share common properties with each other.
Either a subprogram or a package can be generic. A generic is declared by using the key-
word generic. For example:
Listing 1: operator.ads
1 generic
2 type T is private;
3 -- Declaration of formal types and objects
4 -- Below, we could use one of the following:
5 -- <procedure | function | package>
6 procedure Operator (Dummy : in out T);
Listing 2: operator.adb
1 procedure Operator (Dummy : in out T) is
2 begin
3 null;
4 end Operator;
Project: Courses.Intro_To_Ada.Generics.Show_Simple_Generic
MD5: 1321d437043dafdb725fad416e654318
Formal types are abstractions of a specific type. For example, we may want to create an
algorithm that works on any integer type, or even on any type at all, whether a numeric
type or not. The following example declares a formal type T for the Set procedure.
Listing 3: set.ads
1 generic
2 type T is private;
3 -- T is a formal type that indicates that
4 -- any type can be used, possibly a numeric
5 -- type or possibly even a record type.
6 procedure Set (Dummy : T);
123
Learning Ada
Listing 4: set.adb
1 procedure Set (Dummy : T) is
2 begin
3 null;
4 end Set;
Project: Courses.Intro_To_Ada.Generics.Show_Formal_Type_Declaration
MD5: 668156f66b2479c4932d18b5ad35deba
The declaration of T as private indicates that you can map any definite type to it. But you
can also restrict the declaration to allow only some types to be mapped to that formal type.
Here are some examples:
Formal objects are similar to subprogram parameters. They can reference formal types
declared in the formal specification. For example:
Listing 5: set.ads
1 generic
2 type T is private;
3 X : in out T;
4 -- X can be used in the Set procedure
5 procedure Set (E : T);
Listing 6: set.adb
1 procedure Set (E : T) is
2 pragma Unreferenced (E, X);
3 begin
4 null;
5 end Set;
Project: Courses.Intro_To_Ada.Generics.Show_Formal_Object_Declaration
MD5: 1b88bc0e5b8f48a35394966e6af07ac0
Formal objects can be either input parameters or specified using the in out mode.
We don't repeat the generic keyword for the body declaration of a generic subprogram
or package. Instead, we start with the actual declaration and use the generic types and
objects we declared. For example:
Listing 7: set.ads
1 generic
2 type T is private;
3 X : in out T;
4 procedure Set (E : T);
Listing 8: set.adb
1 procedure Set (E : T) is
2 -- Body definition: "generic" keyword
3 -- is not used
4 begin
5 X := E;
6 end Set;
Project: Courses.Intro_To_Ada.Generics.Show_Generic_Body_Definition
MD5: de611ef77b528543fd6bad82c53857f7
Generic subprograms or packages can't be used directly. Instead, they need to be instan-
tiated, which we do using the new keyword, as shown in the following example:
Listing 9: set.ads
1 generic
2 type T is private;
3 X : in out T;
4 -- X can be used in the Set procedure
5 procedure Set (E : T);
4 procedure Show_Generic_Instantiation is
5
6 Main : Integer := 0;
7 Current : Integer;
8
20 begin
21 Current := 10;
22
23 Set_Main (Current);
24 Put_Line ("Value of Main is "
25 & Integer'Image (Main));
26 end Show_Generic_Instantiation;
Project: Courses.Intro_To_Ada.Generics.Show_Generic_Instantiation
MD5: 13dc0692252496d954240952561e1c05
Runtime output
Value of Main is 10
In the example above, we instantiate the procedure Set by mapping the formal parameters
T and X to actual existing elements, in this case the Integer type and the Main variable.
The previous examples focused on generic subprograms. In this section, we look at generic
packages. The syntax is similar to that used for generic subprograms: we start with the
generic keyword and continue with formal declarations. The only difference is that package
is specified instead of a subprogram keyword.
Here's an example:
10 Invalid_Element : exception;
11
12 private
13 Value : T;
14 Valid : Boolean := False;
15 end Element;
3 procedure Set (E : T) is
4 begin
5 Value := E;
6 Valid := True;
7 end Set;
8
9 procedure Reset is
10 begin
11 Valid := False;
12 end Reset;
13
4 procedure Show_Generic_Package is
5
8 procedure Display_Initialized is
9 begin
10 if I.Is_Valid then
11 Put_Line ("Value is initialized");
12 else
13 Put_Line ("Value is not initialized");
14 end if;
15 end Display_Initialized;
(continues on next page)
17 begin
18 Display_Initialized;
19
20 Put_Line ("Initializing...");
21 I.Set (5);
22 Display_Initialized;
23 Put_Line ("Value is now set to "
24 & Integer'Image (I.Get));
25
26 Put_Line ("Resetting...");
27 I.Reset;
28 Display_Initialized;
29
30 end Show_Generic_Package;
Project: Courses.Intro_To_Ada.Generics.Show_Generic_Package
MD5: c5278a06c6d06f1f37353ee0ca6686ec
Runtime output
In the example above, we created a simple container named Element, with just one single
element. This container tracks whether the element has been initialized or not.
After writing the package definition, we create the instance I of the Element. We use the
instance by calling the package subprograms (Set, Reset, and Get).
In addition to formal types and objects, we can also declare formal subprograms or pack-
ages. This course only describes formal subprograms; formal packages are discussed in
the advanced course.
We use the with keyword to declare a formal subprogram. In the example below, we declare
a formal function (Comparison) to be used by the generic procedure Check.
3 procedure Show_Formal_Subprogram is
4
5 A, B : Integer;
6
Project: Courses.Intro_To_Ada.Generics.Show_Formal_Subprogram
MD5: 1c463a47e9ce56b5afbca1da6acd116d
Runtime output
Ada offers generic I/O packages that can be instantiated for standard and derived types.
One example is the generic Float_IO package, which provides procedures such as Put and
Get. In fact, Float_Text_IO — available from the standard library — is an instance of the
Float_IO package, and it's defined as:
with Ada.Text_IO;
You can use it directly with any object of floating-point type. For example:
3 procedure Show_Float_Text_IO is
4 X : constant Float := 2.5;
5
6 use Ada.Float_Text_IO;
7 begin
8 Put (X);
9 end Show_Float_Text_IO;
Project: Courses.Intro_To_Ada.Generics.Show_Float_Text_IO
MD5: 7cc9b547ef301a2071e9fb65caa4631b
Runtime output
2.50000E+00
Instantiating generic I/O packages can be useful for derived types. For example, let's create
a new type Price that must be displayed with two decimal digits after the point, and no
exponent.
3 procedure Show_Float_IO_Inst is
4
10 P : Price;
11 begin
12 -- Set to zero => don't display exponent
13 Price_IO.Default_Exp := 0;
14
15 P := 2.5;
16 Price_IO.Put (P);
17 New_Line;
18
19 P := 5.75;
20 Price_IO.Put (P);
21 New_Line;
22 end Show_Float_IO_Inst;
Project: Courses.Intro_To_Ada.Generics.Show_Float_IO_Inst
MD5: 583c761421d7fdb812dd2a183b676bae
Runtime output
2.50
5.75
By adjusting Default_Exp from the Price_IO instance to remove the exponent, we can
control how variables of Price type are displayed. Just as a side note, we could also have
written:
-- [...]
begin
Price_IO.Default_Aft := 2;
Price_IO.Default_Exp := 0;
In this case, we're ajusting Default_Aft, too, to get two decimal digits after the point when
calling Put.
In addition to the generic Float_IO package, the following generic packages are available
from Ada.Text_IO:
• Enumeration_IO for enumeration types;
• Integer_IO for integer types;
• Modular_IO for modular types;
• Fixed_IO for fixed-point types;
• Decimal_IO for decimal types.
In fact, we could rewrite the example above using decimal types:
3 procedure Show_Decimal_IO_Inst is
4
10 P : Price;
11 begin
12 Price_IO.Default_Exp := 0;
13
14 P := 2.5;
15 Price_IO.Put (P);
16 New_Line;
17
18 P := 5.75;
19 Price_IO.Put (P);
20 New_Line;
21 end Show_Decimal_IO_Inst;
Project: Courses.Intro_To_Ada.Generics.Show_Decimal_IO_Inst
MD5: f413570759dcb32cc166078b3cee1a16
Runtime output
2.50
5.75
An important application of generics is to model abstract data types (ADTs). In fact, Ada
includes a library with numerous ADTs using generics: Ada.Containers (described in the
containers section (page 203)).
A typical example of an ADT is a stack:
17 private
18
19 type Stack_Array is
20 array (Natural range <>) of T;
21
22 Min : constant := 1;
23
29 end Stacks;
32 end Stacks;
4 procedure Show_Stack is
5
11 Values : Integer_Stacks.Stack;
12
13 begin
14 Push (Values, 10);
15 Push (Values, 20);
16
Runtime output
Last value was 20
In this example, we first create a generic stack package (Stacks) and then instantiate it to
create a stack of up to 10 integer values.
10 end Colors;
4 procedure Test_Non_Generic_Swap_Colors is
5 A, B, C : Color;
6 begin
7 A := Blue;
8 B := White;
9 C := Red;
10
18 New_Line;
19 Put_Line ("Swapping A and C...");
20 New_Line;
21 Swap_Colors (A, C);
22
Project: Courses.Intro_To_Ada.Generics.Test_Non_Generic_Swap_Colors
MD5: 4d1cf826a1676c3750a8aabd484ac71f
Runtime output
Value of A is BLUE
Value of B is WHITE
Value of C is RED
Value of A is RED
Value of B is WHITE
Value of C is BLUE
In this example, Swap_Colors can only be used for the Color type. However, this algorithm
can theoretically be used for any type, whether an enumeration type or a complex record
type with many elements. The algorithm itself is the same: it's only the type that differs.
If, for example, we want to swap variables of Integer type, we don't want to duplicate the
implementation. Therefore, such an algorithm is a perfect candidate for abstraction using
generics.
In the example below, we create a generic version of Swap_Colors and name it
Generic_Swap. This generic version can operate on any type due to the declaration of
formal type T.
3 package Colors is
4
11 end Colors;
4 procedure Test_Swap_Colors is
5 A, B, C : Color;
6 begin
7 A := Blue;
8 B := White;
9 C := Red;
10
18 New_Line;
19 Put_Line ("Swapping A and C...");
20 New_Line;
21 Swap_Colors (A, C);
22
Project: Courses.Intro_To_Ada.Generics.Test_Swap_Colors
MD5: a5d94a40bd9d1c6736cc873f8b58e867
Runtime output
Value of A is BLUE
Value of B is WHITE
Value of C is RED
Value of A is RED
Value of B is WHITE
Value of C is BLUE
As we can see in the example, we can create the same Swap_Colors procedure as we had
in the non-generic version of the algorithm by declaring it as an instance of the generic
Generic_Swap procedure. We specify that the generic T type will be mapped to the Color
type by passing it as an argument to the Generic_Swap instantiation.
The previous example, with an algorithm to swap two values, is one of the simplest exam-
ples of using generics. Next we study an algorithm for reversing elements of an array. First,
let's start with a non-generic version of the algorithm, one that works specifically for the
Color type:
6 type Color_Array is
7 array (Integer range <>) of Color;
8
11 end Colors;
22 end Colors;
4 procedure Test_Non_Generic_Reverse_Colors is
5
6 My_Colors : Color_Array (1 .. 5) :=
7 (Black, Red, Green, Blue, White);
8
9 begin
10 for C of My_Colors loop
11 Put_Line ("My_Color: " & Color'Image (C));
12 end loop;
13
14 New_Line;
15 Put_Line ("Reversing My_Color...");
16 New_Line;
17 Reverse_It (My_Colors);
18
23 end Test_Non_Generic_Reverse_Colors;
Project: Courses.Intro_To_Ada.Generics.Test_Non_Generic_Reverse_Colors
MD5: 9b3a489d0bc0ecd79de6ba99fd7cd44f
Runtime output
My_Color: BLACK
My_Color: RED
My_Color: GREEN
My_Color: BLUE
My_Color: WHITE
Reversing My_Color...
My_Color: WHITE
My_Color: BLUE
My_Color: GREEN
My_Color: RED
My_Color: BLACK
The procedure Reverse_It takes an array of colors, starts by swapping the first and last
elements of the array, and continues doing that with successive elements until it reaches
the middle of array. At that point, the entire array has been reversed, as we see from the
output of the test program.
To abstract this procedure, we declare formal types for three components of the algorithm:
• the elements of the array (Color type in the example)
• the range used for the array (Integer range in the example)
• the actual array type (Color_Array type in the example)
This is a generic version of the algorithm:
3 package Colors is
(continues on next page)
8 type Color_Array is
9 array (Integer range <>) of Color;
10
16 end Colors;
4 procedure Test_Reverse_Colors is
5
6 My_Colors : Color_Array (1 .. 5) :=
7 (Black, Red, Green, Blue, White);
8
9 begin
10 for C of My_Colors loop
11 Put_Line ("My_Color: "
12 & Color'Image (C));
13 end loop;
14
15 New_Line;
16 Put_Line ("Reversing My_Color...");
17 New_Line;
18 Reverse_It (My_Colors);
19
25 end Test_Reverse_Colors;
Runtime output
My_Color: BLACK
My_Color: RED
My_Color: GREEN
My_Color: BLUE
My_Color: WHITE
Reversing My_Color...
My_Color: WHITE
My_Color: BLUE
My_Color: GREEN
My_Color: RED
(continues on next page)
In the previous example we've focused only on abstracting the reversing algorithm itself.
However, we could have decided to also abstract our small test application. This could be
useful if we, for example, decide to test other procedures that change elements of an array.
In order to do this, we again have to choose the elements to abstract. We therefore declare
the following formal parameters:
• S: the string containing the array name
• a function Image that converts an element of type T to a string
• a procedure Test that performs some operation on the array
Note that Image and Test are examples of formal subprograms and S is an example of a
formal object.
Here is a version of the test application making use of the generic Perform_Test procedure:
9 New_Line;
10 Put_Line ("Testing " & S & "...");
11 New_Line;
12 Test (X);
13
14 for C of X loop
15 Put_Line (S & ": " & Image (C));
16 end loop;
17 end Perform_Test;
3 package Colors is
4
8 type Color_Array is
9 array (Integer range <>) of Color;
10
16 end Colors;
4 procedure Test_Reverse_Colors is
5
14 My_Colors : Color_Array (1 .. 5) :=
15 (Black, Red, Green, Blue, White);
16
17 begin
18 Perform_Test_Reverse_It (My_Colors);
19 end Test_Reverse_Colors;
Project: Courses.Intro_To_Ada.Generics.Test_Reverse_Colors_2
MD5: 04640309f4f7e9f8bcff137d1a6f8733
Runtime output
My_Color: BLACK
My_Color: RED
My_Color: GREEN
My_Color: BLUE
My_Color: WHITE
Testing My_Color...
My_Color: WHITE
My_Color: BLUE
My_Color: GREEN
My_Color: RED
My_Color: BLACK
FOURTEEN
EXCEPTIONS
Ada uses exceptions for error handling. Unlike many other languages, Ada speaks about
raising, not throwing, an exception and handling, not catching, an exception.
Ada exceptions are not types, but instead objects, which may be peculiar to you if you're
used to the way Java or Python support exceptions. Here's how you declare an exception:
Listing 1: exceptions.ads
1 package Exceptions is
2 My_Except : exception;
3 -- Like an object. *NOT* a type !
4 end Exceptions;
Project: Courses.Intro_To_Ada.Exceptions.Show_Exception
MD5: 6201faeca9b029c790023856d2c8c419
Even though they're objects, you're going to use each declared exception object as a "kind"
or "family" of exceptions. Ada does not require that a subprogram declare every exception
it can potentially raise.
Listing 2: main.adb
1 with Exceptions; use Exceptions;
2
3 procedure Main is
4 begin
5 raise My_Except;
6 -- Execution of current control flow
7 -- abandoned; an exception of kind
8 -- "My_Except" will bubble up until it
9 -- is caught.
10 end Main;
143
Learning Ada
Project: Courses.Intro_To_Ada.Exceptions.Show_Exception
MD5: 24b40ae1509722adf51c3dd0d3ea4fbe
Runtime output
Listing 3: main.adb
1 with Exceptions; use Exceptions;
2
3 procedure Main is
4 begin
5 raise My_Except with "My exception message";
6 -- Execution of current control flow
7 -- abandoned; an exception of kind
8 -- "My_Except" with associated string will
9 -- bubble up until it is caught.
10 end Main;
Runtime output
Next, we address how to handle exceptions that were raised by us or libraries that we call.
The neat thing in Ada is that you can add an exception handler to any statement block as
follows:
Listing 4: open_file.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Exceptions; use Ada.Exceptions;
3
4 procedure Open_File is
5 File : File_Type;
6 begin
7 -- Block (sequence of statements)
8 begin
9 Open (File, In_File, "input.txt");
10 exception
11 when E : Name_Error =>
12 -- ^ Exception to be handled
13 Put ("Cannot open input file : ");
14 Put_Line (Exception_Message (E));
15 raise;
16 -- Reraise current occurence
(continues on next page)
Project: Courses.Intro_To_Ada.Exceptions.Show_Exception_Handling
MD5: 4ea1d5da684a6d7d7ee32908810e9c8f
Runtime output
In the example above, we're using the Exception_Message function from the Ada.
Exceptions package. This function returns the message associated with the exception
as a string.
You don't need to introduce a block just to handle an exception: you can add it to the
statements block of your current subprogram:
Listing 5: open_file.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Exceptions; use Ada.Exceptions;
3
4 procedure Open_File is
5 File : File_Type;
6 begin
7 Open (File, In_File, "input.txt");
8 -- Exception block can be added to any block
9 exception
10 when Name_Error =>
11 Put ("Cannot open input file");
12 end Open_File;
Project: Courses.Intro_To_Ada.Exceptions.Show_Exception_Message
MD5: 838e87ae416b3a717901cdc00eb71b40
Runtime output
Attention
Exception handlers have an important restriction that you need to be careful about: Excep-
tions raised in the declarative section are not caught by the handlers of that block. So for
example, in the following code, the exception will not be caught.
Listing 6: be_careful.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Exceptions; use Ada.Exceptions;
3
4 procedure Be_Careful is
5 function Dangerous return Integer is
6 begin
7 raise Constraint_Error;
(continues on next page)
11 begin
12 declare
13 A : Integer := Dangerous;
14 begin
15 Put_Line (Integer'Image (A));
16 exception
17 when Constraint_Error =>
18 Put_Line ("error!");
19 end;
20 end Be_Careful;
Project: Courses.Intro_To_Ada.Exceptions.Be_Careful
MD5: 6ea8a214bbbaca09d7444136d069e782
Runtime output
This is also the case for the top-level exception block that is part of the current subprogram.
FIFTEEN
TASKING
Tasks and protected objects allow the implementation of concurrency in Ada. The following
sections explain these concepts in more detail.
15.1 Tasks
A task can be thought as an application that runs concurrently with the main application.
In other programming languages, a task might be called a thread18 , and tasking might be
called multithreading19 .
Tasks may synchronize with the main application but may also process information com-
pletely independently from the main application. Here we show how this is accomplished.
Tasks are declared using the keyword task. The task implementation is specified in a task
body block. For example:
Listing 1: show_simple_task.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Simple_Task is
4 task T;
5
6 task body T is
7 begin
8 Put_Line ("In task T");
9 end T;
10 begin
11 Put_Line ("In main");
12 end Show_Simple_Task;
Project: Courses.Intro_To_Ada.Tasking.Show_Simple_Task
MD5: b17d9b35b4b2b53bc59776749e1be219
Runtime output
In task T
In main
18 https://en.wikipedia.org/wiki/Thread_(computing)
19 https://en.wikipedia.org/wiki/Thread_(computing)#Multithreading
147
Learning Ada
Here, we're declaring and implementing the task T. As soon as the main application starts,
task T starts automatically — it's not necessary to manually start this task. By running the
application above, we can see that both calls to Put_Line are performed.
Note that:
• The main application is itself a task (the main or “environment” task).
– In this example, the subprogram Show_Simple_Task is the main task of the appli-
cation.
• Task T is a subtask.
– Each subtask has a master, which represents the program construct in which the
subtask is declared. In this case, the main subprogram Show_Simple_Task is T 's
master.
– The master construct is executed by some enclosing task, which we will refer to
as the "master task" of the subtask.
• The number of tasks is not limited to one: we could include a task T2 in the example
above.
– This task also starts automatically and runs concurrently with both task T and the
main task. For example:
Listing 2: show_simple_tasks.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Simple_Tasks is
4 task T;
5 task T2;
6
7 task body T is
8 begin
9 Put_Line ("In task T");
10 end T;
11
12 task body T2 is
13 begin
14 Put_Line ("In task T2");
15 end T2;
16
17 begin
18 Put_Line ("In main");
19 end Show_Simple_Tasks;
Project: Courses.Intro_To_Ada.Tasking.Multiple_Simple_Task
MD5: 5e24b797e742bec306ad498f4f40d2b4
Runtime output
In task T
In task T2
In main
As we've just seen, as soon as the master construct reaches its “begin”, its subtasks also
start automatically. The master continues its processing until it has nothing more to do. At
that point, however, it will not terminate. Instead, the master waits until its subtasks have
finished before it allows itself to complete. In other words, this waiting process provides
synchronization between the master task and its subtasks. After this synchronization, the
master construct will complete. For example:
Listing 3: show_simple_sync.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Simple_Sync is
4 task T;
5 task body T is
6 begin
7 for I in 1 .. 10 loop
8 Put_Line ("hello");
9 end loop;
10 end T;
11 begin
12 null;
13 -- Will wait here until all tasks
14 -- have terminated
15 end Show_Simple_Sync;
Project: Courses.Intro_To_Ada.Tasking.Show_Simple_Sync
MD5: 84afce465854f99f8cbe0b57714d8a5f
Runtime output
hello
hello
hello
hello
hello
hello
hello
hello
hello
hello
The same mechanism is used for other subprograms that contain subtasks: the subprogram
execution will wait for its subtasks to finish. So this mechanism is not limited to the main
subprogram and also applies to any subprogram called by the main subprogram, directly
or indirectly.
Synchronization also occurs if we move the task to a separate package. In the example
below, we declare a task T in the package Simple_Sync_Pkg.
Listing 4: simple_sync_pkg.ads
1 package Simple_Sync_Pkg is
2 task T;
3 end Simple_Sync_Pkg;
Project: Courses.Intro_To_Ada.Tasking.Simple_Sync_Pkg
MD5: 2f9be044d04994240970f150e2293d5e
Listing 5: simple_sync_pkg.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
Project: Courses.Intro_To_Ada.Tasking.Simple_Sync_Pkg
MD5: b668451e4fb10e802f619889bcd743ff
Because the package is with'ed by the main procedure, the task T defined in the package
will become a subtask of the main task. For example:
Listing 6: test_simple_sync_pkg.adb
1 with Simple_Sync_Pkg;
2
3 procedure Test_Simple_Sync_Pkg is
4 begin
5 null;
6 -- Will wait here until all tasks
7 -- have terminated
8 end Test_Simple_Sync_Pkg;
Project: Courses.Intro_To_Ada.Tasking.Simple_Sync_Pkg
MD5: e51565b91767ce198496ef3e9c582ac8
Runtime output
hello
hello
hello
hello
hello
hello
hello
hello
hello
hello
As soon as the main subprogram returns, the main task synchronizes with any subtasks
spawned by packages T from Simple_Sync_Pkg before finally terminating.
15.1.3 Delay
We can introduce a delay by using the keyword delay. This puts the current task to sleep
for the length of time (in seconds) specified in the delay statement. For example:
Listing 7: show_delay.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Delay is
4
5 task T;
6
7 task body T is
8 begin
9 for I in 1 .. 5 loop
10 Put_Line ("hello from task T");
11 delay 1.0;
12 -- ^ Wait 1.0 seconds
13 end loop;
14 end T;
15 begin
16 delay 1.5;
17 Put_Line ("hello from main");
18 end Show_Delay;
Runtime output
hello from task T
hello from task T
hello from main
hello from task T
hello from task T
hello from task T
In this example, we're making the task T wait one second after each time it displays the
"hello" message. In addition, the main task is waiting 1.5 seconds before displaying its own
"hello" message
The only type of synchronization we've seen so far is the one that happens automatically at
the end of a master construct with a subtask. You can also define custom synchronization
points using the keyword entry. An entry can be viewed as a special kind of subprogram,
which is called by another task using a similar syntax, as we will see later.
In the task body definition, you define which part of the task will accept the entries by using
the keyword accept. A task proceeds until it reaches an accept statement and then waits
for some other task to synchronize with it. Specifically,
• The task with the entry waits at that point (in the accept statement), ready to accept
a call to the corresponding entry from the master task.
• The other task calls the task entry, in a manner similar to a procedure call, to synchro-
nize with the entry.
This synchronization between tasks is called a rendezvous. Let's see an example:
Listing 8: show_rendezvous.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Rendezvous is
4
5 task T is
6 entry Start;
7 end T;
8
9 task body T is
10 begin
11 accept Start;
12 -- ^ Waiting for somebody
13 -- to call the entry
14
18 begin
19 Put_Line ("In Main");
20
Project: Courses.Intro_To_Ada.Tasking.Show_Rendezvous
MD5: 479eea7adc876ac359ad20ac6e3acf66
Runtime output
In Main
In T
In this example, we declare an entry Start for task T. In the task body, we implement this
entry using accept Start. When task T reaches this point, it waits for some other task to
call its entry. This synchronization occurs in the T.Start statement. After the rendezvous
completes, the main task and task T again run concurrently until they synchronize one final
time when the main subprogram Show_Rendezvous finishes.
An entry may be used to perform more than a simple task synchronization: it also may
perform multiple statements during the time both tasks are synchronized. We do this with
a do ... end block. For the previous example, we would simply write accept Start do
<statements>; end;. We use this kind of block in the next example.
There's no limit to the number of times an entry can be accepted. We could even create an
infinite loop in the task and accept calls to the same entry over and over again. An infinite
loop, however, prevents the subtask from finishing, so it blocks its master task when it
reaches the end of its processing. Therefore, a loop containing accept statements in a task
body can be used in conjunction with a select ... or terminate statement. In simple
terms, this statement allows its master task to automatically terminate the subtask when
the master construct reaches its end. For example:
Listing 9: show_rendezvous_loop.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Rendezvous_Loop is
4
5 task T is
6 entry Reset;
7 entry Increment;
8 end T;
9
10 task body T is
11 Cnt : Integer := 0;
12 begin
13 loop
14 select
15 accept Reset do
16 Cnt := 0;
17 end Reset;
18 Put_Line ("Reset");
19 or
20 accept Increment do
21 Cnt := Cnt + 1;
22 end Increment;
23 Put_Line ("In T's loop ("
24 & Integer'Image (Cnt)
25 & ")");
26 or
27 terminate;
28 end select;
29 end loop;
30 end T;
31
32 begin
33 Put_Line ("In Main");
34
35 for I in 1 .. 4 loop
36 -- Calling T's entry multiple times
37 T.Increment;
38 end loop;
39
40 T.Reset;
41 for I in 1 .. 4 loop
42 -- Calling T's entry multiple times
43 T.Increment;
44 end loop;
45
46 end Show_Rendezvous_Loop;
Project: Courses.Intro_To_Ada.Tasking.Show_Rendezvous_Loop
MD5: 0542dbc029cffb9f794d761bab9f3a9d
Runtime output
In Main
In T's loop ( 1)
In T's loop ( 2)
In T's loop ( 3)
In T's loop ( 4)
Reset
In T's loop ( 1)
In T's loop ( 2)
In T's loop ( 3)
In T's loop ( 4)
In this example, the task body implements an infinite loop that accepts calls to the Reset
and Increment entry. We make the following observations:
• The accept E do ... end block is used to increment a counter.
– As long as task T is performing the do ... end block, the main task waits for the
block to complete.
• The main task is calling the Increment entry multiple times in the loop from 1 .. 4.
It is also calling the Reset entry before the second loop.
– Because task T contains an infinite loop, it always accepts calls to the Reset and
Increment entries.
– When the master construct of the subtask (the Show_Rendezvous_Loop subpro-
gram) completes, it checks the status of the T task. Even though task T could
accept new calls to the Reset or Increment entries, the master construct is al-
lowed to terminate task T due to the or terminate part of the select statement.
In a previous example, we saw how to delay a task a specified time by using the delay
keyword. However, using delay statements in a loop is not enough to guarantee regular
intervals between those delay statements. For example, we may have a call to a compu-
tationally intensive procedure between executions of successive delay statements:
while True loop
delay 1.0;
-- ^ Wait 1.0 seconds
Computational_Intensive_App;
end loop;
In this case, we can't guarantee that exactly 10 seconds have elapsed after 10 calls
to the delay statement because a time drift may be introduced by the Computa-
tional_Intensive_App procedure. In many cases, this time drift is not relevant, so using
the delay keyword is good enough.
However, there are situations where a time drift isn't acceptable. In those cases, we need
to use the delay until statement, which accepts a precise time for the end of the delay,
allowing us to define a regular interval. This is useful, for example, in real-time applications.
We will soon see an example of how this time drift may be introduced and how the delay
until statement circumvents the problem. But before we do that, we look at a package
containing a procedure allowing us to measure the elapsed time (Show_Elapsed_Time) and
a dummy Computational_Intensive_App procedure which is simulated by using a simple
delay. This is the complete package:
3 package Delay_Aux_Pkg is
4
8 procedure Show_Elapsed_Time
9 with Inline;
10
11 procedure Computational_Intensive_App;
12 private
13 Start_Time : Time := Clock;
14
18 end Delay_Aux_Pkg;
5 procedure Show_Elapsed_Time is
6 Now_Time : Time;
7 Elapsed_Time : Time_Span;
8 begin
9 Now_Time := Clock;
10 Elapsed_Time := Now_Time - Start_Time;
11 Put_Line ("Elapsed time "
12 & Duration'Image
13 (To_Duration (Elapsed_Time))
14 & " seconds");
15 end Show_Elapsed_Time;
16
17 procedure Computational_Intensive_App is
18 begin
19 delay 0.5;
20 end Computational_Intensive_App;
21
22 end Delay_Aux_Pkg;
Project: Courses.Intro_To_Ada.Tasking.Show_Time
MD5: 422a38c1afa0bbd659ec81de88479e0a
Using this auxiliary package, we're now ready to write our time-drifting application:
4 with Delay_Aux_Pkg;
5
6 procedure Show_Time_Task is
(continues on next page)
9 task T;
10
11 task body T is
12 Cnt : Integer := 1;
13 begin
14 for I in 1 .. 5 loop
15 delay 1.0;
16
17 Aux.Show_Elapsed_Time;
18 Aux.Computational_Intensive_App;
19
27 begin
28 null;
29 end Show_Time_Task;
Runtime output
Elapsed time 1.000528681 seconds
Cycle # 1
Elapsed time 2.501496814 seconds
Cycle # 2
Elapsed time 4.002310614 seconds
Cycle # 3
Elapsed time 5.502655881 seconds
Cycle # 4
Elapsed time 7.012303672 seconds
Cycle # 5
Finished time-drifting loop
We can see by running the application that we already have a time difference of about
four seconds after three iterations of the loop due to the drift introduced by Computa-
tional_Intensive_App. Using the delay until statement, however, we're able to avoid
this time drift and have a regular interval of exactly one second:
4 with Delay_Aux_Pkg;
5
6 procedure Show_Time_Task is
7 package Aux renames Delay_Aux_Pkg;
8
9 task T;
10
11 task body T is
(continues on next page)
17 Cnt : Integer := 1;
18 begin
19 for I in 1 .. 5 loop
20 delay until Next;
21
22 Aux.Show_Elapsed_Time;
23 Aux.Computational_Intensive_App;
24
36 begin
37 null;
38 end Show_Time_Task;
Project: Courses.Intro_To_Ada.Tasking.Show_Time
MD5: 1456c0feee6def8b370d994c0ab75a15
Runtime output
Now, as we can see by running the application, the delay until statement ensures that the
Computational_Intensive_App doesn't disturb the regular interval of one second between
iterations.
When multiple tasks are accessing shared data, corruption of that data may occur. For
example, data may be inconsistent if one task overwrites parts of the information that's
being read by another task at the same time. In order to avoid these kinds of problems and
ensure information is accessed in a coordinated way, we use protected objects.
Protected objects encapsulate data and provide access to that data by means of protected
operations, which may be subprograms or protected entries. Using protected objects en-
sures that data is not corrupted by race conditions or other concurrent access.
Important
Objects can be protected from concurrent access using Ada tasks. In fact, this was the
only way of protecting objects from concurrent access in Ada 83 (the first version of the
Ada language). However, the use of protected objects is much simpler than using similar
mechanisms implemented using only tasks. Therefore, you should use protected objects
when your main goal is only to protect data.
You declare a protected object with the protected keyword. The syntax is similar to that
used for packages: you can declare operations (e.g., procedures and functions) in the public
part and data in the private part. The corresponding implementation of the operations is
included in the protected body of the object. For example:
3 procedure Show_Protected_Objects is
4
5 protected Obj is
6 -- Operations go here (only subprograms)
7 procedure Set (V : Integer);
8 function Get return Integer;
9 private
10 -- Data goes here
11 Local : Integer := 0;
12 end Obj;
13
28 begin
29 Obj.Set (5);
(continues on next page)
Runtime output
Number is: 5
In this example, we define two operations for Obj: Set and Get. The implementation of
these operations is in the Obj body. The syntax used for writing these operations is the same
as that for normal procedures and functions. The implementation of protected objects is
straightforward — we simply access and update Local in these subprograms. To call these
operations in the main application, we use prefixed notation, e.g., Obj.Get.
15.2.2 Entries
In addition to protected procedures and functions, you can also define protected entry
points. Do this using the entry keyword. Protected entry points allow you to define barri-
ers using the when keyword. Barriers are conditions that must be fulfilled before the entry
can start performing its actual processing — we speak of releasing the barrier when the
condition is fulfilled.
The previous example used procedures and functions to define operations on the protected
objects. However, doing so permits reading protected information (via Obj.Get) before it's
set (via Obj.Set). To allow that to be a defined operation, we specified a default value (0).
Instead, by rewriting Obj.Get using an entry instead of a function, we implement a barrier,
ensuring no task can read the information before it's been set.
The following example implements the barrier for the Obj.Get operation. It also contains
two concurrent subprograms (main task and task T) that try to access the protected object.
3 procedure Show_Protected_Objects_Entries is
4
5 protected Obj is
6 procedure Set (V : Integer);
7 entry Get (V : out Integer);
8 private
9 Local : Integer;
10 Is_Set : Boolean := False;
11 end Obj;
12
34 N : Integer := 0;
35
36 task T;
37
38 task body T is
39 begin
40 Put_Line
41 ("Task T will delay for 4 seconds...");
42 delay 4.0;
43
44 Put_Line
45 ("Task T will set Obj...");
46 Obj.Set (5);
47
48 Put_Line
49 ("Task T has just set Obj...");
50 end T;
51 begin
52 Put_Line
53 ("Main application will get Obj...");
54 Obj.Get (N);
55
56 Put_Line
57 ("Main application has retrieved Obj...");
58 Put_Line
59 ("Number is: " & Integer'Image (N));
60
61 end Show_Protected_Objects_Entries;
Project: Courses.Intro_To_Ada.Tasking.Show_Protected_Objects_Entries
MD5: c1134445a96700b871fb76c4d6342359
Runtime output
As we see by running it, the main application waits until the protected object is set (by the
call to Obj.Set in task T) before it reads the information (via Obj.Get). Because a 4-second
delay has been added in task T, the main application is also delayed by 4 seconds. Only
after this delay does task T set the object and release the barrier in Obj.Get so that the
main application can then resume processing (after the information is retrieved from the
protected object).
In the previous examples, we defined single tasks and protected objects. We can, however,
generalize tasks and protected objects using type definitions. This allows us, for example,
to create multiple tasks based on just a single task type.
A task type is a generalization of a task. The declaration is similar to simple tasks: you
replace task with task type. The difference between simple tasks and task types is that
task types don't create actual tasks that automatically start. Instead, a task object decla-
ration is needed. This is exactly the way normal variables and types work: objects are only
created by variable definitions, not type definitions.
To illustrate this, we repeat our first example:
3 procedure Show_Simple_Task is
4 task T;
5
6 task body T is
7 begin
8 Put_Line ("In task T");
9 end T;
10 begin
11 Put_Line ("In main");
12 end Show_Simple_Task;
Runtime output
In task T
In main
We now rewrite it by replacing task T with task type TT. We declare a task (A_Task)
based on the task type TT after its definition:
3 procedure Show_Simple_Task_Type is
4 task type TT;
5
6 task body TT is
7 begin
8 Put_Line ("In task type TT");
9 end TT;
10
11 A_Task : TT;
12 begin
13 Put_Line ("In main");
14 end Show_Simple_Task_Type;
Project: Courses.Intro_To_Ada.Tasking.Show_Simple_Task_Type
MD5: 24c26dcbba6f5c54f0a7d47c3c0da728
Runtime output
In task type TT
In main
We can extend this example and create an array of tasks. Since we're using the same
syntax as for variable declarations, we use a similar syntax for task types: array (<>) of
Task_Type. Also, we can pass information to the individual tasks by defining a Start entry.
Here's the updated example:
3 procedure Show_Task_Type_Array is
4 task type TT is
5 entry Start (N : Integer);
6 end TT;
7
8 task body TT is
9 Task_N : Integer;
10 begin
11 accept Start (N : Integer) do
12 Task_N := N;
13 end Start;
14 Put_Line ("In task T: "
15 & Integer'Image (Task_N));
16 end TT;
17
Project: Courses.Intro_To_Ada.Tasking.Show_Task_Type_Array
MD5: bba072dfc52fb2bfbef6e7b9f8191464
Runtime output
In main
In task T: 1
In task T: 2
In task T: 3
In task T: 4
In task T: 5
In this example, we're declaring five tasks in the array My_Tasks. We pass the array index
to the individual tasks in the entry point (Start). After the synchronization between the
individual subtasks and the main task, each subtask calls Put_Line concurrently.
3 procedure Show_Protected_Object_Type is
4
24 Obj : P_Obj_Type;
25 begin
26 Obj.Set (5);
27 Put_Line ("Number is: "
28 & Integer'Image (Obj.Get));
29 end Show_Protected_Object_Type;
Project: Courses.Intro_To_Ada.Tasking.Show_Protected_Object_Type
MD5: c50321e55afef0d72f263fee0669e55f
Runtime output
Number is: 5
In this example, instead of directly defining the protected object Obj, we first define a
protected type P_Obj_Type and then declare Obj as an object of that protected type. Note
that the main application hasn't changed: we still use Obj.Set and Obj.Get to access the
protected object, just like in the original example.
SIXTEEN
DESIGN BY CONTRACTS
Pre- and postconditions provide expectations regarding input and output parameters of
subprograms and return value of functions. If we say that certain requirements must be met
before calling a subprogram Op, those are preconditions. Similarly, if certain requirements
must be met after a call to the subprogram Op, those are postconditions. We can think
of preconditions and postconditions as promises between the subprogram caller and the
callee: a precondition is a promise from the caller to the callee, and a postcondition is a
promise in the other direction.
Pre- and postconditions are specified using an aspect clause in the subprogram declara-
tion. A with Pre => <condition> clause specifies a precondition and a with Post =>
<condition> clause specifies a postcondition.
The following code shows an example of preconditions:
Listing 1: show_simple_precondition.adb
1 procedure Show_Simple_Precondition is
2
165
Learning Ada
Runtime output
In this example, we want to prevent the name field in our database from containing an
empty string. We implement this requirement by using a precondition requiring that the
length of the string used for the Name parameter of the DB_Entry procedure is greater than
zero. If the DB_Entry procedure is called with an empty string for the Name parameter, the
call will fail because the precondition is not met.
Before we get to our next example, let's briefly discuss quantified expressions, which are
quite useful in concisely writing pre- and postconditions. Quantified expressions return a
Boolean value indicating whether elements of an array or container match the expected
condition. They have the form: (for all I in A'Range => <condition on A(I)>, where
A is an array and I is an index. Quantified expressions using for all check whether the
condition is true for every element. For example:
(for all I in A'Range => A (I) = 0)
This quantified expression is only true when all elements of the array A have a value of zero.
Another kind of quantified expressions uses for some. The form looks similar: (for some
I in A'Range => <condition on A(I)>. However, in this case the qualified expression
tests whether the condition is true only on some elements (hence the name) instead of all
elements.
We illustrate postconditions using the following example:
Listing 2: show_simple_postcondition.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Simple_Postcondition is
4
7 type Int_8_Array is
8 array (Integer range <>) of Int_8;
9
35 Square (V);
36 for E of V loop
37 Put_Line ("Square: "
38 & Int_8'Image (E));
39 end loop;
40 end Show_Simple_Postcondition;
Project: Courses.Intro_To_Ada.Contracts.Show_Simple_Postcondition
MD5: b9bae9fe09cefcbe6769ad9cd6739e2a
Runtime output
Original: -2
Original: -1
Original: 0
Original: 1
Original: 10
Original: 11
Square: 4
Square: 1
Square: 0
Square: 1
Square: 100
Square: 121
We declare a signed 8-bit type Int_8 and an array of that type (Int_8_Array). We want to
ensure each element of the array is squared after calling the procedure Square for an object
of the Int_8_Array type. We do this with a postcondition using a for all expression. This
postcondition also uses the 'Old attribute to refer to the original value of the parameter
(before the call).
We also want to ensure that the result of calls to the Square function for the Int_8 type are
greater than the input to that call. To do that, we write a postcondition using the 'Result
attribute of the function and comparing it to the input value.
We can use both pre- and postconditions in the declaration of a single subprogram. For
example:
Listing 3: show_simple_contract.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
18 V : Int_8;
19 begin
20 V := Square (11);
21 Put_Line ("Square of 11 is "
22 & Int_8'Image (V));
23
Project: Courses.Intro_To_Ada.Contracts.Show_Simple_Contract
MD5: 1d928dd100704907c858562155f90ee2
Runtime output
Square of 11 is 121
In this example, we want to ensure that the input value of calls to the Square function for
the Int_8 type won't cause overflow in that function. We do this by converting the input
value to the Integer type, which is used for the temporary calculation, and check if the
result is in the appropriate range for the Int_8 type. We have the same postcondition in
this example as in the previous one.
16.2 Predicates
Predicates specify expectations regarding types. They're similar to pre- and postconditions,
but apply to types instead of subprograms. Their conditions are checked for each object of a
given type, which allows verifying that an object of type T is conformant to the requirements
of its type.
There are two kinds of predicates: static and dynamic. In simple terms, static predicates
are used to check objects at compile-time, while dynamic predicates are used for checks
at run time. Normally, static predicates are used for scalar types and dynamic predicates
for the more complex types.
Static and dynamic predicates are specified using the following clauses, respectively:
Listing 4: show_dynamic_predicate_courses.adb
1 with Ada.Calendar; use Ada.Calendar;
2
3 with Ada.Containers.Vectors;
4
5 with Ada.Strings.Unbounded;
6 use Ada.Strings.Unbounded;
7
8 procedure Show_Dynamic_Predicate_Courses is
9
10 package Courses is
11 type Course_Container is private;
12
42 use Courses;
43
44 CC : Course_Container;
45 begin
46 Add (CC,
47 Course'(
48 Name =>
49 To_Unbounded_String
50 ("Intro to Photography"),
51 Start_Date =>
52 Time_Of (2018, 5, 1),
53 End_Date =>
54 Time_Of (2018, 5, 10)));
55
68 end Show_Dynamic_Predicate_Courses;
Project: Courses.Intro_To_Ada.Contracts.Show_Dynamic_Predicate_Courses
MD5: 8bd6539e72995fececfcdf9666ffd04f
Runtime output
In this example, the package Courses defines a type Course and a type Course_Container,
an object of which contains all courses. We want to ensure that the dates of each course
are consistent, specifically that the start date is no later than the end date. To enforce this
rule, we declare a dynamic predicate for the Course type that performs the check for each
object. The predicate uses the type name where a variable of that type would normally be
used: this is a reference to the instance of the object being tested.
Note that the example above makes use of unbounded strings and dates. Both types are
available in Ada's standard library. Please refer to the following sections for more informa-
tion about:
• the unbounded string type (Unbounded_String): Unbounded Strings (page 246) sec-
tion;
• dates and times: Dates & Times (page 231) section.
Static predicates, as mentioned above, are mostly used for scalar types and checked during
compilation. They're particularly useful for representing non-contiguous elements of an
enumeration. A classic example is a list of week days:
We can easily create a sub-list of work days in the week by specifying a subtype with a
range based on Week. For example:
Ranges in Ada can only be specified as contiguous lists: they don't allow us to pick specific
days. However, we may want to create a list containing just the first, middle and last day
of the work week. To do that, we use a static predicate:
Listing 5: show_predicates.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Predicates is
4
23 Num_Tests : Tests_Week :=
24 (Mon => 3, Tue => 0,
25 Wed => 4, Thu => 0,
26 Fri => 2, Sat => 0,
27 Sun => 0);
28
39 begin
40 Display_Tests (Num_Tests);
41
Project: Courses.Intro_To_Ada.Contracts.Show_Predicates
MD5: 126c47033fc67fc8b6d7f6479205e752
Runtime output
Here we have an application that wants to perform tests only on three days of the work
week. These days are specified in the Test_Days subtype. We want to track the number
of tests that occur each day. We declare the type Tests_Week as an array, an object of
which will contain the number of tests done each day. According to our requirements,
these tests should happen only in the aforementioned three days; on other days, no tests
should be performed. This requirement is implemented with a dynamic predicate of the
type Tests_Week. Finally, the actual information about these tests is stored in the array
Num_Tests, which is an instance of the Tests_Week type.
The dynamic predicate of the Tests_Week type is verified during the initialization of
Num_Tests. If we have a non-conformant value there, the check will fail. However, as
we can see in our example, individual assignments to elements of the array do not trigger
a check. We can't check for consistency at this point because the initialization of the a
complex data structure (such as arrays or records) may not be performed with a single as-
signment. However, as soon as the object is passed as an argument to a subprogram, the
dynamic predicate is checked because the subprogram requires the object to be consistent.
This happens in the last call to Display_Tests in our example. Here, the predicate check
fails because the previous assignment has a non-conformant value.
Type invariants are another way of specifying expectations regarding types. While predi-
cates are used for non-private types, type invariants are used exclusively to define expec-
tations about private types. If a type T from a package P has a type invariant, the results
of operations on objects of type T are always consistent with that invariant.
Type invariants are specified with a with Type_Invariant => <property> clause. Like
predicates, the property defines a condition that allows us to check if an object of type T
is conformant to its requirements. In this sense, type invariants can be viewed as a sort of
predicate for private types. However, there are some differences in terms of checks. The
following table summarizes the differences:
We could rewrite our previous example and replace dynamic predicates by type invariants.
It would look like this:
Listing 6: show_type_invariant.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar; use Ada.Calendar;
3
4 with Ada.Containers.Vectors;
5
6 with Ada.Strings.Unbounded;
7 use Ada.Strings.Unbounded;
8
9 procedure Show_Type_Invariant is
10
11 package Courses is
12 type Course is private
13 with Type_Invariant => Check (Course);
14
20 function Init
21 (Name : String;
22 Start_Date, End_Date : Time)
23 return Course;
24
28 private
29 type Course is record
30 Name : Unbounded_String;
31 Start_Date : Time;
32 End_Date : Time;
33 end record;
34
56 function Init
57 (Name : String;
58 Start_Date, End_Date : Time)
59 return Course is
(continues on next page)
69 use Courses;
70
71 CC : Course_Container;
72 begin
73 Add (CC,
74 Init (Name =>
75 "Intro to Photography",
76 Start_Date =>
77 Time_Of (2018, 5, 1),
78 End_Date =>
79 Time_Of (2018, 5, 10)));
80
Project: Courses.Intro_To_Ada.Contracts.Show_Type_Invariant
MD5: c6ef863da94285f927dd106645af8650
Runtime output
The major difference is that the Course type was a visible (public) type of the Courses
package in the previous example, but in this example is a private type.
SEVENTEEN
INTERFACING WITH C
Ada allows us to interface with code in many languages, including C and C++. This section
discusses how to interface with C.
By default, when using gprbuild we only compile Ada source files. To compile C files as
well, we need to modify the project file used by gprbuild. We use the Languages entry, as
in the following example:
project Multilang is
end Multilang;
To interface with data types declared in a C application, you specify the Convention aspect
on the corresponding Ada type declaration. In the following example, we interface with the
C_Enum enumeration declared in a C source file:
Listing 1: show_c_enum.adb
1 procedure Show_C_Enum is
2
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Enum
MD5: a14d7d981fd7d6d806cf3c55f35e19c8
175
Learning Ada
To interface with C's built-in types, we use the Interfaces.C package, which contains most
of the type definitions we need. For example:
Listing 2: show_c_struct.adb
1 with Interfaces.C; use Interfaces.C;
2
3 procedure Show_C_Struct is
4
13 begin
14 null;
15 end Show_C_Struct;
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Struct
MD5: dda4d3f8e4ddf5c5138a990a9a8ac427
Here, we're interfacing with a C struct (C_Struct) and using the corresponding data types
in C (int, long, unsigned and double). This is the declaration in C:
Listing 3: c_struct.h
1 struct c_struct
2 {
3 int a;
4 long b;
5 unsigned c;
6 double d;
7 };
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Struct
MD5: 58709b6a9eea2606d7ec0aaca0a749ff
We use a similar approach when interfacing with subprograms written in C. Consider the
following declaration in the C header file:
Listing 4: my_func.h
1 int my_func (int a);
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Func
MD5: 37b9d7ba668f7ec83c2b27ee33637937
Listing 5: my_func.c
1 #include "my_func.h"
2
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Func
MD5: 284b1639cb393fc14ed196d78429f3ba
We can interface this code in Ada using the Import aspect. For example:
Listing 6: show_c_func.adb
1 with Interfaces.C; use Interfaces.C;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Show_C_Func is
5
14 V : int;
15 begin
16 V := my_func (2);
17 Put_Line ("Result is " & int'Image (V));
18 end Show_C_Func;
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Func
MD5: 6c5d85c1debdeaa642946eacf413dfd2
If you want, you can use a different subprogram name in the Ada code. For example, we
could call the C function Get_Value:
Listing 7: show_c_func.adb
1 with Interfaces.C; use Interfaces.C;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Show_C_Func is
5
15 V : int;
16 begin
17 V := Get_Value (2);
18 Put_Line ("Result is " & int'Image (V));
19 end Show_C_Func;
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Func
MD5: 856b4d99dfaa6946fb4597f254fd2f97
You can also call Ada subprograms from C applications. You do this with the Export aspect.
For example:
Listing 8: c_api.ads
1 with Interfaces.C; use Interfaces.C;
2
3 package C_API is
4
11 end C_API;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Func
MD5: 00aa4ec29fc551e710900e2ee7d96bc9
Listing 9: c_api.adb
1 package body C_API is
2
8 end C_API;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Func
MD5: 2b999ab431bbc1ee223a654ad84b8248
On the C side, we do the same as we would if the function were written in C: simply declare
it using the extern keyword. For example:
7 int v = my_func(2);
8
11 return 0;
12 }
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Func
MD5: 69301036be9be16ed45895c2a86bc352
To use global variables from C code, we use the same method as subprograms: we specify
the Import and Convention aspects for each variable we want to import.
Let's reuse an example from the previous section. We'll add a global variable (func_cnt)
to count the number of times the function (my_func) is called:
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Vars
MD5: 11ba8f7a72ce7058571994870a02b052
3 int func_cnt = 0;
4
9 return a * 2;
10 }
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Vars
MD5: 23631537cb877a03d1243c94cb7b48e8
4 procedure Show_C_Func is
5
11 V : int;
12
13 func_cnt : int
14 with
15 Import => True,
16 Convention => C;
17 -- We can access the func_cnt variable
18 -- from test.c
19
20 begin
21 V := my_func (1);
22 V := my_func (2);
23 V := my_func (3);
24
Project: Courses.Intro_To_Ada.Interfacing_With_C.Ada_C_Vars
MD5: cf64a9dfbc6be853ba19729fe55f0ba4
As we see by running the application, the value of the counter is the number of times
my_func was called.
We can use the External_Name aspect to give a different name for the variable in the Ada
application in the same way we do for subprograms.
You can also use variables declared in Ada files in C applications. In the same way as we
did for subprograms, you do this with the Export aspect.
Let's reuse a past example and add a counter, as in the previous example, but this time
have the counter incremented in Ada code:
3 package C_API is
4
5 func_cnt : int := 0
6 with
7 Export => True,
8 Convention => C;
9
16 end C_API;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Vars
MD5: fc118cddd797b669d2c68e57f90f69b2
9 end C_API;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Vars
MD5: adff5f3088da8b0dd853f1fb8b1e204f
In the C application, we just need to declare the variable and use it:
9 int v;
10
11 v = my_func(1);
12 v = my_func(2);
13 v = my_func(3);
14
20 return 0;
21 }
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Ada_Vars
MD5: 07fb3fbadb8ed4c0543fbfd7b5ef5c57
Again, by running the application, we see that the value from the counter is the number of
times that my_func was called.
In the examples above, we manually added aspects to our Ada code to correspond to the C
source-code we're interfacing with. This is called creating a binding. We can automate this
process by using the Ada spec dump compiler option: -fdump-ada-spec. We illustrate this
by revisiting our previous example.
This was our C header file:
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds
MD5: 11ba8f7a72ce7058571994870a02b052
6 package test_h is
7
14 end test_h;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds
MD5: 8d18aeae72dba3a9ab4f9f3943fab839
5 procedure Show_C_Func is
6 V : int;
7 begin
8 V := my_func (1);
9 V := my_func (2);
10 V := my_func (3);
11
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds
MD5: 8a07aae87b9f36c3fce84b75e8388933
You can specify the name of the parent unit for the bindings you're creating as the operand
to fdump-ada-spec:
5 end Ext_C_Code.test_h;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds_2
MD5: 3bd4087edff145a70d2a6db8543859ad
The compiler does the best it can when creating bindings for a C header file. However,
sometimes it has to guess about the translation and the generated bindings don't always
match our expectations. For example, this can happen when creating bindings for functions
that have pointers as arguments. In this case, the compiler may use System.Address as
the type of one or more pointers. Although this approach works fine (as we'll see later),
this is usually not how a human would interpret the C header file. The following example
illustrates this issue.
Let's start with this C header file:
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds_3
MD5: af642d9ea995bf01f13f8ff41bb0f4f6
5 #include "test.h"
6
7 struct test {
8 char name[80];
9 char address[120];
10 };
11
12 static size_t
13 strlcpy_stat(char *dst,
14 const char *src,
15 size_t dstsize)
16 {
17 size_t len = strlen(src);
18 if (dstsize) {
19 size_t bl = (len < dstsize-1 ?
20 len : dstsize-1);
21 ((char*)memcpy(dst, src, bl))[bl] = 0;
22 }
23 return len;
(continues on next page)
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds_3
MD5: 32652eb76ad92212609680d64e5687d3
8 package test_h is
9
30 end test_h;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds_3
MD5: 3bf8f01b94fd28594e4121a6a36afdf7
As we can see, the binding generator completely ignores the declaration struct test and
all references to the test struct are replaced by addresses (System.Address). Neverthe-
less, these bindings are good enough to allow us to create a test application in Ada:
3 with Interfaces.C;
4 use Interfaces.C;
5
6 with Interfaces.C.Strings;
7 use Interfaces.C.Strings;
8
11 with System;
12
13 procedure Show_Automatic_C_Struct_Bindings is
14
20 T : System.Address := test_create;
21
22 begin
23 test_reset (T);
(continues on next page)
27 test_display (T);
28 test_destroy (T);
29 end Show_Automatic_C_Struct_Bindings;
We can successfully bind our C code with Ada using the automatically-generated bindings,
but they aren't ideal. Instead, we would prefer Ada bindings that match our (human) inter-
pretation of the C header file. This requires manual analysis of the header file. The good
news is that we can use the automatic generated bindings as a starting point and adapt
them to our needs. For example, we can:
1. Define a Test type based on System.Address and use it in all relevant functions.
2. Remove the test_ prefix in all operations on the Test type.
This is the resulting specification:
6 package adapted_test_h is
7
30 end adapted_test_h;
4 with Interfaces.C.Strings;
5 use Interfaces.C.Strings;
6
9 with System;
10
11 procedure Show_Adapted_C_Struct_Bindings is
12
18 T : Test := Create;
19
20 begin
21 Reset (T);
22 Set_Name (T, Name);
23 Set_Address (T, Address);
24
25 Display (T);
26 Destroy (T);
27 end Show_Adapted_C_Struct_Bindings;
Project: Courses.Intro_To_Ada.Interfacing_With_C.C_Binds_3
MD5: 626d07b080fbbd2bf1d5f9140b64955c
Now we can use the Test type and its operations in a clean, readable way.
EIGHTEEN
OBJECT-ORIENTED PROGRAMMING
Note: It's possible to program in Ada without ever creating tagged types. If that's your
prefered style of programming or you have no specific use for tagged types, feel free to not
use them, as is the case for many features of Ada.
However, they can be the best way to express solutions to certain problems and they may
be the best way to solve your problem. If that's the case, read on!
189
Learning Ada
Before presenting tagged types, we should discuss a topic we have brushed on, but not
really covered, up to now:
You can create one or more new types from every type in Ada. Type derivation is built into
the language.
Listing 1: newtypes.ads
1 package Newtypes is
2 type Point is record
3 X, Y : Integer;
4 end record;
5
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Newtypes
MD5: 0d45096755b4bfb08ba8db19ecba3f57
Type derivation is useful to enforce strong typing because the type system treats the two
types as incompatible.
But the benefits are not limited to that: you can inherit things from the type you derive
from. You not only inherit the representation of the data, but you can also inherit behavior.
When you inherit a type you also inherit what are called primitive operations. A primitive
operation (or just a primitive) is a subprogram attached to a type. Ada defines primitives
as subprograms defined in the same scope as the type.
Listing 2: primitives.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Primitives is
4 package Week is
5 type Days is (Monday, Tuesday, Wednesday,
6 Thursday, Friday,
7 Saturday, Sunday);
8
9 -- Print_Day is a primitive
10 -- of the type Days
11 procedure Print_Day (D : Days);
12 end Week;
13
21 use Week;
22 type Weekend_Days is new
23 Days range Saturday .. Sunday;
24
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Primitives
MD5: eb1b0eb66f03a4a17bd9686ec4e12e2e
Runtime output
SATURDAY
This kind of inheritance can be very useful, and is not limited to record types (you can use
it on discrete types, as in the example above), but it's only superficially similar to object-
oriented inheritance:
• Records can't be extended using this mechanism alone. You also can't specify a new
representation for the new type: it will always have the same representation as the
base type.
• There's no facility for dynamic dispatch or polymorphism. Objects are of a fixed, static
type.
There are other differences, but it's not useful to list them all here. Just remember that this
is a kind of inheritance you can use if you only want to statically inherit behavior without
duplicating code or using composition, but a kind you can't use if you want any dynamic
features that are usually associated with OOP.
The 1995 revision of the Ada language introduced tagged types to fullfil the need for an
unified solution that allows programming in an object-oriented style similar to the one de-
scribed at the beginning of this chapter.
Tagged types are very similar to normal records except that some functionality is added:
• Types have a tag, stored inside each object, that identifies the runtime type20 of that
object.
• Primitives can dispatch. A primitive on a tagged type is what you would call a method
in Java or C++. If you derive a base type and override a primitive of it, you can often
call it on an object with the result that which primitive is called depends on the exact
runtime type of the object.
20 https://en.wikipedia.org/wiki/Run-time_type_information
• Subtyping rules are introduced allowing a tagged type derived from a base type to be
statically compatible with the base type.
Let's see our first tagged type declarations:
Listing 3: p.ads
1 package P is
2 type My_Class is tagged null record;
3 -- Just like a regular record, but
4 -- with tagged qualifier
5
21 overriding
22 procedure Foo (Self : in out Derived);
23 -- The "overriding" qualifier is optional,
24 -- but if it is present, it must be valid.
25 end P;
Listing 4: p.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 package body P is
4 procedure Foo (Self : in out My_Class) is
5 begin
6 Put_Line ("In My_Class.Foo");
7 end Foo;
8
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: 45baaad66a1047358addb574d0fa00bc
To remain consistent with the rest of the language, a new notation needed to be introduced
to say "This object is of this type or any descendant derives tagged type".
In Ada, we call this the classwide type. It's used in OOP as soon as you need polymorphism.
For example, you can't do the following:
Listing 5: main.adb
1 with P; use P;
2
3 procedure Main is
4
5 O1 : My_Class;
6 -- Declaring an object of type My_Class
7
11 O3 : My_Class := O2;
12 -- INVALID: Trying to assign a value
13 -- of type derived to a variable of
14 -- type My_Class.
15 begin
16 null;
17 end Main;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: c87ad8bb686cb1763740750846258357
Build output
This is because an object of a type T is exactly of the type T, whether T is tagged or not.
What you want to say as a programmer is "I want O3 to be able to hold an object of type
My_Class or any type descending from My_Class". Here's how you do that:
Listing 6: main.adb
1 with P; use P;
2
3 procedure Main is
4 O1 : My_Class;
5 -- Declare an object of type My_Class
6
10 O3 : My_Class'Class := O2;
11 -- Now valid: My_Class'Class designates
12 -- the classwide type for My_Class,
13 -- which is the set of all types
14 -- descending from My_Class (including
15 -- My_Class).
16 begin
(continues on next page)
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: 35412176a248015a26e507164ce526af
Attention: Because an object of a classwide type can be the size of any descendant of
its base type, it has an unknown size. It's therefore an indefinite type, with the expected
restrictions:
• It can't be stored as a field/component of a record
• An object of a classwide type needs to be initialized immediately (you can't specify
the constraints of such a type in any way other than by initializing it).
We saw that you can override operations in types derived from another tagged type. The
eventual goal of OOP is to make a dispatching call: a call to a primitive (method) that
depends on the exact type of the object.
But, if you think carefully about it, a variable of type My_Class always contains an object
of exactly that type. If you want to have a variable that can contain a My_Class or any
derived type, it has to be of type My_Class'Class.
In other words, to make a dispatching call, you must first have an object that can be either
of a type or any type derived from this type, namely an object of a classwide type.
Listing 7: main.adb
1 with P; use P;
2
3 procedure Main is
4 O1 : My_Class;
5 -- Declare an object of type My_Class
6
10 O3 : My_Class'Class := O2;
11
12 O4 : My_Class'Class := O1;
13 begin
14 Foo (O1);
15 -- Non dispatching: Calls My_Class.Foo
16 Foo (O2);
17 -- Non dispatching: Calls Derived.Foo
18 Foo (O3);
19 -- Dispatching: Calls Derived.Foo
20 Foo (O4);
21 -- Dispatching: Calls My_Class.Foo
22 end Main;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: 7631f823b0dd9e5474f6bb2dc35af2a2
Runtime output
In My_Class.Foo
In Derived.Foo, A = 12
In Derived.Foo, A = 12
In My_Class.Foo
Attention
You can convert an object of type Derived to an object of type My_Class. This is called
a view conversion in Ada parlance and is useful, for example, if you want to call a parent
method.
In that case, the object really is converted to a My_Class object, which means its tag is
changed. Since tagged objects are always passed by reference, you can use this kind of
conversion to modify the state of an object: changes to converted object will affect the
original one.
Listing 8: main.adb
1 with P; use P;
2
3 procedure Main is
4 O1 : Derived := (A => 12);
5 -- Declare an object of type Derived
6
9 O3 : My_Class'Class := O2;
10 begin
11 Foo (O1);
12 -- Non dispatching: Calls Derived.Foo
13 Foo (O2);
14 -- Non dispatching: Calls My_Class.Foo
15
16 Foo (O3);
17 -- Dispatching: Calls My_Class.Foo
18 end Main;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: b92112b05201ff14789baca258fa0cbc
Runtime output
In Derived.Foo, A = 12
In My_Class.Foo
In My_Class.Foo
You can also call primitives of tagged types with a notation that's more familiar to object ori-
ented programmers. Given the Foo primitive above, you can also write the above program
this way:
Listing 9: main.adb
1 with P; use P;
2
3 procedure Main is
4 O1 : My_Class;
5 -- Declare an object of type My_Class
6
10 O3 : My_Class'Class := O2;
11
12 O4 : My_Class'Class := O1;
13 begin
14 O1.Foo;
15 -- Non dispatching: Calls My_Class.Foo
16 O2.Foo;
17 -- Non dispatching: Calls Derived.Foo
18 O3.Foo;
19 -- Dispatching: Calls Derived.Foo
20 O4.Foo;
21 -- Dispatching: Calls My_Class.Foo
22 end Main;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: 9c6ebdfec9ceeb986d92eb90ec9ff59b
Runtime output
In My_Class.Foo
In Derived.Foo, A = 12
In Derived.Foo, A = 12
In My_Class.Foo
If the dispatching parameter of a primitive is the first parameter, which is the case in our
examples, you can call the primitive using the dot notation. Any remaining parameter are
passed normally:
3 procedure Main is
4 package Extend is
5 type D2 is new Derived with null record;
6
19 use Extend;
20
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Types
MD5: fec4f5cc4213cc111708dcc276e870c2
Runtime output
In Derived.Foo, A = 17
We've seen previously (in the Privacy (page 113) chapter) that types can be declared limited
or private. These encapsulation techniques can also be applied to tagged types, as we'll
see in this section.
This is an example of a tagged private type:
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Private_Types
MD5: 4cd4bcd1a54d5f6407a500558b5da417
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Limited_Types
MD5: 13228777133aa6db97da1c29f732459c
Naturally, you can combine both limited and private types and declare a tagged limited
private type:
8 end P;
3 procedure Main is
4 T1, T2 : T;
5 begin
6 T1.Init;
7 T2.Init;
8
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Tagged_Limited_Private_
↪Types
MD5: 68240374505bcaf7aad4ebaed3b9127b
Note that the code in the Main procedure above presents two assignments that trigger
compilation errors because type T is limited private. In fact, you cannot:
• assign to T1.E directly because type T is private;
• assign T1 to T2 because type T is limited.
In this case, there's no distinction between tagged and non-tagged types: these compilation
errors would also occur for non-tagged types.
In this section, we'll discuss an useful pattern for object-oriented programming in Ada: class-
wide access type. Let's start with an example where we declare a tagged type T and a
derived type T_New:
3 package body P is
4
17 end P;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Classwide_Error
MD5: fd5cb99925d3c88536546aa0be8104b7
Note that we're using null records for both types T and T_New. Although these types don't
actually have any component, we can still use them to demonstrate dispatching. Also note
that the example above makes use of the 'External_Tag attribute in the implementation
of the Show procedure to get a string for the corresponding tagged type.
As we've seen before, we must use a classwide type to create objects that can make dis-
patching calls. In other words, objects of type T'Class will dispatch. For example:
3 procedure Dispatching_Example is
(continues on next page)
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Classwide_Error
MD5: f8957b31c9c62db23759baad7b867a57
Runtime output
A more useful application is to declare an array of objects that can dispatch. For example,
we'd like to declare an array T_Arr, loop over this array and dispatch according to the actual
type of each individual element:
3 procedure Classwide_Compilation_Error is
4 T_Arr : array (1 .. 2) of T'Class;
5 -- ^
6 -- Compilation Error!
7 begin
8 for I in T_Arr'Range loop
9 T_Arr (I).Show;
10 end loop;
11 end Classwide_Compilation_Error;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Classwide_Error
MD5: e86f6c6ee35dced8f330bf6177d178fd
Build output
In fact, it's impossible for the compiler to know which type would actually be used for each
element of the array. However, if we use dynamic allocation via access types, we can
allocate objects of different types for the individual elements of an array T_Arr. We do this
by using classwide access types, which have the following format:
We can rewrite the previous example using the T_Class type. In this case, dynamically
allocated objects of this type will dispatch according to the actual type used during the
allocation. Also, let's introduce an Init procedure that won't be overridden for the derived
T_New type. This is the adapted code:
16 end P;
3 package body P is
4
23 end P;
4 procedure Main is
5 T_Arr : array (1 .. 2) of T_Class;
6 begin
7 T_Arr (1) := new T;
8 T_Arr (2) := new T_New;
9
14 T_Arr (I).Init;
15 T_Arr (I).Show;
16
17 Put_Line ("-----------");
18 end loop;
19 end Main;
Project: Courses.Intro_To_Ada.Object_Oriented_Programming.Classwide_Access
MD5: 97c05a8f911d0a0e39c0cc90fae184a7
Runtime output
Element # 1
Initializing type T...
Using type P.T
-----------
Element # 2
Initializing type T...
Using type P.T_NEW
-----------
In this example, the first element (T_Arr (1)) is of type T, while the second element is
of type T_New. When running the example, the Init procedure of type T is called for both
elements of the T_Arr array, while the call to the Show procedure selects the corresponding
procedure according to the type of each element of T_Arr.
NINETEEN
In previous chapters, we've used arrays as the standard way to group multiple objects of a
specific data type. In many cases, arrays are good enough for manipulating those objects.
However, there are situations that require more flexibility and more advanced operations.
For those cases, Ada provides support for containers — such as vectors and sets — in its
standard library.
We present an introduction to containers here. For a list of all containers available in Ada,
see Appendix B (page 271).
19.1 Vectors
19.1.1 Instantiation
Listing 1: show_vector_inst.adb
1 with Ada.Containers.Vectors;
2
3 procedure Show_Vector_Inst is
4
10 V : Integer_Vectors.Vector;
11 begin
12 null;
13 end Show_Vector_Inst;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Inst
MD5: 8b737842d2784f25502990f21e1cf6de
Containers are based on generic packages, so we can't simply declare a vector as we would
declare an array of a specific type:
203
Learning Ada
Instead, we first need to instantiate one of those packages. We with the container pack-
age (Ada.Containers.Vectors in this case) and instantiate it to create an instance of the
generic package for the desired type. Only then can we declare the vector using the type
from the instantiated package. This instantiation needs to be done for any container type
from the standard library.
In the instantiation of Integer_Vectors, we indicate that the vector contains elements of
Integer type by specifying it as the Element_Type. By setting Index_Type to Natural, we
specify that the allowed range includes all natural numbers. We could have used a more
restrictive range if desired.
19.1.2 Initialization
One way to initialize a vector is from a concatenation of elements. We use the & operator,
as shown in the following example:
Listing 2: show_vector_init.adb
1 with Ada.Containers; use Ada.Containers;
2 with Ada.Containers.Vectors;
3
6 procedure Show_Vector_Init is
7
13 use Integer_Vectors;
14
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Init
MD5: 0087b0a15e0c88b27ac36c3b27159a17
Runtime output
We specify use Integer_Vectors, so we have direct access to the types and operations
from the instantiated package. Also, the example introduces another operation on the
vector: Length, which retrieves the number of elements in the vector. We can use the dot
notation because Vector is a tagged type, allowing us to write either V.Length or Length
(V).
You add elements to a vector using the Prepend and Append operations. As the names
suggest, these operations add elements to the beginning or end of a vector, respectively.
For example:
Listing 3: show_vector_append.adb
1 with Ada.Containers; use Ada.Containers;
2 with Ada.Containers.Vectors;
3
6 procedure Show_Vector_Append is
7
13 use Integer_Vectors;
14
15 V : Vector;
16 begin
17 Put_Line ("Appending some elements "
18 & "to the vector...");
19 V.Append (20);
20 V.Append (10);
21 V.Append (0);
22 V.Append (13);
23 Put_Line ("Finished appending.");
24
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Append
MD5: f88d393ba96a7950f58d9f1c0c74a021
Runtime output
This example puts elements into the vector in the following sequence: (100, 40, 30, 20, 10,
0, 13).
The Reference Manual specifies that the worst-case complexity must be:
• O(log N) for the Append operation, and
We access the first and last elements of a vector using the First_Element and
Last_Element functions. For example:
Listing 4: show_vector_first_last_element.adb
1 with Ada.Containers; use Ada.Containers;
2 with Ada.Containers.Vectors;
3
6 procedure Show_Vector_First_Last_Element is
7
13 use Integer_Vectors;
14
26 -- Using V.First_Element to
27 -- retrieve first element
28 Put_Line ("First element is "
29 & Img (V.First_Element));
30
31 -- Using V.Last_Element to
32 -- retrieve last element
33 Put_Line ("Last element is "
34 & Img (V.Last_Element));
35 end Show_Vector_First_Last_Element;
Runtime output
Vector has 4 elements
First element is 20
Last element is 13
You can swap elements by calling the procedure Swap and retrieving a reference (a cursor)
to the first and last elements of the vector by calling First and Last. A cursor allows us to
iterate over a container and process individual elements from it.
With these operations, we're able to write code to swap the first and last elements of a
vector:
Listing 5: show_vector_first_last_element.adb
1 with Ada.Containers; use Ada.Containers;
2 with Ada.Containers.Vectors;
3
6 procedure Show_Vector_First_Last_Element is
7
13 use Integer_Vectors;
14
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_First_Last_Element
MD5: 1a0c0bf28bb661b3f328473ac3c2eb54
Runtime output
19.1.5 Iterating
The easiest way to iterate over a container is to use a for E of Our_Container loop. This
gives us a reference (E) to the element at the current position. We can then use E directly.
For example:
Listing 6: show_vector_iteration.adb
1 with Ada.Containers.Vectors;
2
5 procedure Show_Vector_Iteration is
6
12 use Integer_Vectors;
13
21 --
22 -- Using for ... of loop to iterate:
23 --
24 for E of V loop
25 Put_Line ("- " & Img (E));
26 end loop;
27
28 end Show_Vector_Iteration;
Runtime output
Vector elements are:
- 20
- 10
- 0
- 13
We can also use indices to access vector elements. The format is similar to a loop over array
elements: we use a for I in <range> loop. The range is provided by V.First_Index and
V.Last_Index. We can access the current element by using it as an array index: V (I).
For example:
Listing 7: show_vector_index_iteration.adb
1 with Ada.Containers.Vectors;
2
5 procedure Show_Vector_Index_Iteration is
6
18 --
19 -- Using indices in a "for I in ..." loop
20 -- to iterate:
21 --
22 for I in V.First_Index .. V.Last_Index loop
23 -- Displaying current index I
24 Put ("- ["
25 & Extended_Index'Image (I)
26 & "] ");
27
34 New_Line;
35 end loop;
36
37 end Show_Vector_Index_Iteration;
Runtime output
Vector elements are:
- [ 0] 20
- [ 1] 10
- [ 2] 0
- [ 3] 13
Here, in addition to displaying the vector elements, we're also displaying each index, I, just
like what we can do for array indices. Also, we can access the element by using either the
short form V (I) or the longer form V.Element (I) but not V.I.
As mentioned in the previous section, you can use cursors to iterate over containers. For
this, use the function Iterate, which retrieves a cursor for each position in the vector. The
corresponding loop has the format for C in V.Iterate loop. Like the previous example
using indices, you can again access the current element by using the cursor as an array
index: V (C). For example:
Listing 8: show_vector_cursor_iteration.adb
1 with Ada.Containers.Vectors;
2
5 procedure Show_Vector_Cursor_Iteration is
6
12 use Integer_Vectors;
13
18 --
19 -- Use a cursor to iterate in a loop:
20 --
21 for C in V.Iterate loop
22 -- Using To_Index function to retrieve
23 -- the index for the cursor position
24 Put ("- ["
25 & Extended_Index'Image (To_Index (C))
26 & "] ");
27
34 New_Line;
35 end loop;
36
50 end Show_Vector_Cursor_Iteration;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Cursor_Iteration
MD5: de789bbd2e1814aae3fb5213c99ac25c
Runtime output
Instead of accessing an element in the loop using V (C), we could also have used the longer
form Element (C). In this example, we're using the function To_Index to retrieve the index
corresponding to the current cursor.
As shown in the comments after the loop, we could also use a while ... loop to iterate
over the vector. In this case, we would start with a cursor for the first element (retrieved
by calling V.First) and then call Next (C) to retrieve a cursor for subsequent elements.
Next (C) returns No_Element when the cursor reaches the end of the vector.
You can directly modify the elements using a reference. This is what it looks like when using
both indices and cursors:
The Reference Manual requires that the worst-case complexity for accessing an element be
O(log N).
Another way of modifying elements of a vector is using a process procedure, which takes
an individual element and does some processing on it. You can call Update_Element and
pass both a cursor and an access to the process procedure. For example:
Listing 9: show_vector_update.adb
1 with Ada.Containers.Vectors;
2
5 procedure Show_Vector_Update is
6
12 use Integer_Vectors;
13
28 end Show_Vector_Update;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Update
MD5: 5dcc3dd8020632a8ea2ce975ecd8f4da
You can locate a specific element in a vector by retrieving its index. Find_Index retrieves
the index of the first element matching the value you're looking for. Alternatively, you can
use Find to retrieve a cursor referencing that element. For example:
5 procedure Show_Find_Vector_Element is
6
12 use Integer_Vectors;
13
Project: Courses.Intro_To_Ada.Standard_Library.Show_Find_Vector_Element
MD5: c3da01cd66c8705a7cbccae8390d5f81
Runtime output
As we saw in the previous section, we can directly access vector elements by using either
an index or cursor. However, an exception is raised if we try to access an element with an
invalid index or cursor, so we must check whether the index or cursor is valid before using
it to access an element. In our example, Find_Index or Find might not have found the
element in the vector. We check for this possibility by comparing the index to No_Index or
the cursor to No_Element. For example:
Instead of writing V (C) := 14, we could use the longer form V.Replace_Element (C,
14).
In the previous sections, we've seen examples of how to add elements to a vector:
• using the concatenation operator (&) at the vector declaration, or
• calling the Prepend and Append procedures.
You may want to insert an element at a specific position, e.g. before a certain element in
the vector. You do this by calling Insert. For example:
6 procedure Show_Vector_Insert is
7
13 use Integer_Vectors;
14
35 New_Line;
36 Put_Line ("Adding element with value 9");
37 Put_Line (" (before 10)...");
38
39 --
(continues on next page)
48 Show_Elements (V);
49
50 end Show_Vector_Insert;
Runtime output
In this example, we're looking for an element with the value of 10. If we find it, we insert
an element with the value of 9 before it.
You can remove elements from a vector by passing either a valid index or cursor to the
Delete procedure. If we combine this with the functions Find_Index and Find from the
previous section, we can write a program that searches for a specific element and deletes
it, if found:
5 procedure Show_Remove_Vector_Element is
6 package Integer_Vectors is new
7 Ada.Containers.Vectors
8 (Index_Type => Natural,
9 Element_Type => Integer);
10
11 use Integer_Vectors;
(continues on next page)
37 end Show_Remove_Vector_Element;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Remove_Vector_Element
MD5: 540d0dc5715e58926e9dc4600bd6ad5d
We can extend this approach to delete all elements matching a certain value. We just need
to keep searching for the element in a loop until we get an invalid index or cursor. For
example:
6 procedure Show_Remove_Vector_Elements is
7
13 use Integer_Vectors;
14
34 --
35 -- Remove elements using an index
36 --
37 declare
38 E : constant Integer := 10;
39 I : Extended_Index;
40 begin
41 New_Line;
42 Put_Line
43 ("Removing all elements with value of "
44 & Integer'Image (E) & "...");
45 loop
46 I := V.Find_Index (E);
47 exit when I = No_Index;
48 V.Delete (I);
49 end loop;
50 end;
51
52 --
53 -- Remove elements using a cursor
54 --
55 declare
56 E : constant Integer := 13;
57 C : Cursor;
58 begin
59 New_Line;
60 Put_Line
61 ("Removing all elements with value of "
62 & Integer'Image (E) & "...");
63 loop
64 C := V.Find (E);
65 exit when C = No_Element;
66 V.Delete (C);
67 end loop;
68 end;
69
70 Show_Elements (V);
71 end Show_Remove_Vector_Elements;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Remove_Vector_Elements
MD5: 6e364843b9638224bd9a36eb9d45e446
Runtime output
In this example, we remove all elements with the value 10 from the vector by retrieving
their index. Likewise, we remove all elements with the value 13 by retrieving their cursor.
We've seen some operations on vector elements. Here, we'll see operations on the vector
as a whole. The most prominent is the concatenation of multiple vectors, but we'll also see
operations on vectors, such as sorting and sorted merging operations, that view the vector
as a sequence of elements and operate on the vector considering the element's relations
to each other.
We do vector concatenation using the & operator on vectors. Let's consider two vectors V1
and V2. We can concatenate them by doing V := V1 & V2. V contains the resulting vector.
The generic package Generic_Sorting is a child package of Ada.Containers.Vectors. It
contains sorting and merging operations. Because it's a generic package, you can't use it
directly, but have to instantiate it. In order to use these operations on a vector of integer
values (Integer_Vectors, in our example), you need to instantiate it directly as a child of
Integer_Vectors. The next example makes it clear how to do this.
After instantiating Generic_Sorting, we make all the operations available to us with the
use statement. We can then call Sort to sort the vector and Merge to merge one vector
into another.
The following example presents code that manipulates three vectors (V1, V2, V3) using the
concatenation, sorting and merging operations:
6 procedure Show_Vector_Ops is
7
13 package Integer_Vectors_Sorting is
14 new Integer_Vectors.Generic_Sorting;
15
40 New_Line;
41 Put_Line ("---- V1 ----");
42 Show_Elements (V1);
43
44 New_Line;
45 Put_Line ("---- V2 ----");
46 Show_Elements (V2);
47
48 New_Line;
49 Put_Line ("---- V3 ----");
50 Show_Elements (V3);
51
52 New_Line;
53 Put_Line
54 ("Concatenating V1, V2 and V3 into V:");
55
58 Show_Elements (V);
59
60 New_Line;
61 Put_Line ("Sorting V:");
62
63 Sort (V);
64
65 Show_Elements (V);
66
67 New_Line;
68 Put_Line ("Merging V2 into V1:");
69
72 Show_Elements (V1);
73
74 end Show_Vector_Ops;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Vector_Ops
MD5: 3301513e4e7fd2f28488966e5b24e448
Runtime output
---- V1 ----
---- V2 ----
---- V3 ----
Sorting V:
The Reference Manual requires that the worst-case complexity of a call to Sort be O(N2 )
and the average complexity be better than O(N2 ).
19.2 Sets
Sets are another class of containers. While vectors allow duplicated elements to be in-
serted, sets ensure that no duplicated elements exist.
In the following sections, we'll see operations you can perform on sets. However, since
many of the operations on vectors are similar to the ones used for sets, we'll cover them
more quickly here. Please refer back to the section on vectors for a more detailed discus-
sion.
To initialize a set, you can call the Insert procedure. However, if you do, you need to
ensure no duplicate elements are being inserted: if you try to insert a duplicate, you'll get
an exception. If you have less control over the elements to be inserted so that there may
be duplicates, you can use another option instead:
• a version of Insert that returns a Boolean value indicating whether the insertion was
successful;
• the Include procedure, which silently ignores any attempt to insert a duplicated ele-
ment.
To iterate over a set, you can use a for E of S loop, as you saw for vectors. This gives
you a reference to each element in the set.
Let's see an example:
6 procedure Show_Set_Init is
7
12 use Integer_Sets;
13
14 S : Set;
15 -- Same as: S : Integer_Sets.Set;
16 C : Cursor;
17 Ins : Boolean;
18 begin
19 S.Insert (20);
20 S.Insert (10);
(continues on next page)
48 --
49 -- Iterate over set using for .. of loop
50 --
51 Put_Line ("Elements:");
52 for E of S loop
53 Put_Line ("- " & Integer'Image (E));
54 end loop;
55 end Show_Set_Init;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Set_Init
MD5: b87f6729fea278396347248b95a30cb6
Runtime output
6 procedure Show_Set_Element_Ops is
7
12 use Integer_Sets;
13
26 S : Set;
27 begin
28 S.Insert (20);
29 S.Insert (10);
30 S.Insert (0);
31 S.Insert (13);
32
33 S.Delete (13);
34
52 Show_Elements (S);
53 end Show_Set_Element_Ops;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Set_Element_Ops
MD5: 77fb2aaba4221e337b0f90dd1a49c556
Runtime output
In addition to ordered sets used in the examples above, the standard library also offers
hashed sets. The Reference Manual requires the following average complexity of each
operation:
The previous sections mostly dealt with operations on individual elements of a set. But
Ada also provides typical set operations: union, intersection, difference and symmetric
difference. In contrast to some vector operations we've seen before (e.g. Merge), here
you can use built-in operators, such as -. The following table lists the operations and its
associated operator:
6 procedure Show_Set_Ops is
7
12 use Integer_Sets;
13
38 S2.Insert (0);
39 S2.Insert (10);
40 S2.Insert (14);
41
42 S3.Insert (0);
43 S3.Insert (10);
44
45 New_Line;
46 Put_Line ("---- Set #1 ----");
47 Show_Elements (S1);
48
49 New_Line;
50 Put_Line ("---- Set #2 ----");
51 Show_Elements (S2);
52
53 New_Line;
54 Put_Line ("---- Set #3 ----");
55 Show_Elements (S3);
56
57 New_Line;
58 if S3.Is_Subset (S1) then
59 Put_Line ("S3 is a subset of S1");
(continues on next page)
64 S3 := S1 and S2;
65 Show_Op (S3, "Intersection");
66 Show_Elements (S3);
67
68 S3 := S1 or S2;
69 Show_Op (S3, "Union");
70 Show_Elements (S3);
71
72 S3 := S1 - S2;
73 Show_Op (S3, "Difference");
74 Show_Elements (S3);
75
76 S3 := S1 xor S2;
77 Show_Op (S3, "Symmetric difference");
78 Show_Elements (S3);
79
80 end Show_Set_Ops;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Set_Ops
MD5: be9086591fc643e53facaf2ffea6c26d
Runtime output
S3 is a subset of S1
The previous sections presented containers for elements of definite types. Although most
examples in those sections presented Integer types as element type of the containers,
containers can also be used with indefinite types, an example of which is the String type.
However, indefinite types require a different kind of containers designed specially for them.
We'll also be exploring a different class of containers: maps. They associate a key with a
specific value. An example of a map is the one-to-one association between a person and
their age. If we consider a person's name to be the key, the value is the person's age.
Hashed maps are maps that make use of a hash as a key. The hash itself is calculated by
a function you provide.
In other languages
Hashed maps are similar to dictionaries in Python and hashes in Perl. One of the main
differences is that these scripting languages allow using different types for the values con-
tained in a single map, while in Ada, both the type of key and value are specified in the
package instantiation and remains constant for that specific map. You can't have a map
where two elements are of different types or two keys are of different types. If you want
to use multiple types, you must create a different map for each and use only one type in
each map.
is raised. To verify if a key is available, use the function Contains (as we've seen above in
the section on sets).
Let's see an example:
6 procedure Show_Hashed_Map is
7
15 use Integer_Hashed_Maps;
16
17 M : Map;
18 -- Same as:
19 --
20 -- M : Integer_Hashed_Maps.Map;
21 begin
22 M.Include ("Alice", 24);
23 M.Include ("John", 40);
24 M.Include ("Bob", 28);
25
42 end Show_Hashed_Map;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Hashed_Map
MD5: 6117775bd9ce2b1466f448b100117ded
Runtime output
Alice's age is 24
Ordered maps share many features with hashed maps. The main differences are:
• A hash function isn't needed. Instead, you must provide an ordering function (< oper-
ator), which the ordered map will use to order elements and allow fast access, O(log
N), using a binary search.
– If the type specified in Key_Type has a standard < operator, you can use it in a
similar way as we did for Equivalent_Keys above for hashed maps.
Let's see an example:
5 procedure Show_Ordered_Map is
6
12 use Integer_Ordered_Maps;
13
14 M : Map;
15 begin
16 M.Include ("Alice", 24);
17 M.Include ("John", 40);
18 M.Include ("Bob", 28);
19
35 end Show_Ordered_Map;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Ordered_Map
MD5: 3deb3c685e767cee271b06e87727b086
Runtime output
Alice's age is 24
You can see a great similarity between the examples above and from the previous section.
In fact, since both kinds of maps share many operations, we didn't need to make extensive
modifications when we changed our example to use ordered maps instead of hashed maps.
The main difference is seen when we run the examples: the output of a hashed map is
usually unordered, but the output of a ordered map is always ordered, as implied by its
name.
19.3.3 Complexity
Hashed maps are generally the fastest data structure available to you in Ada if you need to
associate heterogeneous keys to values and search for them quickly. In most cases, they
are slightly faster than ordered maps. So if you don't need ordering, use hashed maps.
The Reference Manual requires the following average complexity of operations:
TWENTY
The standard library supports processing of dates and times using two approaches:
• Calendar approach, which is suitable for handling dates and times in general;
• Real-time approach, which is better suited for real-time applications that require en-
hanced precision — for example, by having access to an absolute clock and handling
time spans. Note that this approach only supports times, not dates.
The following sections present these two approaches.
The Ada.Calendar package supports handling of dates and times. Let's look at a simple
example:
Listing 1: display_current_time.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar; use Ada.Calendar;
3
4 with Ada.Calendar.Formatting;
5 use Ada.Calendar.Formatting;
6
7 procedure Display_Current_Time is
8 Now : Time := Clock;
9 begin
10 Put_Line ("Current time: " & Image (Now));
11 end Display_Current_Time;
Project: Courses.Intro_To_Ada.Standard_Library.Display_Current_Time
MD5: 4a88069b33ecf80314b0164a472ff606
Runtime output
This example displays the current date and time, which is retrieved by a call to the Clock
function. We call the function Image from the Ada.Calendar.Formatting package to get
a String for the current date and time. We could instead retrieve each component using
the Split function. For example:
231
Learning Ada
Listing 2: display_current_year.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar; use Ada.Calendar;
3
4 procedure Display_Current_Year is
5 Now : Time := Clock;
6
7 Now_Year : Year_Number;
8 Now_Month : Month_Number;
9 Now_Day : Day_Number;
10 Now_Seconds : Day_Duration;
11 begin
12 Split (Now,
13 Now_Year,
14 Now_Month,
15 Now_Day,
16 Now_Seconds);
17
Runtime output
Current year is: 2024
Current month is: 6
Current day is: 30
You can delay an application so that it restarts at a specific date and time. We saw some-
thing similar in the chapter on tasking. You do this using a delay until statement. For
example:
Listing 3: display_delay_next_specific_time.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar; use Ada.Calendar;
3
4 with Ada.Calendar.Formatting;
5 use Ada.Calendar.Formatting;
6
7 with Ada.Calendar.Time_Zones;
8 use Ada.Calendar.Time_Zones;
9
10 procedure Display_Delay_Next_Specific_Time is
11 TZ : Time_Offset := UTC_Time_Offset;
12 Next : Time :=
(continues on next page)
Project: Courses.Intro_To_Ada.Standard_Library.Display_Delay_Next_Specific_Time
MD5: 36ec2bdce7c1e8d107fae54ef9852d3f
Runtime output
In this example, we specify the date and time by initializing Next using a call to Time_Of,
a function taking the various components of a date (year, month, etc) and returning an
element of the Time type. Because the date specified is in the past, the delay until
statement won't produce any noticeable effect. However, if we passed a date in the future,
the program would wait until that specific date and time arrived.
Here we're converting the time to the local timezone. If we don't specify a timezone, Co-
ordinated Universal Time (abbreviated to UTC) is used by default. By retrieving the time
offset to UTC with a call to UTC_Time_Offset from the Ada.Calendar.Time_Zones package,
we can initialize TZ and use it in the call to Time_Of. This is all we need do to make the
information provided to Time_Of relative to the local time zone.
We could achieve a similar result by initializing Next with a String. We can do this with a
call to Value from the Ada.Calendar.Formatting package. This is the modified code:
Listing 4: display_delay_next_specific_time.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar; use Ada.Calendar;
3
4 with Ada.Calendar.Formatting;
5 use Ada.Calendar.Formatting;
6
7 with Ada.Calendar.Time_Zones;
8 use Ada.Calendar.Time_Zones;
9
10 procedure Display_Delay_Next_Specific_Time is
11 TZ : Time_Offset := UTC_Time_Offset;
(continues on next page)
Project: Courses.Intro_To_Ada.Standard_Library.Display_Delay_Next_Specific_Time
MD5: fdf6ad7fca303d4d7bd444c23e11c7bd
Runtime output
In this example, we're again using TZ in the call to Value to adjust the input time to the
current time zone.
In the examples above, we were delaying to a specific date and time. Just like we saw in
the tasking chapter, we could instead specify the delay relative to the current time. For
example, we could delay by 5 seconds, using the current time:
Listing 5: display_delay_next.adb
1 with Ada.Calendar; use Ada.Calendar;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Display_Delay_Next is
5 D : Duration := 5.0;
6 -- ^ seconds
7 Now : Time := Clock;
8 Next : Time := Now + D;
9 -- ^ use duration to
10 -- specify next
11 -- point in time
12 begin
13 Put_Line ("Let's wait "
14 & Duration'Image (D)
15 & " seconds...");
16 delay until Next;
17 Put_Line ("Enough waiting!");
18 end Display_Delay_Next;
Project: Courses.Intro_To_Ada.Standard_Library.Display_Delay_Next
MD5: 58360d93388c3fe027c3d9d67389efc7
Runtime output
Here, we're specifying a duration of 5 seconds in D, adding it to the current time from Now,
and storing the sum in Next. We then use it in the delay until statement.
20.2 Real-time
In addition to Ada.Calendar, the standard library also supports time operations for real-time
applications. These are included in the Ada.Real_Time package. This package also include
a Time type. However, in the Ada.Real_Time package, the Time type is used to represent
an absolute clock and handle a time span. This contrasts with the Ada.Calendar, which
uses the Time type to represent dates and times.
In the previous section, we used the Time type from the Ada.Calendar and the delay until
statement to delay an application by 5 seconds. We could have used the Ada.Real_Time
package instead. Let's modify that example:
Listing 6: display_delay_next_real_time.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Real_Time; use Ada.Real_Time;
3
4 procedure Display_Delay_Next_Real_Time is
5 D : Time_Span := Seconds (5);
6 Next : Time := Clock + D;
7 begin
8 Put_Line ("Let's wait "
9 & Duration'Image (To_Duration (D))
10 & " seconds...");
11 delay until Next;
12 Put_Line ("Enough waiting!");
13 end Display_Delay_Next_Real_Time;
Project: Courses.Intro_To_Ada.Standard_Library.Display_Delay_Next_Real_Time
MD5: a80e96c4ac7bd3ba7813f983b10cb038
Runtime output
The main difference is that D is now a variable of type Time_Span, defined in the Ada.
Real_Time package. We call the function Seconds to initialize D, but could have gotten a
finer granularity by calling Nanoseconds instead. Also, we need to first convert D to the
Duration type using To_Duration before we can display it.
20.2.1 Benchmarking
Listing 7: display_benchmarking.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Real_Time; use Ada.Real_Time;
3
4 procedure Display_Benchmarking is
5
6 procedure Computational_Intensive_App is
7 begin
8 delay 5.0;
9 end Computational_Intensive_App;
10
14 begin
15 Start_Time := Clock;
16
17 Computational_Intensive_App;
18
19 Stop_Time := Clock;
20 Elapsed_Time := Stop_Time - Start_Time;
21
Project: Courses.Intro_To_Ada.Standard_Library.Display_Benchmarking
MD5: 4b20940cb613d3f634be5224f409efeb
Runtime output
Listing 8: display_benchmarking_cpu_time.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Real_Time; use Ada.Real_Time;
3 with Ada.Execution_Time; use Ada.Execution_Time;
4
5 procedure Display_Benchmarking_CPU_Time is
6
7 procedure Computational_Intensive_App is
8 begin
(continues on next page)
15 begin
16 Start_Time := Clock;
17
18 Computational_Intensive_App;
19
20 Stop_Time := Clock;
21 Elapsed_Time := Stop_Time - Start_Time;
22
Runtime output
CPU time: 0.000089157 seconds
In this example, Start_Time and Stop_Time are of type CPU_Time instead of Time. How-
ever, we still call the Clock function to initialize both variables and calculate the elapsed
time in the same way as before. By running this program, we see that the CPU time is sig-
nificantly lower than the 5 seconds we've seen before. This is because the delay statement
doesn't require much CPU time. The results will be different if we change the implementa-
tion of Computational_Intensive_App to use a mathematical function in a long loop. For
example:
Listing 9: display_benchmarking_math.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Real_Time; use Ada.Real_Time;
3 with Ada.Execution_Time; use Ada.Execution_Time;
4
5 with Ada.Numerics.Generic_Elementary_Functions;
6
7 procedure Display_Benchmarking_Math is
8
9 procedure Computational_Intensive_App is
10 package Funcs is new
11 Ada.Numerics.Generic_Elementary_Functions
12 (Float_Type => Long_Long_Float);
13 use Funcs;
14
15 X : Long_Long_Float;
16 begin
17 for I in 0 .. 1_000_000 loop
18 X := Tan (Arctan
19 (Tan (Arctan
20 (Tan (Arctan
21 (Tan (Arctan
22 (Tan (Arctan
(continues on next page)
28 procedure Benchm_Elapsed_Time is
29 Start_Time, Stop_Time : Time;
30 Elapsed_Time : Time_Span;
31
32 begin
33 Start_Time := Clock;
34
35 Computational_Intensive_App;
36
37 Stop_Time := Clock;
38 Elapsed_Time := Stop_Time - Start_Time;
39
46 procedure Benchm_CPU_Time is
47 Start_Time, Stop_Time : CPU_Time;
48 Elapsed_Time : Time_Span;
49
50 begin
51 Start_Time := Clock;
52
53 Computational_Intensive_App;
54
55 Stop_Time := Clock;
56 Elapsed_Time := Stop_Time - Start_Time;
57
Project: Courses.Intro_To_Ada.Standard_Library.Display_Benchmarking_Math
MD5: 06fe96bf03321c248dd1ed843648cf0b
Runtime output
TWENTYONE
In previous chapters, we've seen source-code examples using the String type, which is a
fixed-length string type — essentialy, it's an array of characters. In many cases, this data
type is good enough to deal with textual information. However, there are situations that
require more advanced text processing. Ada offers alternative approaches for these cases:
• Bounded strings: similar to fixed-length strings, bounded strings have a maximum
length, which is set at its instantiation. However, bounded strings are not arrays of
characters. At any time, they can contain a string of varied length — provided this
length is below or equal to the maximum length.
• Unbounded strings: similar to bounded strings, unbounded strings can contain strings
of varied length. However, in addition to that, they don't have a maximum length. In
this sense, they are very flexible.
The following sections present an overview of the different string types and common oper-
ations for string types.
Listing 1: show_find_substring.adb
1 with Ada.Strings.Fixed; use Ada.Strings.Fixed;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Show_Find_Substring is
5
239
Learning Ada
31 end Show_Find_Substring;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Find_Substring
MD5: faa8373bf9aec9f9f5507cf55590b0c0
Runtime output
We initialize the string S using a multiplication. Writing "Hello" & 3 * " World" creates
the string Hello World World World. We then call the function Count to get the number
of instances of the word World in S. Next we call the function Index in a loop to find the
index of each instance of World in S.
That example looked for instances of a specific substring. In the next example, we retrieve
all the words in the string. We do this using Find_Token and specifying whitespaces as
separators. For example:
Listing 2: show_find_words.adb
1 with Ada.Strings; use Ada.Strings;
2 with Ada.Strings.Fixed; use Ada.Strings.Fixed;
3 with Ada.Strings.Maps; use Ada.Strings.Maps;
4 with Ada.Text_IO; use Ada.Text_IO;
5
6 procedure Show_Find_Words is
7
29 exit when L = 0;
30
36 I := L + 1;
37 end loop;
38 end Show_Find_Words;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Find_Words
MD5: e622f489af5901e5d31f314efc3324d2
Runtime output
Operation Description
Insert Insert substring in a string
Overwrite Overwrite a string with a substring
Delete Delete a substring
Trim Remove whitespaces from a string
All these operations are available both as functions or procedures. Functions create a new
string but procedures perform the operations in place. The procedure will raise an excep-
tion if the constraints of the string are not satisfied. For example, if we have a string S
containing 10 characters, inserting a string with two characters (e.g. "!!") into it produces
a string containing 12 characters. Since it has a fixed length, we can't increase its size. One
possible solution in this case is to specify that truncation should be applied while inserting
the substring. This keeps the length of S fixed. Let's see an example that makes use of
both function and procedure versions of Insert, Overwrite, and Delete:
Listing 3: show_adapted_strings.adb
1 with Ada.Strings; use Ada.Strings;
2 with Ada.Strings.Fixed; use Ada.Strings.Fixed;
3 with Ada.Text_IO; use Ada.Text_IO;
4
5 procedure Show_Adapted_Strings is
6
11 procedure Display_Adapted_String
12 (Source : String;
13 Before : Positive;
14 New_Item : String;
15 Pattern : String)
16 is
17 S_Ins_In : String := Source;
18 S_Ovr_In : String := Source;
19 S_Del_In : String := Source;
20
21 S_Ins : String :=
22 Insert (Source,
23 Before,
24 New_Item & " ");
25 S_Ovr : String :=
26 Overwrite (Source,
27 Before,
28 New_Item);
29 S_Del : String :=
30 Trim (Delete (Source,
31 Before,
32 Before +
33 Pattern'Length - 1),
34 Ada.Strings.Right);
35 begin
36 Insert (S_Ins_In,
37 Before,
38 New_Item,
39 Right);
40
41 Overwrite (S_Ovr_In,
42 Before,
43 New_Item,
44 Right);
45
46 Delete (S_Del_In,
47 Before,
48 Before + Pattern'Length - 1);
49
68 Idx : Natural;
69 begin
70 Idx := Index
71 (Source => S,
72 Pattern => P);
73
Project: Courses.Intro_To_Ada.Standard_Library.Show_Adapted_Strings
MD5: b31b6bc94d8bdbec717c6b6b2534beb6
Runtime output
In this example, we look for the index of the substring World and perform operations on
this substring within the outer string. The procedure Display_Adapted_String uses both
versions of the operations. For the procedural version of Insert and Overwrite, we apply
truncation to the right side of the string (Right). For the Delete procedure, we specify
the range of the substring, which is replaced by whitespaces. For the function version of
Delete, we also call Trim which trims the trailing whitespace.
Using fixed-length strings is usually good enough for strings that are initialized when they
are declared. However, as seen in the previous section, procedural operations on strings
cause difficulties when done on fixed-length strings because fixed-length strings are arrays
of characters. The following example shows how cumbersome the initialization of fixed-
length strings can be when it's not performed in the declaration:
Listing 4: show_char_array.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Char_Array is
4 S : String (1 .. 15);
5 -- Strings are arrays of Character
6 begin
7 S := "Hello ";
(continues on next page)
Runtime output
String: Hello
String Length: 15
In this case, we can't simply write S := "Hello" because the resulting array of characters
for the Hello constant has a different length than the S string. Therefore, we need to
include trailing whitespaces to match the length of S. As shown in the example, we could
use an exact range for the initialization ( S (1 .. 5)) or use an explicit array of individual
characters.
When strings are initialized or manipulated at run-time, it's usually better to use bounded
or unbounded strings. An important feature of these types is that they aren't arrays, so the
difficulties presented above don't apply. Let's start with bounded strings.
Listing 5: show_bounded_string.adb
1 with Ada.Strings; use Ada.Strings;
2 with Ada.Strings.Bounded;
3 with Ada.Text_IO; use Ada.Text_IO;
4
5 procedure Show_Bounded_String is
6 package B_Str is new
7 Ada.Strings.Bounded.Generic_Bounded_Length
8 (Max => 15);
9 use B_Str;
10
13 procedure Display_String_Info
14 (S : Bounded_String)
15 is
16 begin
17 Put_Line ("String: " & To_String (S));
18 Put_Line ("String Length: "
19 & Integer'Image (Length (S)));
20 -- String:
21 -- S'Length => ok
22 -- Bounded_String:
23 -- S'Length => compilation error:
24 -- bounded strings are
25 -- not arrays!
26
31 begin
32 S1 := To_Bounded_String ("Hello");
33 Display_String_Info (S1);
34
38 S1 := To_Bounded_String
39 ("Something longer to say here...",
40 Right);
41 Display_String_Info (S1);
42 end Show_Bounded_String;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Bounded_String
MD5: a51fdeacfd43923145ee92bf5c72ecd6
Runtime output
String: Hello
String Length: 5
Max. Length: 15
String: Hello World
String Length: 11
Max. Length: 15
String: Something longe
String Length: 15
Max. Length: 15
By using bounded strings, we can easily assign to S1 and S2 multiple times during execu-
tion. We use the To_Bounded_String and To_String functions to convert, in the respective
direction, between fixed-length and bounded strings. A call to To_Bounded_String raises
an exception if the length of the input string is greater than the maximum capacity of the
bounded string. To avoid this, we can use the truncation parameter (Right in our example).
Bounded strings are not arrays, so we can't use the 'Length attribute as we did for fixed-
length strings. Instead, we call the Length function, which returns the length of the bounded
string. The Max_Length constant represents the maximum length of the bounded string that
we set when we instantiated the package.
After initializing a bounded string, we can manipulate it. For example, we can append
a string to a bounded string using Append or concatenate bounded strings using the &
operator. Like so:
Listing 6: show_bounded_string_op.adb
1 with Ada.Strings; use Ada.Strings;
2 with Ada.Strings.Bounded;
3 with Ada.Text_IO; use Ada.Text_IO;
4
5 procedure Show_Bounded_String_Op is
6 package B_Str is new
7 Ada.Strings.Bounded.Generic_Bounded_Length
8 (Max => 30);
9 use B_Str;
10
11 S1, S2 : Bounded_String;
12 begin
13 S1 := To_Bounded_String ("Hello");
14 -- Alternatively:
15 --
16 -- A := Null_Bounded_String & "Hello";
17
24 S2 := To_Bounded_String ("Hello!");
25 S1 := S1 & " " & S2;
26 Put_Line ("String: " & To_String (S1));
27 end Show_Bounded_String_Op;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Bounded_String_Op
MD5: c7c6a840c314a9cd9f75aac082a63159
Runtime output
We can initialize a bounded string with an empty string using the Null_Bounded_String
constant. Also, we can use the Append procedure and specify the truncation mode like we
do with the To_Bounded_String function.
Initialization of unbounded strings is very similar to bounded strings. Let's look at an ex-
ample:
Listing 7: show_unbounded_string.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Strings; use Ada.Strings;
3
4 with Ada.Strings.Unbounded;
5 use Ada.Strings.Unbounded;
6
7 procedure Show_Unbounded_String is
8 S1, S2 : Unbounded_String;
9
10 procedure Display_String_Info
11 (S : Unbounded_String)
12 is
13 begin
14 Put_Line ("String: " & To_String (S));
15 Put_Line ("String Length: "
16 & Integer'Image (Length (S)));
17 end Display_String_Info;
18 begin
19 S1 := To_Unbounded_String ("Hello");
20 -- Alternatively:
21 --
22 -- A := Null_Unbounded_String & "Hello";
23
24 Display_String_Info (S1);
25
29 S1 := To_Unbounded_String
30 ("Something longer to say here...");
31 Display_String_Info (S1);
32 end Show_Unbounded_String;
Runtime output
String: Hello
String Length: 5
String: Hello World
String Length: 11
String: Something longer to say here...
String Length: 31
Like bounded strings, we can assign to S1 and S2 multiple times during execution and use
the To_Unbounded_String and To_String functions to convert back-and-forth between
fixed-length strings and unbounded strings. However, in this case, truncation is not needed.
And, just like for bounded strings, you can use the Append procedure and the & operator for
unbounded strings. For example:
Listing 8: show_unbounded_string_op.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
(continues on next page)
6 procedure Show_Unbounded_String_Op is
7 S1, S2 : Unbounded_String :=
8 Null_Unbounded_String;
9 begin
10 S1 := S1 & "Hello";
11 S2 := S2 & "Hello!";
12
Project: Courses.Intro_To_Ada.Standard_Library.Show_Unbounded_String_Op
MD5: 806e24a6dd0bc87e76f73a22e42ba390
Runtime output
In this example, we're concatenating the unbounded S1 and S2 strings with the "Hello"
and "Hello!" strings, respectively. Also, we're using the Append procedure, just like we did
with bounded strings.
TWENTYTWO
In most parts of this course, we used the Put_Line procedure to display information on the
console. However, this procedure also accepts a File_Type parameter. For example, you
can select between standard output and standard error by setting this parameter explicitly:
Listing 1: show_std_text_out.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Std_Text_Out is
4 begin
5 Put_Line (Standard_Output, "Hello World #1");
6 Put_Line (Standard_Error, "Hello World #2");
7 end Show_Std_Text_Out;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Std_Text_Out
MD5: 4d75bd2906226897244e3d2a611c9725
249
Learning Ada
Runtime output
Hello World #1
Hello World #2
You can also use this parameter to write information to any text file. To create a new file for
writing, use the Create procedure, which initializes a File_Type element that you can later
pass to Put_Line (instead of, e.g., Standard_Output). After you finish writing information,
you can close the file by calling the Close procedure.
You use a similar method to read information from a text file. However, when opening the
file, you must specify that it's an input file (In_File) instead of an output file. Also, instead
of calling the Put_Line procedure, you call the Get_Line function to read information from
the file.
Let's see an example that writes information into a new text file and then reads it back from
the same file:
Listing 2: show_simple_text_file_io.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Simple_Text_File_IO is
4 F : File_Type;
5 File_Name : constant String := "simple.txt";
6 begin
7 Create (F, Out_File, File_Name);
8 Put_Line (F, "Hello World #1");
9 Put_Line (F, "Hello World #2");
10 Put_Line (F, "Hello World #3");
11 Close (F);
12
Project: Courses.Intro_To_Ada.Standard_Library.Show_Simple_Text_File_IO
MD5: 7461e946eef18c93219fa4ce3afbb1ea
Runtime output
Hello World #1
Hello World #2
Hello World #3
In addition to the Create and Close procedures, the standard library also includes a Reset
procedure, which, as the name implies, resets (erases) all the information from the file. For
example:
Listing 3: show_text_file_reset.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Text_File_Reset is
4 F : File_Type;
5 File_Name : constant String := "simple.txt";
6 begin
(continues on next page)
Project: Courses.Intro_To_Ada.Standard_Library.Show_Text_File_Reset
MD5: 5e5498f03b2c829513af062c5959fc93
Runtime output
Hello World #2
By running this program, we notice that, although we've written the first string ("Hello
World #1") to the file, it has been erased because of the call to Reset.
In addition to opening a file for reading or writing, you can also open an existing file and
append to it. Do this by calling the Open procedure with the Append_File option.
When calling the Open procedure, an exception is raised if the specified file isn't found.
Therefore, you should handle exceptions in that context. The following example deletes a
file and then tries to open the same file for reading:
Listing 4: show_text_file_input_except.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Text_File_Input_Except is
4 F : File_Type;
5 File_Name : constant String := "simple.txt";
6 begin
7 -- Open output file and delete it
8 Create (F, Out_File, File_Name);
9 Delete (F);
10
Project: Courses.Intro_To_Ada.Standard_Library.Show_Text_File_Input_Except
MD5: c8d257091831c48d10b6e70e34b4261b
Runtime output
In this example, we create the file by calling Create and then delete it by calling Delete.
After the call to Delete, we can no longer use the File_Type element. After deleting the
file, we try to open the non-existent file, which raises a Name_Error exception.
The previous section presented details about text file I/O. Here, we discuss doing file I/O in
binary format. The first package we'll explore is the Ada.Sequential_IO package. Because
this package is a generic package, you need to instantiate it for the data type you want to
use for file I/O. Once you've done that, you can use the same procedures we've seen in the
previous section: Create, Open, Close, Reset and Delete. However, instead of calling the
Get_Line and Put_Line procedures, you'd call the Read and Write procedures.
In the following example, we instantiate the Ada.Sequential_IO package for floating-point
types:
Listing 5: show_seq_float_io.adb
1 with Ada.Text_IO;
2 with Ada.Sequential_IO;
3
4 procedure Show_Seq_Float_IO is
5 package Float_IO is
6 new Ada.Sequential_IO (Float);
7 use Float_IO;
8
9 F : Float_IO.File_Type;
10 File_Name : constant String :=
11 "float_file.bin";
12 begin
13 Create (F, Out_File, File_Name);
14 Write (F, 1.5);
15 Write (F, 2.4);
16 Write (F, 6.7);
17 Close (F);
18
19 declare
20 Value : Float;
21 begin
22 Open (F, In_File, File_Name);
23 while not End_Of_File (F) loop
24 Read (F, Value);
25 Ada.Text_IO.Put_Line
26 (Float'Image (Value));
27 end loop;
28 Close (F);
29 end;
30 end Show_Seq_Float_IO;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Seq_Float_IO
MD5: 27aa5daf92cba5df23fdc55c3578aa34
Runtime output
1.50000E+00
2.40000E+00
6.70000E+00
We use the same approach to read and write complex information. The following example
uses a record that includes a Boolean and a floating-point value:
Listing 6: show_seq_rec_io.adb
1 with Ada.Text_IO;
2 with Ada.Sequential_IO;
3
4 procedure Show_Seq_Rec_IO is
5 type Num_Info is record
6 Valid : Boolean := False;
7 Value : Float;
8 end record;
9
26 F : Num_Info_IO.File_Type;
27 File_Name : constant String :=
28 "float_file.bin";
29 begin
30 Create (F, Out_File, File_Name);
31 Write (F, (True, 1.5));
32 Write (F, (False, 2.4));
33 Write (F, (True, 6.7));
34 Close (F);
35
36 declare
37 Value : Num_Info;
38 begin
39 Open (F, In_File, File_Name);
40 while not End_Of_File (F) loop
41 Read (F, Value);
42 Put_Line (Value);
43 end loop;
44 Close (F);
45 end;
46 end Show_Seq_Rec_IO;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Seq_Rec_IO
MD5: a88b1428cc50745dce0509087e74adb7
Runtime output
(ok, 1.50000E+00)
(not ok, -----------)
(ok, 6.70000E+00)
As the example shows, we can use the same approach we used for floating-point types to
perform file I/O for this record. Once we instantiate the Ada.Sequential_IO package for
the record type, file I/O operations are performed the same way.
Direct I/O is available in the Ada.Direct_IO package. This mechanism is similar to the
sequential I/O approach just presented, but allows us to access any position in the file. The
package instantiation and most operations are very similar to sequential I/O. To rewrite
the Show_Seq_Float_IO application presented in the previous section to use the Ada.
Direct_IO package, we just need to replace the instances of the Ada.Sequential_IO pack-
age by the Ada.Direct_IO package. This is the new source code:
Listing 7: show_dir_float_io.adb
1 with Ada.Text_IO;
2 with Ada.Direct_IO;
3
4 procedure Show_Dir_Float_IO is
5 package Float_IO is new Ada.Direct_IO (Float);
6 use Float_IO;
7
8 F : Float_IO.File_Type;
9 File_Name : constant String :=
10 "float_file.bin";
11 begin
12 Create (F, Out_File, File_Name);
13 Write (F, 1.5);
14 Write (F, 2.4);
15 Write (F, 6.7);
16 Close (F);
17
18 declare
19 Value : Float;
20 begin
21 Open (F, In_File, File_Name);
22 while not End_Of_File (F) loop
23 Read (F, Value);
24 Ada.Text_IO.Put_Line
25 (Float'Image (Value));
26 end loop;
27 Close (F);
28 end;
29 end Show_Dir_Float_IO;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Dir_Float_IO
MD5: e4e5855976de44f53a821eb90dcbb206
Runtime output
1.50000E+00
2.40000E+00
6.70000E+00
Unlike sequential I/O, direct I/O allows you to access any position in the file. However, it
doesn't offer an option to append information to a file. Instead, it provides an Inout_File
mode allowing reading and writing to a file via the same File_Type element.
To access any position in the file, call the Set_Index procedure to set the new position /
index. You can use the Index function to retrieve the current index. Let's see an example:
Listing 8: show_dir_float_in_out_file.adb
1 with Ada.Text_IO;
2 with Ada.Direct_IO;
3
4 procedure Show_Dir_Float_In_Out_File is
5 package Float_IO is new Ada.Direct_IO (Float);
6 use Float_IO;
7
8 F : Float_IO.File_Type;
9 File_Name : constant String :=
10 "float_file.bin";
11 begin
12 -- Open file for input / output
13 Create (F, Inout_File, File_Name);
14 Write (F, 1.5);
15 Write (F, 2.4);
16 Write (F, 6.7);
17
23 declare
24 Value : Float;
25 begin
26 -- Set index to start of file
27 Set_Index (F, 1);
28
Project: Courses.Intro_To_Ada.Standard_Library.Show_Dir_Float_In_Out_File
MD5: 17b83a16ab8fa30f07cf8a0bd54078a1
Runtime output
1.50000E+00
2.40000E+00
7.70000E+00
By running this example, we see that the file contains 7.7, rather than the previous 6.7 that
we wrote. We overwrote the value by changing the index to the previous position before
doing another write.
In this example we used the Inout_File mode. Using that mode, we just changed the
index back to the initial position before reading from the file (Set_Index (F, 1)) instead
All the previous approaches for file I/O in binary format (sequential and direct I/O) are spe-
cific for a single data type (the one we instantiate them with). You can use these approaches
to write objects of a single data type that may be an array or record (potentially with many
fields), but if you need to create and process files that include different data types, or any
objects of an unbounded type, these approaches are not sufficient. Instead, you should use
stream I/O.
Stream I/O shares some similarities with the previous approaches. We still use the Create,
Open and Close procedures. However, instead of accessing the file directly via a File_Type
element, you use a Stream_Access element. To read and write information, you use the
'Read or 'Write attributes of the data types you're reading or writing.
Let's look at a version of the Show_Dir_Float_IO procedure from the previous section that
makes use of stream I/O instead of direct I/O:
Listing 9: show_float_stream.adb
1 with Ada.Text_IO;
2
3 with Ada.Streams.Stream_IO;
4 use Ada.Streams.Stream_IO;
5
6 procedure Show_Float_Stream is
7 F : File_Type;
8 S : Stream_Access;
9 File_Name : constant String :=
10 "float_file.bin";
11 begin
12 Create (F, Out_File, File_Name);
13 S := Stream (F);
14
19 Close (F);
20
21 declare
22 Value : Float;
23 begin
24 Open (F, In_File, File_Name);
25 S := Stream (F);
26
Project: Courses.Intro_To_Ada.Standard_Library.Show_Float_Stream
MD5: 34ccf04b0821074a332019ac0e38bb3e
Runtime output
1.50000E+00
2.40000E+00
6.70000E+00
After the call to Create, we retrieve the corresponding Stream_Access element by calling
the Stream function. We then use this stream to write information to the file via the 'Write
attribute of the Float type. After closing the file and reopening it for reading, we again
retrieve the corresponding Stream_Access element and processed to read information from
the file via the 'Read attribute of the Float type.
You can use streams to create and process files containing different data types within the
same file. You can also read and write unbounded data types such as strings. However,
when using unbounded data types you must call the 'Input and 'Output attributes of the
unbounded data type: these attributes write information about bounds or discriminants in
addition to the object's actual data.
The following example shows file I/O that mixes both strings of different lengths and floating-
point values:
3 with Ada.Streams.Stream_IO;
4 use Ada.Streams.Stream_IO;
5
6 procedure Show_String_Stream is
7 F : File_Type;
8 S : Stream_Access;
9 File_Name : constant String :=
10 "float_file.bin";
11
28 begin
29 Create (F, Out_File, File_Name);
30 S := Stream (F);
31
36 Close (F);
37
46 end Show_String_Stream;
Project: Courses.Intro_To_Ada.Standard_Library.Show_String_Stream
MD5: 3ae8276ada5f24cab49994e368e0fa34
Runtime output
When you use Stream I/O, no information is written into the file indicating the type of the
data that you wrote. If a file contains data from different types, you must reference types
in the same order when reading a file as when you wrote it. If not, the information you get
will be corrupted. Unfortunately, strong data typing doesn't help you in this case. Writing
simple procedures for file I/O (as in the example above) may help ensuring that the file
format is consistent.
Like direct I/O, stream I/O support also allows you to access any location in the file. However,
when doing so, you need to be extremely careful that the position of the new index is
consistent with the data types you're expecting.
TWENTYTHREE
The standard library provides support for common numeric operations on floating-point
types as well as on complex types and matrices. In the sections below, we present a brief
introduction to these numeric operations.
Listing 1: show_elem_math.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics; use Ada.Numerics;
3
4 with Ada.Numerics.Elementary_Functions;
5 use Ada.Numerics.Elementary_Functions;
6
7 procedure Show_Elem_Math is
8 X : Float;
9 begin
10 X := 2.0;
11 Put_Line ("Square root of "
12 & Float'Image (X)
13 & " is "
14 & Float'Image (Sqrt (X)));
15
16 X := e;
17 Put_Line ("Natural log of "
18 & Float'Image (X)
19 & " is "
20 & Float'Image (Log (X)));
21
22 X := 10.0 ** 6.0;
23 Put_Line ("Log_10 of "
24 & Float'Image (X)
25 & " is "
26 & Float'Image (Log (X, 10.0)));
27
28 X := 2.0 ** 8.0;
29 Put_Line ("Log_2 of "
30 & Float'Image (X)
31 & " is "
32 & Float'Image (Log (X, 2.0)));
33
259
Learning Ada
40 X := -1.0;
41 Put_Line ("Arccos of "
42 & Float'Image (X)
43 & " is "
44 & Float'Image (Arccos (X)));
45 end Show_Elem_Math;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Elem_Math
MD5: 17511d7e17cd98d4b6e49ad302d6dcb6
Runtime output
Here we use the standard e and Pi constants from the Ada.Numerics package.
The Ada.Numerics.Elementary_Functions package provides operations for the Float
type. Similar packages are available for Long_Float and Long_Long_Float types.
For example, the Ada.Numerics.Long_Elementary_Functions package offers the
same set of operations for the Long_Float type. In addition, the Ada.Numerics.
Generic_Elementary_Functions package is a generic version of the package that you
can instantiate for custom floating-point types. In fact, the Elementary_Functions pack-
age can be defined as follows:
Listing 2: show_float_random_num.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 with Ada.Numerics.Float_Random;
4 use Ada.Numerics.Float_Random;
5
6 procedure Show_Float_Random_Num is
7 G : Generator;
8 X : Uniformly_Distributed;
9 begin
(continues on next page)
Runtime output
Some random numbers between 0.00000E+00 and 1.00000E+00:
9.74383E-01
3.73925E-01
7.63879E-01
2.70624E-01
5.32957E-02
1.24468E-01
4.45997E-01
9.39235E-01
3.75469E-01
7.85886E-01
4.37336E-01
4.67083E-01
9.41378E-01
1.42369E-01
5.85918E-01
The standard library also includes a random number generator for discrete numbers, which
is part of the Ada.Numerics.Discrete_Random package. Since it's a generic package, you
have to instantiate it for the desired discrete type. This allows you to specify a range for
the generator. In the following example, we create an application that displays random
integers between 1 and 10:
Listing 3: show_discrete_random_num.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics.Discrete_Random;
3
4 procedure Show_Discrete_Random_Num is
5
8 package R is new
9 Ada.Numerics.Discrete_Random (Random_Range);
10 use R;
11
12 G : Generator;
13 X : Random_Range;
14 begin
(continues on next page)
23 for I in 1 .. 15 loop
24 X := Random (G);
25 Put_Line (Integer'Image (X));
26 end loop;
27 end Show_Discrete_Random_Num;
Project: Courses.Intro_To_Ada.Standard_Library.Show_Discrete_Random_Num
MD5: 892f6525477f9a2c56f88885de011fba
Runtime output
Here, package R is instantiated with the Random_Range type, which has a constrained range
between 1 and 10. This allows us to control the range used for the random numbers.
We could easily modify the application to display random integers between 0 and 20 by
changing the specification of the Random_Range type. We can also use floating-point or
fixed-point types.
Listing 4: show_elem_math.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics; use Ada.Numerics;
(continues on next page)
4 with Ada.Numerics.Complex_Types;
5 use Ada.Numerics.Complex_Types;
6
7 with Ada.Numerics.Complex_Elementary_Functions;
8 use Ada.Numerics.Complex_Elementary_Functions;
9
10 with Ada.Text_IO.Complex_IO;
11
12 procedure Show_Elem_Math is
13
18 X, Y : Complex;
19 R, Th : Float;
20 begin
21 X := (2.0, -1.0);
22 Y := (3.0, 4.0);
23
24 Put (X);
25 Put (" * ");
26 Put (Y);
27 Put (" is ");
28 Put (X * Y);
29 New_Line;
30 New_Line;
31
32 R := 3.0;
33 Th := Pi / 2.0;
34 X := Compose_From_Polar (R, Th);
35 -- Alternatively:
36 -- X := R * Exp ((0.0, Th));
37 -- X := R * e ** Complex'(0.0, Th);
38
Project: Courses.Intro_To_Ada.Standard_Library.Show_Elem_Math
MD5: 24fd48ab69aeac28286e6ec8065899c5
Runtime output
As we can see from this example, all the common operators, such as * and +, are available
for complex types. You also have typical operations on complex numbers, such as Argument
and Exp. In addition to initializing complex numbers in the cartesian form using aggregates,
you can do so from the polar form by calling the Compose_From_Polar function.
The Ada.Numerics.Complex_Types and Ada.Numerics.Complex_Elementary_Functions
packages provide operations for the Float type. Similar packages are avail-
able for Long_Float and Long_Long_Float types. In addition, the Ada.Numerics.
Generic_Complex_Types and Ada.Numerics.Generic_Complex_Elementary_Functions
packages are generic versions that you can instantiate for custom or pre-defined floating-
point types. For example:
with Ada.Numerics.Generic_Complex_Types;
with Ada.Numerics.Generic_Complex_Elementary_Functions;
with Ada.Text_IO.Complex_IO;
procedure Show_Elem_Math is
X, Y : Complex;
R, Th : Float;
Listing 5: show_matrix.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 with Ada.Numerics.Real_Arrays;
4 use Ada.Numerics.Real_Arrays;
5
6 procedure Show_Matrix is
7
31 M1 : Real_Matrix :=
32 ((1.0, 5.0, 1.0),
33 (2.0, 2.0, 1.0));
34 M2 : Real_Matrix :=
35 ((31.0, 11.0, 10.0),
36 (34.0, 16.0, 11.0),
37 (32.0, 12.0, 10.0),
38 (31.0, 13.0, 10.0));
39 M3 : Real_Matrix := ((1.0, 2.0),
40 (2.0, 3.0));
41 begin
42 Put_Line ("V1");
43 Put_Vector (V1);
44 Put_Line ("V2");
45 Put_Vector (V2);
46 Put_Line ("V1 * V2 =");
47 Put_Line (" "
48 & Float'Image (V1 * V2));
49 Put_Line ("V1 * V2 =");
50 Put_Matrix (V1 * V2);
51 New_Line;
52
53 Put_Line ("M1");
54 Put_Matrix (M1);
55 Put_Line ("M2");
56 Put_Matrix (M2);
57 Put_Line ("M2 * Transpose(M1) =");
58 Put_Matrix (M2 * Transpose (M1));
59 New_Line;
60
Project: Courses.Intro_To_Ada.Standard_Library.Show_Matrix
MD5: c9df45a742a42bd47e03fbf2d0282238
Runtime output
V1
( 1.00000E+00 3.00000E+00 )
V2
( 7.50000E+01 1.10000E+01 )
V1 * V2 =
1.08000E+02
V1 * V2 =
( 7.50000E+01 1.10000E+01 )
( 2.25000E+02 3.30000E+01 )
M1
( 1.00000E+00 5.00000E+00 1.00000E+00 )
( 2.00000E+00 2.00000E+00 1.00000E+00 )
M2
( 3.10000E+01 1.10000E+01 1.00000E+01 )
( 3.40000E+01 1.60000E+01 1.10000E+01 )
( 3.20000E+01 1.20000E+01 1.00000E+01 )
( 3.10000E+01 1.30000E+01 1.00000E+01 )
M2 * Transpose(M1) =
( 9.60000E+01 9.40000E+01 )
( 1.25000E+02 1.11000E+02 )
( 1.02000E+02 9.80000E+01 )
( 1.06000E+02 9.80000E+01 )
M3
( 1.00000E+00 2.00000E+00 )
( 2.00000E+00 3.00000E+00 )
Inverse (M3) =
(-3.00000E+00 2.00000E+00 )
( 2.00000E+00 -1.00000E+00 )
abs Inverse (M3) =
( 3.00000E+00 2.00000E+00 )
( 2.00000E+00 1.00000E+00 )
Determinant (M3) =
-1.00000E+00
Solve (M3, V1) =
( 3.00000E+00 -1.00000E+00 )
Eigenvalues (M3) =
(continues on next page)
Matrix dimensions are automatically determined from the aggregate used for initialization
when you don't specify them. You can, however, also use explicit ranges. For example:
M1 : Real_Matrix (1 .. 2, 1 .. 3) :=
((1.0, 5.0, 1.0),
(2.0, 2.0, 1.0));
The Ada.Numerics.Real_Arrays package implements operations for the Float type. Sim-
ilar packages are available for Long_Float and Long_Long_Float types. In addition, the
Ada.Numerics.Generic_Real_Arrays package is a generic version that you can instantiate
with custom floating-point types. For example, the Real_Arrays package can be defined
as follows:
TWENTYFOUR
APPENDICES
The following tables contain examples of available formal types for generics:
269
Learning Ada
Many of the examples above can be used for formal indefinite types:
The same examples could also contain discriminants. In this case, (<>) is replaced by a
list of discriminants, e.g.: (D: DT).
The following table shows all containers available in Ada, including their versions (standard,
bounded, unbounded, indefinite):
Note: To get the correct container name, replace the whitespace by _ in the names above.
(For example, Hashed Maps becomes Hashed_Maps.)
The following table presents the prefixing applied to the container name that depends on
its version. As indicated in the table, the standard version does not have a prefix associated
with it.
273
Learning Ada
Note: The code examples in this course use a 50-column limit, which greatly improves
the readability of the code on devices with a small screen size. This constraint, however,
leads to an unusual coding style. For instance, instead of calling Put_Line in a single line,
we have this:
Put_Line
(" is in the northeast quadrant");
or this:
Note that typical Ada code uses a limit of at least 79 columns. Therefore, please don't take
the coding style from this course as a reference!
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn website22 .
The directory structure in the zip file is based on the code block metadata. For example, if
you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
21 http://creativecommons.org/licenses/by-sa/4.0
22 https://learn.adacore.com/zip/learning-ada_code.zip
275
Learning Ada
This course will teach you advanced topics of the Ada programming language. The Intro-
duction to Ada (page 5) course is a prerequisite for this course.
This document was written by Gustavo A. Hoffmann, with major contributions from Robert
A. Duff. The document also includes contributions from Franco Gasperoni, Gary Dismukes,
Patrick Rogers, and Robert Dewar.
These contributions are clearly indicated in the document, together with the original publi-
cation source.
Special thanks to Patrick Rogers for all comments and suggestions. In particular, thanks for
sharing the training slides on access types: many ideas from those slides were integrated
into this course.
This document was reviewed by Patrick Rogers and Tucker Taft.
CHANGELOG
Changes are being tracked on the CHANGELOG page.
276
CHAPTER
TWENTYFIVE
DATA TYPES
25.1 Types
In general terms, scalar types are the most basic types that we can get. As we know, we
can classify them as follows:
Many attributes exist for scalar types. For example, we can use the Image and Value at-
tributes to convert between a given type and a string type. The following table presents
the main attributes for scalar types:
We already discussed some of these attributes in the Introduction to Ada course (in the
sections about range and related attributes (page 76) and image attribute (page 13)). In
this section, we'll discuss some aspects that have been left out of the previous course.
23 http://www.ada-auth.org/standards/22rm/html/RM-3-5.html
277
Learning Ada
Ranges
We've seen that the First and Last attributes can be used with discrete types. Those
attributes are also available for real types. Here's an example using the Float type and a
subtype of it:
Listing 1: show_first_last_real.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_First_Last_Real is
4 subtype Norm is Float range 0.0 .. 1.0;
5 begin
6 Put_Line ("Float'First: " & Float'First'Image);
7 Put_Line ("Float'Last: " & Float'Last'Image);
8 Put_Line ("Norm'First: " & Norm'First'Image);
9 Put_Line ("Norm'Last: " & Norm'Last'Image);
10 end Show_First_Last_Real;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Ranges_Real_Types
MD5: 89745a94fbdc41a2880ba14e50401acb
Runtime output
Float'First: -3.40282E+38
Float'Last: 3.40282E+38
Norm'First: 0.00000E+00
Norm'Last: 1.00000E+00
This program displays the first and last values of both the Float type and the Norm subtype.
In the case of the Float type, we see the full range, while for the Norm subtype, we get the
values we used in the declaration of the subtype (i.e. 0.0 and 1.0).
We can use the Pred and Succ attributes to get the predecessor and successor of a specific
value. For discrete types, this is simply the next discrete value. For example, Pred (2) is
1 and Succ (2) is 3. Let's look at a complete source-code example:
Listing 2: show_succ_pred_discrete.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Succ_Pred_Discrete is
4 type State is (Idle, Started,
5 Processing, Stopped);
6
9 I : constant Integer := 2;
10 begin
11 Put_Line ("State : "
12 & Machine_State'Image);
13 Put_Line ("State'Pred (Machine_State): "
14 & State'Pred (Machine_State)'Image);
15 Put_Line ("State'Succ (Machine_State): "
16 & State'Succ (Machine_State)'Image);
17 Put_Line ("----------");
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Show_Succ_Pred_Discrete
MD5: e11d0f50105864fdc1594b3bb72d927e
Runtime output
State : STARTED
State'Pred (Machine_State): IDLE
State'Succ (Machine_State): PROCESSING
----------
I : 2
Integer'Pred (I): 1
Integer'Succ (I): 3
In this example, we use the Pred and Succ attributes for a variable of enumeration type
(State) and a variable of Integer type.
We can also use the Pred and Succ attributes with real types. In this case, however, the
value we get depends on the actual type we're using:
• for fixed-point types, the value is calculated using the smallest value (Small), which
is derived from the declaration of the fixed-point type;
• for floating-point types, the value used in the calculation depends on representation
constraints of the actual target machine.
Let's look at this example with a decimal type (Decimal) and a floating-point type
(My_Float):
Listing 3: show_succ_pred_real.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Succ_Pred_Real is
4 subtype My_Float is
5 Float range 0.0 .. 0.5;
6
7 type Decimal is
8 delta 0.1 digits 2
9 range 0.0 .. 0.5;
10
11 D : Decimal;
12 N : My_Float;
13 begin
14 Put_Line ("---- DECIMAL -----");
15 Put_Line ("Small: " & Decimal'Small'Image);
16 Put_Line ("----- Succ -------");
17 D := Decimal'First;
18 loop
19 Put_Line (D'Image);
20 D := Decimal'Succ (D);
21
26 D := Decimal'Last;
27 loop
28 Put_Line (D'Image);
29 D := Decimal'Pred (D);
30
44 for I in 1 .. 5 loop
45 Put_Line (N'Image);
46 N := My_Float'Pred (N);
47 end loop;
48 end Show_Succ_Pred_Real;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Show_Succ_Pred_Real
MD5: f426d6539c3ce863101f1e6afb21c08f
Runtime output
As the output of the program indicates, the smallest value (see Decimal'Small in the ex-
ample) is used to calculate the previous and next values of Decimal type.
In the case of the My_Float type, the difference between the current and the previous or
next values is 1.40130E-45 (or 2-149 ) on a standard PC.
We've seen that we can use the Image and Value attributes to perform conversions between
values of a given subtype and a string:
Listing 4: show_image_value_attr.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Image_Value_Attr is
4 I : constant Integer := Integer'Value ("42");
5 begin
6 Put_Line (I'Image);
7 end Show_Image_Value_Attr;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Image_Value_Attr
MD5: 9daa13b1f05511fac7e108eb9b8eefa7
Runtime output
42
The Image and Value attributes are used for the String type specifically. In addition
to them, there are also attributes for different string types — namely Wide_String and
Wide_Wide_String. This is the complete list of available attributes:
We discuss more about Wide_String and Wide_Wide_String in another section (page 495).
Width attribute
When converting a value to a string by using the Image attribute, we get a string with
variable width. We can assess the maximum width of that string for a specific subtype
by using the Width attribute. For example, Integer'Width gives us the maximum width
returned by the Image attribute when converting a value of Integer type to a string of
String type.
This attribute is useful when we're using bounded strings in our code to store the string
returned by the Image attribute. For example:
Listing 5: show_width_attr.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Strings; use Ada.Strings;
3 with Ada.Strings.Bounded;
4
5 procedure Show_Width_Attr is
6 package B_Str is new
7 Ada.Strings.Bounded.Generic_Bounded_Length
8 (Max => Integer'Width);
9 use B_Str;
10
11 Str_I : Bounded_String;
12
Runtime output
Value: 42
String Length: 3
----
Value: 103
String Length: 4
In this example, we're storing the string returned by Image in the Str_I variable of
Bounded_String type.
Similar to the Image and Value attributes, the Width attribute is also available for string
types other than String. In fact, we can use:
• the Wide_Width attribute for strings returned by Wide_Image; and
• the Wide_Wide_Width attribute for strings returned by Wide_Wide_Image.
Base
The Base attribute gives us the unconstrained underlying hardware representation selected
for a given numeric type. As an example, let's say we declared a subtype of the Integer
type named One_To_Ten:
Listing 6: my_integers.ads
1 package My_Integers is
2
6 end My_Integers;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Base_Attr
MD5: e3f8310ed742e61a65728fecb6caa557
If we then use the Base attribute — by writing One_To_Ten'Base —, we're actually refer-
ring to the unconstrained underlying hardware representation selected for One_To_Ten. As
One_To_Ten is a subtype of the Integer type, this also means that One_To_Ten'Base is
equivalent to Integer'Base, i.e. they refer to the same base type. (This base type is the
underlying hardware type representing the Integer type — but is not the Integer type
itself.)
type Integer is
range -2 ** 31 .. 2 ** 31 - 1;
However, even though Integer is a predefined Ada type, it's actually a subtype of an anony-
mous type. That anonymous "type" is the hardware's representation for the numeric type
as chosen by the compiler based on the requested range (for the signed integer types) or
digits of precision (for floating-point types). In other words, these types are actually sub-
types of something that does not have a specific name in Ada, and that is not constrained.
In effect,
type Integer is
range -2 ** 31 .. 2 ** 31 - 1;
subtype Integer is
Some_Hardware_Type_With_Sufficient_Range
range -2 ** 31 .. 2 ** 31 - 1;
range requested, the compilation is rejected. For example, in current architectures, the
code below most likely won't compile:
Listing 7: int_def.ads
1 package Int_Def is
2
3 type Too_Big_To_Fail is
4 range -2 ** 255 .. 2 ** 255 - 1;
5
6 end Int_Def;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Very_Big_Range
MD5: 29f54776dc814dc8a5d245105b527992
Build output
Otherwise, the compiler maps the named Ada type to the hardware "type", presumably
choosing the smallest one that supports the requested range. (That's why the range has
to be static in the source code, unlike for explicit subtypes.)
The following example shows how the Base attribute affects the bounds of a variable:
Listing 8: show_base.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with My_Integers; use My_Integers;
3
4 procedure Show_Base is
5 C : constant One_To_Ten := One_To_Ten'Last;
6 begin
7 Using_Constrained_Subtype : declare
8 V : One_To_Ten := C;
9 begin
10 Put_Line
11 ("Increasing value for One_To_Ten...");
12
13 V := One_To_Ten'Succ (V);
14 exception
15 when others =>
16 Put_Line ("Exception raised!");
17 end Using_Constrained_Subtype;
18
19 Using_Base : declare
20 V : One_To_Ten'Base := C;
21 begin
22 Put_Line
23 ("Increasing value for One_To_Ten'Base...");
24
25 V := One_To_Ten'Succ (V);
26 exception
27 when others =>
28 Put_Line ("Exception raised!");
29 end Using_Base;
30
Build output
show_base.adb:13:22: warning: value not in range of type "One_To_Ten" defined at␣
↪my_integers.ads:3 [enabled by default]
Runtime output
Increasing value for One_To_Ten...
Exception raised!
Increasing value for One_To_Ten'Base...
One_To_Ten'Last: 10
One_To_Ten'Base'Last: 2147483647
In the first block of the example (Using_Constrained_Subtype), we're asking for the next
value after the last value of a range — in this case, One_To_Ten'Succ (One_To_Ten'Last).
As expected, since the last value of the range doesn't have a successor, a constraint ex-
ception is raised.
In the Using_Base block, we're declaring a variable V of One_To_Ten'Base subtype. In
this case, the next value exists — because the condition One_To_Ten'Last + 1 <=
One_To_Ten'Base'Last is true —, so we can use the Succ attribute without having an
exception being raised.
In the following example, we adjust the result of additions and subtractions to avoid con-
straint errors:
Listing 9: my_integers.ads
1 package My_Integers is
2
11 end My_Integers;
31 end My_Integers;
4 procedure Show_Base is
5
8 procedure Display_Saturate
9 (V1, V2 : One_To_Ten;
10 Op : Display_Saturate_Op)
11 is
12 Res : One_To_Ten;
13 begin
14 case Op is
15 when Add =>
16 Res := Sat_Add (V1, V2);
17 when Sub =>
18 Res := Sat_Sub (V1, V2);
19 end case;
20 Put_Line ("SATURATE " & Op'Image
21 & " (" & V1'Image
22 & ", " & V2'Image
23 & ") = " & Res'Image);
24 end Display_Saturate;
25
26 begin
27 Display_Saturate (1, 1, Add);
28 Display_Saturate (10, 8, Add);
29 Display_Saturate (1, 8, Sub);
30 end Show_Base;
Project: Courses.Advanced_Ada.Data_Types.Types.Scalar_Types.Base_Attr_Sat
MD5: e9b31345c2efc056bdb71824072852d0
Runtime output
SATURATE ADD ( 1, 1) = 2
SATURATE ADD ( 10, 8) = 10
SATURATE SUB ( 1, 8) = 1
In this example, we're using the Base attribute to declare the parameters of the Sat_Add,
Sat_Sub and Saturate functions. Note that the parameters of the Display_Saturate
procedure are of One_To_Ten type, while the parameters of the Sat_Add, Sat_Sub and
Saturate functions are of the (unconstrained) base subtype (One_To_Ten'Base). In those
functions, we perform operations using the parameters of unconstrained subtype and ad-
just the result — in the Saturate function — before returning it as a constrained value of
One_To_Ten subtype.
The code in the body of the My_Integers package contains lines that were commented
out — to be more precise, a call to Put_Line call in the Saturate function. If you uncom-
ment them, you'll see the value of the input parameter V (of One_To_Ten'Base type) in the
runtime output of the program before it's adapted to fit the constraints of the One_To_Ten
subtype.
25.1.2 Enumerations
We've introduced enumerations back in the Introduction to Ada course (page 51). In this
section, we'll discuss a few useful features of enumerations, such as enumeration renaming,
enumeration overloading and representation clauses.
Enumerations as functions
If you have used programming language such as C in the past, you're familiar with the
concept of enumerations being constants with integer values. In Ada, however, enumera-
tions are not integers. In fact, they're actually parameterless functions! Let's consider this
example:
18 end Days;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_As_Function
MD5: fa3e58b58edffa5a3e04b060a7f8cb8b
In the package Days, we're declaring the enumeration type Day. When we do this, we're
essentially declaring seven parameterless functions, one for each enumeration. For exam-
ple, the Mon enumeration corresponds to function Mon return Day. You can see all seven
function declarations in the comments of the example above.
Note that this has no direct relation to how an Ada compiler generates machine code for
enumeration. Even though enumerations are parameterless functions, a typical Ada com-
piler doesn't generate function calls for code that deals with enumerations.
Enumeration renaming
The idea that enumerations are parameterless functions can be used when we want to
rename enumerations. For example, we could rename the enumerations of the Day type
like this:
15 end Enumeration_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Renaming
MD5: e2e12bb3bfcb0b6e94769ced9a4b80f9
Now, we can use both Monday or Mon to refer to Monday of the Day type:
4 procedure Show_Renaming is
5 D1 : constant Day := Mon;
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Renaming
MD5: 2d7177def2c9e9fb11c7dc5e036c3be3
Runtime output
D1 = D2
MON = MON
When running this application, we can confirm that D1 is equal to D2. Also, even though
we've assigned Monday to D2 (instead of Mon), the application displays Mon = Mon, since
Monday is just another name to refer to the actual enumeration (Mon).
Hint
If you just want to have a single (renamed) enumeration visible in your application — and
make the original enumeration invisible —, you can use a separate package. For example:
7 end Enumeration_Example;
3 package Enumeration_Renaming is
4
22 end Enumeration_Renaming;
3 with Enumeration_Renaming;
4 use Enumeration_Renaming;
5
6 procedure Show_Renaming is
7 D1 : constant Day := Monday;
8 begin
9 Put_Line (Day'Image (D1));
10 end Show_Renaming;
Runtime output
MON
Note that the call to Put_Line still display Mon instead of Monday.
Enumeration overloading
Enumerations can be overloaded. In simple terms, this means that the same name can be
used to declare an enumeration of different types. A typical example is the declaration of
colors:
3 type Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
16 type Primary_Color is
17 (Red,
18 Green,
19 Blue);
20
21 end Colors;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Overloading
MD5: b808f90d9164f044b6b7a8931863726f
Note that we have Red as an enumeration of type Color and of type Primary_Color. The
same applies to Green and Blue. Because Ada is a strongly-typed language, in most cases,
the enumeration that we're referring to is clear from the context. For example:
4 procedure Red_Colors is
5 C1 : constant Color := Red;
6 -- Using Red from Color
7
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Overloading
MD5: dd590eab88164773e974e748d77a51af
Runtime output
C1 = Red
C2 = Red
When assigning Red to C1 and C2, it is clear that, in the first case, we're referring to Red
of Color type, while in the second case, we're referring to Red of the Primary_Color type.
The same logic applies to comparisons such as the one in if C1 = Red: because the type
of C1 is defined (Color), it's clear that the Red enumeration is the one of Color type.
Enumeration subtypes
Note that enumeration overloading is not the same as enumeration subtypes. For example,
we could define the following subtype:
3 subtype Blue_Shades is
4 Colors range Blue .. Darkblue;
5
6 end Colors.Shades;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Overloading
MD5: 9c13508bda487cae02dbf8b403271540
In this case, Blue of Blue_Shades and Blue of Colors are the same enumeration.
Enumeration ambiguities
A situation where enumeration overloading might lead to ambiguities is when we use them
in ranges. For example:
3 type Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
16 type Primary_Color is
17 (Red,
18 Green,
19 Blue);
20
21 end Colors;
4 procedure Color_Loop is
5 begin
6 for C in Red .. Blue loop
7 -- ^^^^^^^^^^^
8 -- ERROR: range is ambiguous!
9 Put_Line (Color'Image (C));
10 end loop;
11 end Color_Loop;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Ambiguities
MD5: 82d0d3f28f1faf6b296a4f44db71f41b
Build output
Here, it's not clear whether the range in the loop is of Color type or of Primary_Color
type. Therefore, we get a compilation error for this code example. The next line in the
code example — the one with the call to Put_Line — gives us a hint about the developer's
intention to refer to the Color type. In this case, we can use qualification — for example,
Color'(Red) — to resolve the ambiguity:
4 procedure Color_Loop is
5 begin
6 for C in Color'(Red) .. Color'(Blue) loop
7 Put_Line (Color'Image (C));
8 end loop;
9 end Color_Loop;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Ambiguities
MD5: c3e946d330bb6aed258bcd005a540794
Runtime output
RED
DARKRED
LIME
FORESTGREEN
GREEN
DARKGREEN
BLUE
Note that, in the case of ranges, we can also rewrite the loop by using a range declaration:
4 procedure Color_Loop is
5 begin
6 for C in Color range Red .. Blue loop
7 Put_Line (Color'Image (C));
8 end loop;
9 end Color_Loop;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Ambiguities
MD5: 23f8db4fcb5710f7bda6b511234e0448
Runtime output
RED
DARKRED
LIME
FORESTGREEN
GREEN
DARKGREEN
BLUE
4 procedure Color_Loop is
5 subtype Red_To_Blue is Color range Red .. Blue;
6 begin
7 for C in Red_To_Blue loop
8 Put_Line (Color'Image (C));
9 end loop;
10 end Color_Loop;
As we've said above, a typical Ada compiler doesn't generate function calls for code that
deals with enumerations. On the contrary, each enumeration has values associated with
it, and the compiler uses those values instead.
Each enumeration has:
• a position value, which is a natural value indicating the position of the enumeration in
the enumeration type; and
• an internal code, which, by default, in most cases, is the same as the position value.
Also, by default, the value of the first position is zero, the value of the second position is
one, and so on. We can see this by listing each enumeration of the Day type and displaying
the value of the corresponding position:
7 end Days;
4 procedure Show_Days is
5 begin
6 for D in Day loop
7 Put_Line (Day'Image (D)
8 & " position = "
9 & Integer'Image (Day'Pos (D)));
10 Put_Line (Day'Image (D)
11 & " internal code = "
12 & Integer'Image
13 (Day'Enum_Rep (D)));
14 end loop;
15 end Show_Days;
Project: Courses.Advanced_Ada.Data_Types.Types.Enumerations.Enumeration_Values
MD5: d6c5cb99b9770893b7277c470f40e805
Runtime output
MON position = 0
MON internal code = 0
TUE position = 1
TUE internal code = 1
WED position = 2
WED internal code = 2
THU position = 3
THU internal code = 3
FRI position = 4
FRI internal code = 4
SAT position = 5
SAT internal code = 5
SUN position = 6
SUN internal code = 6
Note that this application also displays the internal code, which, in this case, is equivalent
to the position value for all enumerations.
We may, however, change the internal code of an enumeration using a representation
clause. We discuss this topic in another section (page 347).
Indefinite types were mentioned back in the Introduction to Ada course (page 81). In this
section, we'll recapitulate and extend on both definite and indefinite types.
Definite types are the basic kind of types we commonly use when programming applica-
tions. For example, we can only declare variables of definite types; otherwise, we get a
compilation error. Interestingly, however, to be able to explain what definite types are, we
need to first discuss indefinite types.
Indefinite types include:
• unconstrained arrays;
• record types with unconstrained discriminants without defaults.
Let's see some examples of indefinite types:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
17 end Unconstrained_Types;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: e569dc73150b834c9315b14d46c0ac79
Important
Note that we cannot use indefinite subtypes as discriminants. For example, the following
code won't compile:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
11 end Unconstrained_Types;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types_Error
MD5: cf73d308ddb4a8c2503146ecd550a791
Build output
Integer_Array is a correct type declaration — although the type itself is indefinite af-
ter the declaration. However, we cannot use it as the discriminant in the declaration of
Simple_Record. We could, however, have a correct declaration by using discriminants as
access values:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 type Integer_Array_Access is
7 access Integer_Array;
8
9 type Simple_Record
10 (Arr : Integer_Array_Access) is
11 record
(continues on next page)
15 end Unconstrained_Types;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types_Error
MD5: dc8193e3684b172e8503e1c5427cf93d
3 procedure Using_Unconstrained_Type is
4
5 A : Integer_Array;
6
7 R : Simple_Record;
8
9 begin
10 null;
11 end Using_Unconstrained_Type;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: 806d4ec64b911a9978ad30fa45a6df10
Build output
As we can see when we try to build this example, the compiler complains about the dec-
laration of A and R because we're trying to use indefinite types to declare variables. The
main reason we cannot use indefinite types here is that the compiler needs to know at this
point how much memory it should allocate. Therefore, we need to provide the information
that is missing. In other words, we need to change the declaration so the type becomes
definite. We can do this by either declaring a definite type or providing constraints in the
variable declaration. For example:
3 procedure Using_Unconstrained_Type is
4
5 subtype Integer_Array_5 is
6 Integer_Array (1 .. 5);
7
8 A1 : Integer_Array_5;
9 A2 : Integer_Array (1 .. 5);
10
11 subtype Simple_Record_Ext is
12 Simple_Record (Extended => True);
13
14 R1 : Simple_Record_Ext;
15 R2 : Simple_Record (Extended => True);
16
17 begin
18 null;
19 end Using_Unconstrained_Type;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: f8e192537f42eea0ebc7873bdaa898f1
Build output
In this example, we declare the Integer_Array_5 subtype, which is definite because we're
constraining it to a range from 1 to 5, thereby defining the information that was missing in
the indefinite type Integer_Array. Because we now have a definite type, we can use it to
declare the A1 variable. Similarly, we can use the indefinite type Integer_Array directly
in the declaration of A2 by specifying the previously unknown range.
Similarly, in this example, we declare the Simple_Record_Ext subtype, which is definite
because we're initializing the record discriminant Extended. We can therefore use it in
the declaration of the R1 variable. Alternatively, we can simply use the indefinite type
Simple_Record and specify the information required for the discriminants. This is what we
do in the declaration of the R2 variable.
Although we cannot use indefinite types directly in variable declarations, they're very useful
to generalize algorithms. For example, we can use them as parameters of a subprogram:
4 procedure Using_Unconstrained_Type is
5 A_5 : constant Integer_Array (1 .. 5) :=
6 (1, 2, 3, 4, 5);
7 A_10 : constant Integer_Array (1 .. 10) :=
8 (1, 2, 3, 4, 5, others => 99);
9 begin
10 Show_Integer_Array (A_5);
11 Show_Integer_Array (A_10);
12 end Using_Unconstrained_Type;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: 3f744fa5921a55865bc5361ec4c6eb88
Runtime output
1: 1
2: 2
3: 3
4: 4
5: 5
--------
1: 1
2: 2
3: 3
4: 4
5: 5
6: 99
7: 99
8: 99
9: 99
10: 99
--------
In this particular example, the compiler doesn't know a priori which range is used for the
A parameter of Show_Integer_Array. It could be a range from 1 to 5 as used for vari-
able A_5 of the Using_Unconstrained_Type procedure, or it could be a range from 1
to 10 as used for variable A_10, or it could be anything else. Although the parameter
A of Show_Integer_Array is unconstrained, both calls to Show_Integer_Array — in Us-
3 procedure Show_Integer_Array_Header
4 (AA : Integer_Array;
5 HH : String);
4 procedure Show_Integer_Array_Header
5 (AA : Integer_Array;
6 HH : String)
7 is
8 begin
9 Put_Line (HH);
10 Show_Integer_Array (AA);
11 end Show_Integer_Array_Header;
3 with Show_Integer_Array_Header;
4
5 procedure Using_Unconstrained_Type is
6 A_5 : constant Integer_Array (1 .. 5) :=
7 (1, 2, 3, 4, 5);
8 A_10 : constant Integer_Array (1 .. 10) :=
9 (1, 2, 3, 4, 5, others => 99);
10 begin
11 Show_Integer_Array_Header (A_5,
12 "First example");
13 Show_Integer_Array_Header (A_10,
14 "Second example");
15 end Using_Unconstrained_Type;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: dd09f8c4089c6ad4c18410879f80f731
Runtime output
First example
1: 1
2: 2
3: 3
4: 4
5: 5
--------
Second example
1: 1
(continues on next page)
In this case, we're calling the Show_Integer_Array procedure with another unconstrained
parameter (the AA parameter). However, although we could have a long chain of proce-
dure calls using indefinite types in their parameters, we still use a (definite) object at the
beginning of this chain. For example, for the A_5 object, we have this chain:
A_5
3 procedure Show_Integer_Array_Plus
4 (A : Integer_Array;
5 V : Integer);
3 procedure Show_Integer_Array_Plus
4 (A : Integer_Array;
5 V : Integer)
6 is
7 A_Plus : Integer_Array (A'Range);
8 begin
9 for I in A_Plus'Range loop
10 A_Plus (I) := A (I) + V;
11 end loop;
12 Show_Integer_Array (A_Plus);
13 end Show_Integer_Array_Plus;
3 with Show_Integer_Array_Plus;
4
5 procedure Using_Unconstrained_Type is
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Indefinite_Types
MD5: e58ae62272ff0b27c5f6e171c88a6880
Runtime output
1: 6
2: 7
3: 8
4: 9
5: 10
--------
Constrained Attribute
We can use the Constrained attribute to verify whether an object of discriminated type
is constrained or not. Let's start our discussion by reusing the Simple_Record type from
previous examples. In this version of the Unconstrained_Types package, we're adding a
Reset procedure for the discriminated record type:
3 type Simple_Record
4 (Extended : Boolean := False) is
5 record
6 V : Integer;
7 case Extended is
8 when False =>
9 null;
10 when True =>
11 V_Float : Float;
12 end case;
13 end record;
(continues on next page)
25 http://www.ada-auth.org/standards/22rm/html/RM-3-3.html
17 end Unconstrained_Types;
10 Zero_Extended : constant
11 Simple_Record := (Extended => True,
12 V => 0,
13 V_Float => 0.0);
14 begin
15 Put_Line ("---- Reset: R'Constrained => "
16 & R'Constrained'Image);
17
29 end Unconstrained_Types;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Constrained_Attribute
MD5: b56e6d71fd4f05e8490412d7fe40b923
As the name indicates, the Reset procedure initializes all record components with zero.
Note that we use the Constrained attribute to verify whether objects are constrained before
assigning to them. For objects that are not constrained, we can simply assign another object
to it — as we do with the R := Zero_Extended statement. When an object is constrained,
however, the discriminants must match. If we assign an object to R, the discriminant of
that object must match the discriminant of R. This is the kind of verification that we do in
the else part of that procedure: we check the state of the Extended discriminant before
assigning an object to the R parameter.
The Using_Constrained_Attribute procedure below declares two objects of Sim-
ple_Record type: R1 and R2. Because the Simple_Record type has a default value for
its discriminant, we can declare objects of this type without specifying a value for the dis-
criminant. This is exactly what we do in the declaration of R1. Here, we don't specify any
constraints, so that it takes the default value (Extended => False). In the declaration of
R2, however, we explicitly set Extended to False:
5 procedure Using_Constrained_Attribute is
6 R1 : Simple_Record;
7 R2 : Simple_Record (Extended => False);
8
9 procedure Show_Rs is
10 begin
11 Put_Line ("R1'Constrained => "
12 & R1'Constrained'Image);
13 Put_Line ("R1.Extended => "
14 & R1.Extended'Image);
15 Put_Line ("--");
16 Put_Line ("R2'Constrained => "
17 & R2'Constrained'Image);
18 Put_Line ("R2.Extended => "
19 & R2.Extended'Image);
20 Put_Line ("----------------");
21 end Show_Rs;
22 begin
23 Show_Rs;
24
25 Reset (R1);
26 Reset (R2);
27 Put_Line ("----------------");
28
29 Show_Rs;
30 end Using_Constrained_Attribute;
Project: Courses.Advanced_Ada.Data_Types.Types.Definite_Indefinite_Subtypes.
↪Constrained_Attribute
MD5: f7517fcd3c68a784f55064f188d4e7bb
Runtime output
When we run this code, the user messages from Show_Rs indicate to us that R1 is not
constrained, while R2 is constrained. Because we declare R1 without specifying a value for
the Extended discriminant, R1 is not constrained. In the declaration of R2, on the other
hand, the explicit value for the Extended discriminant makes this object constrained. Note
that, for both R1 and R2, the value of Extended is False in the declarations.
As we were just discussing, the Reset procedure includes checks to avoid mismatches in
discriminants. When we don't have those checks, we might get exceptions at runtime. We
can force this situation by replacing the implementation of the Reset procedure with the
following lines:
-- [...]
begin
Put_Line ("---- Reset: R'Constrained => "
& R'Constrained'Image);
R := Zero_Extended;
end Reset;
This exception is raised during the call to Reset (R2). As see in the code, R2 is con-
strained. Also, its Extended discriminant is set to False, which means that it doesn't have
the V_Float component. Therefore, R2 is not compatible with the constant Zero_Extended
object, so we cannot assign Zero_Extended to R2. Also, because R2 is constrained, its
Extended discriminant cannot be modified.
The behavior is different for the call to Reset (R1), which works fine. Here, when we
pass R1 as an argument to the Reset procedure, its Extended discriminant is False by
default. Thus, R1 is also not compatible with the Zero_Extended object. However, because
R1 is not constrained, the assignment modifies R1 (by changing the value of the Extended
discriminant). Therefore, with the call to Reset, the Extended discriminant of R1 changes
from False to True.
Incomplete types — as the name suggests — are types that have missing information in
their declaration. This is a simple example:
type Incomplete;
Because this type declaration is incomplete, we need to provide the missing information at
some later point. Consider the incomplete type R in the following example:
3 type R;
4 -- Incomplete type declaration!
5
6 type R is record
7 I : Integer;
8 end record;
9 -- type R is now complete!
10
11 end Incomplete_Type_Example;
26 http://www.ada-auth.org/standards/22rm/html/RM-3-7-2.html
Project: Courses.Advanced_Ada.Data_Types.Types.Incomplete_Types.Incomplete_Types
MD5: 5ca250595f2b0cc101df286ab319982f
3 private
4
5 type R;
6 -- Incomplete type declaration!
7
8 end Incomplete_Type_Example;
3 type R is record
4 I : Integer;
5 end record;
6 -- type R is now complete!
7
8 end Incomplete_Type_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.Incomplete_Types.Incomplete_Types_2
MD5: fd2f0301b4a63887add1cb2093692ddb
A typical application of incomplete types is to create linked lists using access types based
on those incomplete types. This kind of type is called a recursive type. For example:
3 type Integer_List;
4
12 end Linked_List_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.Incomplete_Types.Linked_List_Example
MD5: b2d3a048473d498bbe691bc6e38ca1e9
Here, the N component of Integer_List is essentially giving us access to the next element
of Integer_List type. Because the Next type is both referring to the Integer_List type
and being used in the declaration of the Integer_List type, we need to start with an
incomplete declaration of the Integer_List type and then complete it after the declaration
of Next.
Incomplete types are useful to declare mutually dependent types (page 418), as we'll see
later on. Also, we can also have formal incomplete types, as we'll discuss later.
Ada distinguishes between the partial and the full view of a type. The full view is a type
declaration that contains all the information needed by the compiler. For example, the
following declaration of type R represents the full view of this type:
8 end Full_View;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Full_View
MD5: d37792287d08f9aa3d32499e233516df
As soon as we start applying encapsulation and information hiding — via the private key-
word — to a specific type, we are introducing a partial view and making only that view
compile-time visible to clients. Doing so requires us to introduce the private part of the
package (unless already present). For example:
6 private
7
13 end Partial_Full_Views;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Partial_Full_View
MD5: b0cf748e43b23ea6c845e283c4266ff3
As indicated in the example, the type R is private declaration is the partial view of the
R type, while the type R is record [...] declaration in the private part of the package
is the full view.
Although the partial view doesn't contain the full type declaration, it contains very important
information for the users of the package where it's declared. In fact, the partial view of a
private type is all that users actually need to know to effectively use this type, while the
full view is only needed by the compiler.
In the previous example, the partial view indicates that R is a private type, which means
that, even though users cannot directly access any information stored in this type — for
example, read the value of the I component of R —, they can use the R type to declare
objects. For example:
3 procedure Main is
4 -- Partial view of R indicates that
5 -- R exists as a private type, so we
6 -- can declare objects of this type:
7 C : R;
8 begin
9 -- But we cannot directly access any
10 -- information declared in the full
11 -- view of R:
12 --
13 -- C.I := 42;
14 --
15 null;
16 end Main;
Build output
main.adb:7:04: warning: variable "C" is never read and never assigned [-gnatwv]
In many cases, the restrictions applied to the partial and full views must match. For exam-
ple, if we declare a limited type in the full view of a private type, its partial view must also
be limited:
7 private
8
13 end Limited_Private_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Limited_Private
MD5: 23d01b93fe052a500c8ca6ff76a2fd51
There are, however, situations where the full view may contain additional requirements that
aren't mentioned in the partial view. For example, a type may be declared as non-tagged
in the partial view, but, at the same time, be tagged in the full view:
6 private
7
13 end Tagged_Full_View_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Tagged_Full_View
MD5: 0ff9142b1ee086695b98b72a9d0f50ac
In this case, from a user's perspective, the R type is non-tagged, so that users cannot
use any object-oriented programming features for this type. In the package body of
Tagged_Full_View_Example, however, this type is tagged, so that all object-oriented pro-
gramming features are available for subprograms of the package body that make use of this
type. Again, the partial view of the private type contains the most important information
for users that want to declare objects of this type.
Although it's very common to declare private types as record types, this is not the only
option. In fact, we could declare any type in the full view — scalars, for example —, so we
could declare a "private integer" type:
6 private
7
11 end Private_Integers;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Integer
MD5: f1fcbed95e0f66a6f67d1bfd9ba9df1c
This code compiles as expected, but isn't very useful. We can improve it by adding operators
to it, for example:
9 private
10
14 end Private_Integers;
20 end Private_Integers;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Integer
MD5: ac161cb5debfde16465c45949cf682d7
3 procedure Show_Private_Integers is
4 A, B : Private_Integer;
5 begin
6 A := A + B;
7 end Show_Private_Integers;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Integer
MD5: 5933779ce5f0802b448df96c42e65a8d
Build output
In this example, we use the + operator as if we were adding two common integer variables
of Integer type.
Unconstrained Types
There are, however, some limitations: we cannot use unconstrained types such as arrays
or even discriminants for arrays in the same way as we did for scalars. For example, the
following declarations won't work:
5 type Private_Constrained_Array
6 (L : Positive) is private;
7
8 private
9
10 type Integer_Array is
11 array (Positive range <>) of Integer;
12
13 type Private_Unconstrained_Array is
14 array (Positive range <>) of Integer;
15
16 type Private_Constrained_Array
17 (L : Positive) is
18 array (1 .. 2) of Integer;
19
26 end Private_Arrays;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Array
MD5: b873c2d381c159532b429101e4533c05
Build output
Completing the private type with an unconstrained array type in the full view is not allowed
because clients could expect, according to their view, to declare objects of the type. But
doing so would not be allowed according to the full view. So this is another case of the
partial view having to present clients with a sufficiently true view of the type's capabilities.
One solution is to rewrite the declaration of Private_Constrained_Array using a record
type:
3 type Private_Constrained_Array
4 (L : Positive) is private;
5
6 private
7
8 type Integer_Array is
9 array (Positive range <>) of Integer;
10
11 type Private_Constrained_Array
12 (L : Positive) is
13 record
14 Arr : Integer_Array (1 .. 2);
15 end record;
16
17 end Private_Arrays;
3 procedure Declare_Private_Array is
4 Arr : Private_Constrained_Array (5);
5 begin
6 null;
7 end Declare_Private_Array;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Array
MD5: 3830721499a59d85efddd4989aa7c288
Build output
Now, the code compiles fine — but we had to use a record type in the full view to make it
work.
Another solution is to make the private type indefinite. In this case, the client's partial view
would be consistent with a completion as an indefinite type in the private part:
6 function Init
7 (L : Positive)
8 return Private_Constrained_Array;
9
10 private
11
12 type Private_Constrained_Array is
13 array (Positive range <>) of Integer;
14
15 end Private_Arrays;
3 function Init
4 (L : Positive)
5 return Private_Constrained_Array
6 is
7 PCA : Private_Constrained_Array (1 .. L);
8 begin
9 return PCA;
10 end Init;
11
12 end Private_Arrays;
3 procedure Declare_Private_Array is
4 Arr : Private_Constrained_Array := Init (5);
5 begin
6 null;
7 end Declare_Private_Array;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_View.Private_Array
MD5: cd170a1e44fffb93314776a68f1cb413
Build output
The bounds for the object's declaration come from the required initial value when an object
is declared. In this case, we initialize the object with a call to the Init function.
An important operation when dealing with objects of different types is type conversion,
which we already discussed in the Introduction to Ada course (page 56). In fact, we can
convert an object Obj_X of an operand type X to a similar, closely related target type Y by
simply indicating the target type: Y (Obj_X). In this section, we discuss type conversions
for different kinds of types.
Ada distinguishes between two kinds of conversion: value conversion and view conversion.
The main difference is the way how the operand (argument) of the conversion is evaluated:
• in a value conversion, the operand is evaluated as an expression (page 581);
• in a view conversion, the operand is evaluated as a name.
In other words, we cannot use expressions such as 2 * A in a view conversion, but only A.
In a value conversion, we could use both forms.
Value conversion
Value conversions are possible for various types. In this section, we see some examples,
starting with types derived from scalar types up to array conversions.
Let's start with the conversion between a scalar type and its derived types. For example,
we can convert back-and-forth between the Integer type and the derived Int type:
10 end Custom_Integers;
4 procedure Show_Conversion is
5 Int_Var : Int := 1;
6 Integer_Var : Integer := 2;
7 begin
8 -- Int to Integer conversion
9 Integer_Var := Integer (Int_Var);
(continues on next page)
29 http://www.ada-auth.org/standards/22rm/html/RM-4-6.html
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Root_Derived_Type_
↪Conversion
MD5: 7cd324f308edc34de3bc4bccce63f1ee
Runtime output
Integer_Var : 1
Integer_Var : 2
Int_Var : 4
In the Show_Conversion procedure from this example, we first convert from Int to Integer.
Then, we do the same conversion while providing the resulting value as an actual parameter
for the Double function. Finally, we convert the Integer_Var * 2 expression from Integer
to Int.
Note that the converted value must conform to any constraints that the target type might
have. In the example above, Int has a predicate that dictates that its value cannot be zero.
This (dynamic) predicate is checked at runtime, so an exception is raised if it fails:
4 procedure Show_Conversion is
5 Int_Var : Int;
6 Integer_Var : Integer;
7 begin
8 Integer_Var := 0;
9 Int_Var := Int (Integer_Var);
10
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Root_Derived_Type_
↪Conversion
MD5: 4150cdffd4c1fed39fa1728a77fa599f
Runtime output
In this case, the conversion from Integer to Int fails because, while zero is a valid integer
value, it doesn't obey Int's predicate.
A typical conversion is the one between integer and floating-point values. For example:
3 procedure Show_Conversion is
4 F : Float := 1.0;
5 I : Integer := 2;
6 begin
7 I := Integer (F);
8
12 I := 4;
13 F := Float (I);
14
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Numeric_Type_
↪Conversion
MD5: f64649c786377617b0bc9ff49475ba55
Runtime output
I : 1
F : 4.00000E+00
Also, we can convert between fixed-point types and floating-point or integer types:
4 procedure Show_Conversion is
5 F : Float;
6 FP : TQ15_31;
7 I : Integer;
8 begin
9 FP := TQ15_31 (10.25);
10 I := Integer (FP);
11
17 I := 128;
18 FP := TQ15_31 (I);
19 F := Float (FP);
20
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Numeric_Type_
↪Conversion
MD5: 70714ba396b03469397b982e00299561
Runtime output
FP : 10.25000
I : 10
FP : 128.00000
F : 1.28000E+02
As we can see in the examples above, converting between different numeric types works in
all directions. (Of course, rounding is applied when converting from floating-point to integer
types, but this is expected.)
Enumeration conversion
We can also convert between an enumeration type and a type derived from it:
8 end Custom_Enumerations;
4 procedure Show_Conversion is
5 P : Priority := Low;
6 IP : Important_Priority := High;
7 begin
8 P := Priority (IP);
9
13 P := Mid;
14 IP := Important_Priority (P);
15
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Enumeration_Type_
↪Conversion
MD5: b1e42cbd8b57291d3b3a9968c41efdd7
Runtime output
P: HIGH
IP: MID
In this example, we have the Priority type and the derived type Important_Priority. As
expected, the conversion works fine when the converted value is in the range of the target
type. If not, an exception is raised:
4 procedure Show_Conversion is
5 P : Priority;
6 IP : Important_Priority;
7 begin
8 P := Low;
9 IP := Important_Priority (P);
10
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Enumeration_Type_
↪Conversion
MD5: 6bbc777d4b44023bf572ca5dc6c2b4f8
Build output
Runtime output
Array conversion
Similarly, we can convert between array types. For example, if we have the array type
Integer_Array and its derived type Derived_Integer_Array, we can convert between
those array types:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
9 end Custom_Arrays;
6 procedure Show_Conversion is
7 subtype Common_Range is Positive range 1 .. 3;
8
9 AI : Integer_Array (Common_Range);
10 AI_D : Derived_Integer_Array (Common_Range);
11 begin
12 AI_D := [1, 2, 3];
13 AI := Integer_Array (AI_D);
14
18 AI := [4, 5, 6];
19 AI_D := Derived_Integer_Array (AI);
20
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Array_Type_
↪Conversion
MD5: e0a9fd519685b418a06dc7a3d0dab1c0
Runtime output
AI:
[ 1, 2, 3]
AI_D:
[ 4, 5, 6]
Note that both arrays must have the same number of components in order for the conver-
sion to be successful. (Sliding is fine, though.) In this example, both arrays have the same
range: Common_Range.
We can also convert between array types that aren't derived one from the other. As long
as the components and the index subtypes are of the same type, the conversion between
those types is possible. To be more precise, these are the requirements for the array con-
version to be accepted:
• The component types must be the same type.
• The index types (or subtypes) must be the same or, at least, convertible.
• The dimensionality of the arrays must be the same.
• The bounds must be compatible (but not necessarily equal).
Converting between different array types can be very handy, especially when we're dealing
with array types that were not declared in the same package. For example:
3 type Integer_Array_1 is
4 array (Positive range <>) of Integer;
5
6 type Float_Array_1 is
7 array (Positive range <>) of Float;
8
9 end Custom_Arrays_1;
3 type Integer_Array_2 is
4 array (Positive range <>) of Integer;
5
6 type Float_Array_2 is
7 array (Positive range <>) of Float;
8
9 end Custom_Arrays_2;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Array_Type_
↪Conversion
MD5: 5c62f7cf94eedf8b0b223c24a83cc8d3
Runtime output
AI_1:
[ 1, 2, 3]
AI_2:
[ 4, 5, 6]
AF_1:
[ 4.00000E+00, 5.00000E+00, 6.00000E+00]
AF_2:
[ 4.00000E+00, 5.00000E+00, 6.00000E+00]
As we can see in this example, the fact that Integer_Array_1 and Integer_Array_2 have
the same component type (Integer) allows us to convert between them. The same applies
to the Float_Array_1 and Float_Array_2 types.
A conversion is not possible when the component types don't match. Even though we can
convert between integer and floating-point types, we cannot convert an array of integers
to an array of floating-point directly. Therefore, we cannot write a statement such as AF_1
:= Float_Array_1 (AI_1);.
However, when the components don't match, we can of course implement the array conver-
sion by converting the individual components. For the example above, we used an iterated
component association in an array aggregate: [for I in AF_1'Range => Float (AI_1
(I))];. (We discuss this topic later in another chapter (page 450).)
We may also encounter array types originating from the instantiation of generic packages.
In this case as well, we can use array conversions. Consider the following generic package:
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Generic_Array_Type_
↪Conversion
MD5: 8b3a963a1292a90d99d83c6d81ce3995
We could instantiate this generic package and reuse parts of the previous code example:
6 procedure Show_Conversion is
7 package CA_Int_1 is
8 new Custom_Arrays (T => Integer);
9 package CA_Int_2 is
10 new Custom_Arrays (T => Integer);
11
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Generic_Array_Type_
↪Conversion
MD5: 956186d864763924b93b6a9d807525b6
Runtime output
AI_1:
[ 1, 2, 3]
AI_2:
[ 4, 5, 6]
As we can see in this example, each of the instantiated CA_Int_1 and CA_Int_2 packages
has a T_Array type. Even though these T_Array types have the same name, they're actu-
ally completely unrelated types. However, we can still convert between them in the same
way as we did in the previous code examples.
View conversion
As mentioned before, view conversions just allow names to be converted. Thus, we cannot
use expressions in this case.
Note that a view conversion never changes the value during the conversion. We could say
that a view conversion is simply making us view an object from a different angle. The object
itself is still the same for both the original and the target types.
For example, consider this package:
11 Obj : T_Derived;
12
13 end Some_Tagged_Types;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Tagged_Types_View
MD5: 2e18ba972682f1ae1d38e38842fde48e
Here, Obj is an object of type T_Derived. When we view this object, we notice that it has
two components: A and B. However, we could view this object as being of type T. From that
perspective, this object only has one component: A. (Note that changing the perspective
doesn't change the object itself.) Therefore, a view conversion from T_Derived to T just
makes us view the object Obj from a different angle.
In this sense, a view conversion changes the view of a given object to the target type's
view, both in terms of components that exist and operations that are available. It doesn't
really change anything at all in the value itself.
There are basically two kinds of view conversions: the ones using tagged types and the
ones using untagged types. We discuss these kinds of conversion in this section.
A conversion between tagged types is a view conversion. Let's consider a typical code
example that declares one, two and three-dimensional points:
21 end Points;
23 end Points;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Tagged_Type_
↪Conversion
MD5: 0acc05ae2310ab4ba038dfdb6bae0495
We can use the types from the Points package and convert between each other:
4 procedure Show_Conversion is
5 P_1D : Point_1D;
6 P_3D : Point_3D;
7 begin
8 P_3D := (X => 0.1, Y => 0.5, Z => 0.3);
9 P_1D := Point_1D (P_3D);
10
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Tagged_Type_
↪Conversion
MD5: fb8e07c8f2399cfae935179d8f413150
Runtime output
In this example, as expected, we're able to convert from the Point_3D type (which has
three components) to the Point_1D type, which has only one component.
For untagged types, a view conversion is the one that happens when we have an object of
an untagged type as an actual parameter for a formal in out or out parameter.
Let's see a code example. Consider the following simple procedure:
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Untagged_Type_View_
↪Conversion
MD5: 31f4409d9faeaf213c5940de65eeb014
The Double procedure has an in out parameter of Float type. We can call this procedure
using an integer variable I as the actual parameter. For example:
4 procedure Show_Conversion is
5 I : Integer;
6 begin
7 I := 2;
8 Put_Line ("I : "
9 & I'Image);
10
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Untagged_Type_View_
↪Conversion
MD5: 2256d3c120d569789dcd4c9959ed9d0f
Runtime output
I : 2
I : 4
In this case, the Float (I) conversion in the call to Double creates a temporary floating-
point variable. This is the same as if we had written the following code:
4 procedure Show_Conversion is
5 I : Integer;
6 begin
7 I := 2;
8 Put_Line ("I : "
9 & I'Image);
10
11 declare
12 F : Float := Float (I);
13 begin
14 Double (F);
15 I := Integer (F);
16 end;
17 Put_Line ("I : "
18 & I'Image);
19 end Show_Conversion;
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Untagged_Type_View_
↪Conversion
MD5: 3b90caf789952710ece42141a7b60968
Runtime output
I : 2
I : 4
In this sense, the view conversion that happens in Double (Float (I)) can be considered
syntactic sugar, as it allows us to elegantly write two conversions in a single statement.
Implicit conversions
Implicit conversions are only possible when we have a type T and a subtype S related to
the T type. For example:
12 end Custom_Integers;
4 procedure Show_Conversion is
5 Int_Var : Int;
6 Sub_Int_1_Var : Sub_Int_1;
7 Sub_Int_2_Var : Sub_Int_2;
8 Integer_Var : Integer;
9 begin
10 Integer_Var := 5;
11 Int_Var := Int (Integer_Var);
12
16 -- Implicit conversions:
17 -- no explicit conversion required!
18 Sub_Int_1_Var := Integer_Var;
19 Sub_Int_2_Var := Integer_Var;
20
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Implicit_Subtype_
↪Conversion
MD5: dbbe498fa66701ca94f48119b1bc1a91
Runtime output
Int_Var : 5
Sub_Int_1_Var : 5
Sub_Int_2_Var : 5
In this example, we declare the Int type and the Sub_Int_1 and Sub_Int_2 subtypes:
• the Int type is derived from the Integer type,
• Sub_Int_1 is a subtype of the Integer type, and
• Sub_Int_2 is a subtype of the Sub_Int_1 subtype.
We need an explicit conversion when converting between the Integer and Int types. How-
ever, as the conversion is implicit for subtypes, we can simply write Sub_Int_1_Var :=
Integer_Var;. (Of course, writing the explicit conversion Sub_Int_1 (Integer_Var) in
the assignment is possible as well.) Also, the same applies to the Sub_Int_2 subtype: we
can write an implicit conversion in the Sub_Int_2_Var := Integer_Var; statement.
For other kinds of types, such as records, a direct conversion as we've seen so far isn't pos-
sible. In this case, we have to write a conversion function ourselves. A common convention
in Ada is to name this function To_Typename. For example, if we want to convert from any
type to Integer or Float, we implement the To_Integer and To_Float functions, respec-
tively. (Obviously, because Ada supports subprogram overloading, we can have multiple
To_Typename functions for different operand types.)
Let's see a code example:
11 end Custom_Rec;
4 procedure Show_Conversion is
5 R : Rec;
6 I : Integer;
7 begin
8 R := (X => 2);
9 I := To_Integer (R);
10
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Other_Type_
↪Conversions
MD5: d52a4fde48243a7dd6942f0b2b91ce62
Runtime output
I : 2
In this example, we have the To_Integer function that converts from the Rec type to the
Integer type.
In other languages
In C++, you can define conversion operators to cast between objects of different classes.
Also, you can overload the = operator. Consider this example:
#include <iostream>
class T1 {
public:
T1 (float x) :
x(x) {}
void display();
private:
float x;
};
class T3 {
public:
T3 (float x, float y, float z) :
x(x), y(y), z(z) {}
// implicit conversion
operator float() const {
return (x + y + z) / 3.0;
}
// implicit conversion
//
// operator T1() const {
// return T1((x + y + z) / 3.0);
// }
void display();
private:
float x, y, z;
};
(continues on next page)
void T1::display()
{
std::cout << "(x => " << x
<< ")" << std::endl;
}
void T3::display()
{
std::cout << "(x => " << x
<< "y => " << y
<< "z => " << z
<< ")" << std::endl;
}
int main ()
{
const T3 t_3 (0.5, 0.4, 0.6);
T1 t_1 (0.0);
float f;
// Implicit conversion
f = t_3;
// Explicit conversion
f = static_cast<float>(t_3);
// f = (float)t_3;
// Explicit conversion
t_1 = static_cast<T1>(t_3);
// t_1 = (T1)t_3;
Here, we're using operator float() and operator T1() to cast from an object of class
T3 to a floating-point value and an object of class T1, respectively. (If we switch the order
and declare the T3 class before the T1 class, we could overload the = operator, as you can
see in the commented-out lines.)
In Ada, this kind of conversions isn't available. Instead, we have to implement conversion
functions such as the To_Integer function from the previous code example. This is the
corresponding implementation:
3 type T1 is private;
4
10 type T3 is private;
11
23 private
24 type T1 is record
25 X : Float;
26 end record;
27
32 type T3 is record
33 X, Y, Z : Float;
34 end record;
35
40 end Custom_Defs;
27 end Custom_Defs;
4 procedure Show_Conversion is
5 T_3 : constant T3 := Init (0.5, 0.4, 0.6);
6 T_1 : T1 := Init (0.0);
7 F : Float;
8 begin
9 -- Explicit conversion from
10 -- T3 to Float type
11 F := To_Float (T_3);
12
Project: Courses.Advanced_Ada.Data_Types.Types.Type_Conversion.Explicit_Rec_
↪Conversion
MD5: b3e7be5488fb8026b4386063ba16aaeb
Runtime output
F : 5.00000E-01
T_1 : (X => 5.00000E-01)
In this example, we translate the casting operators from the C++ version by implementing
the To_Float and To_T1 functions. (In addition to that, we replace the C++ constructors
by Init functions.)
We already saw qualified expressions in the Introduction to Ada (page 91) course. As men-
tioned there, a qualified expression specifies the exact type or subtype that the target
expression will be resolved to, and it can be either any expression in parentheses, or an
aggregate:
8 end Simple_Integers;
3 procedure Show_Qualified_Expressions is
4 I : Int;
5 begin
6 -- Using qualified expression Int'(N)
7 I := Int'(0);
8 end Show_Qualified_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Types.Qualified_Expressions.Example
MD5: 0a83e10b51c72827e322984bd5c8009d
Here, the qualified expression Int'(0) indicates that the value zero is of Int type.
Verifying subtypes
3 procedure Show_Qualified_Expressions is
4 I : Int;
5 begin
6 I := Int_Not_Zero'(0);
7 end Show_Qualified_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Types.Qualified_Expressions.Example
MD5: 3c4ab8ad7bf75ae029047f673aa15d70
Build output
30 http://www.ada-auth.org/standards/22rm/html/RM-4-7.html
Runtime output
Here, the qualified expression Int_Not_Zero'(0) checks the dynamic predicate of the sub-
type. (This predicate check fails at runtime.)
In the Introduction to Ada course (page 65), we've seen that record components can have
default values. For example:
3 type R is record
4 X : Positive := 1;
5 Y : Positive := 10;
6 end record;
7
8 end Defaults;
In this section, we'll extend the concept of default values to other kinds of type declarations,
such as scalar types and arrays.
To assign a default value for a scalar type declaration — such as an enumeration and a new
integer —, we use the Default_Value aspect:
9 end Defaults;
3 subtype T is Integer
4 with Default_Value => -1;
(continues on next page)
7 end Defaults;
Project: Courses.Advanced_Ada.Data_Types.Types.Default_Initial_Values.Defaults_3
MD5: beef68e4a7a3714cfa3e547bdcda9a0c
Build output
3 type Arr is
4 array (Positive range <>) of Integer
5 with Default_Component_Value => -1;
6
7 end Defaults;
Project: Courses.Advanced_Ada.Data_Types.Types.Default_Initial_Values.Defaults_4
MD5: 2c390e3900e4af42498381025a37955e
15 type R is record
16 X : Positive := 1;
17 Y : Positive := 10;
18 end record;
19
20 type Arr is
21 array (Positive range <>) of Integer
22 with Default_Component_Value => -1;
23
24 end Defaults;
Project: Courses.Advanced_Ada.Data_Types.Types.Default_Initial_Values.Defaults
MD5: e9263ff5b96523c129a3d2d9bbb5a4dd
In the example below, we declare variables of the types from the Defaults package:
4 procedure Use_Defaults is
5 E1 : E;
6 T1 : T;
7 R1 : R;
8 A1 : Arr (1 .. 5);
9 begin
10 Put_Line ("Enumeration: "
11 & E'Image (E1));
12 Put_Line ("Integer type: "
13 & T'Image (T1));
14 Put_Line ("Record type: "
15 & Positive'Image (R1.X)
16 & ", "
17 & Positive'Image (R1.Y));
18
Project: Courses.Advanced_Ada.Data_Types.Types.Default_Initial_Values.Defaults
MD5: f8e55d31cbda2447fe14eb07eaad1975
Runtime output
Enumeration: E1
Integer type: -1
Record type: 1, 10
Array type: -1 -1 -1 -1 -1
As we see in the Use_Defaults procedure, all variables still have their default values, since
we haven't assigned any value to them.
31 http://www.ada-auth.org/standards/22rm/html/RM-3-5.html
32 http://www.ada-auth.org/standards/22rm/html/RM-3-6.html
Deferred constants are declarations where the value of the constant is not specified im-
mediately, but rather deferred to a later point. In that sense, if a constant declaration is
deferred, it is actually declared twice:
1. in the deferred constant declaration, and
2. in the full constant declaration.
The simplest form of deferred constant is the one that has a full constant declaration in the
private part of the package specification. For example:
8 private
9
13 end Deferred_Constants;
Project: Courses.Advanced_Ada.Data_Types.Types.Deferred_Constants.Deferred_
↪Constant_Private
MD5: f76e42326889f70fa7e1e216576f9771
Another form of deferred constant is the one that imports a constant from an external imple-
mentation — using the Import keyword. We can use this to import a constant declaration
from an implementation in C. For example, we can declare the light constant in a C file:
Project: Courses.Advanced_Ada.Data_Types.Types.Deferred_Constants.Deferred_
↪Constant_C
MD5: 71194a329dc5adaac3e01aff143a9943
11 end Deferred_Constants;
MD5: 9355d194e973c6c6540485178b2259c9
In this case, we don't have a full declaration in the Deferred_Constants package, as the
Light constant is imported from the constants.c file.
As a rule, the deferred and the full declarations should match — except, of course, for the
actual value that is missing in the deferred declaration. For instance, we're not allowed to
use different types in both declarations. However, we may use a subtype in the full decla-
ration — as long as it's compatible with the type that was used in the deferred declaration.
For example:
5 subtype Positive_Speed is
6 Speed range 0.0 .. Speed'Last;
7
11 private
12
18 end Deferred_Constants;
MD5: ad6e13e30bacb6d97ccfa6c7345ffb67
Here, we're using the Speed type in the deferred declaration of the Light constant, but
we're using the Positive_Speed subtype in the full declaration.
A useful application of deferred constants is when the value of the constant is calculated
using entities not meant to be compile-time visible to clients. As such, these other entities
are only visible in the private part of the package, so that's where the value of the deferred
constant must be computed. For example, the full constant declaration may be computed
by a call to an expression function:
8 private
9
17 end Deferred_Constants;
Project: Courses.Advanced_Ada.Data_Types.Types.Deferred_Constants.Deferred_
↪Constant_Function
MD5: f0b1a9521af31a4b48bbd54891f1c32b
Here, we call the Calculate_Light function — declared in the private part of the De-
ferred_Constants package — for the full declaration of the Light constant.
Any type definition has a kind of literal associated with it. For example, integer types are
associated with integer literals. Therefore, we can initialize an object of integer type with
an integer literal:
3 procedure Simple_Integer_Literal is
4 V : Integer;
5 begin
6 V := 10;
7
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Simple_
↪Integer_Literal
MD5: 9f65e7c319be2b292dc1fdf02dd7cfb4
Runtime output
33 http://www.ada-auth.org/standards/22rm/html/RM-7-4.html
10
Here, 10 is the integer literal that we use to initialize the integer variable V. Other examples
of literals are real literals and string literals, as we'll see later.
When we declare an enumeration type, we limit the set of literals that we can use to initialize
objects of that type:
3 procedure Simple_Enumeration is
4 type Activation_State is (Unknown, Off, On);
5
6 S : Activation_State;
7 begin
8 S := On;
9 Put_Line (Activation_State'Image (S));
10 end Simple_Enumeration;
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Simple_
↪Enumeration
MD5: 075df146fcb567817dadfdb245659773
Runtime output
ON
For objects of Activation_State type, such as S, the only possible literals that we can use
are Unknown, Off and On. In this sense, types have a constrained set of literals that can be
used for objects of that type.
User-defined literals allow us to extend this set of literals. We could, for example, extend
the type declaration of Activation_State and allow the use of integer literals for objects of
that type. In this case, we need to use the Integer_Literal aspect and specify a function
that implements the conversion from literals to the type we're declaring. For this conversion
from integer literals to the Activation_State type, we could specify that 0 corresponds
to Off, 1 corresponds to On and other values correspond to Unknown. We'll see the corre-
sponding implementation later.
These are the three kinds of literals and their corresponding aspect:
7 function Integer_To_Activation_State
8 (S : String)
9 return Activation_State;
10
11 end Activation_States;
MD5: 67b6d96f049ab6cde962aefda96bffca
Based on this specification, we can now use an integer literal to initialize an object S of
Activation_State type:
S : Activation_State := 1;
3 function Integer_To_Activation_State
4 (S : String)
5 return Activation_State is
6 begin
7 case Integer'Value (S) is
8 when 0 => return Off;
9 when 1 => return On;
10 when others => return Unknown;
11 end case;
12 end Integer_To_Activation_State;
13
14 end Activation_States;
MD5: 104a835915b93ea3b860bce03fd709a3
Let's look at a complete example that makes use of all three kinds of literals:
11 function To_Activation_State
12 (S : Wide_Wide_String)
13 return Activation_State;
14
15 function Integer_To_Activation_State
16 (S : String)
17 return Activation_State;
18
19 function Real_To_Activation_State
20 (S : String)
21 return Activation_State;
22
23 end Activation_States;
3 function To_Activation_State
4 (S : Wide_Wide_String)
5 return Activation_State
6 is
7 begin
8 if S = "Off" then
9 return Off;
10 elsif S = "On" then
11 return On;
12 else
13 return Unknown;
14 end if;
15 end To_Activation_State;
16
17 function Integer_To_Activation_State
18 (S : String)
19 return Activation_State
20 is
21 begin
22 case Integer'Value (S) is
23 when 0 => return Off;
24 when 1 => return On;
25 when others => return Unknown;
26 end case;
27 end Integer_To_Activation_State;
28
29 function Real_To_Activation_State
30 (S : String)
31 return Activation_State
32 is
33 V : constant Float := Float'Value (S);
34 begin
35 if V < 0.0 then
36 return Unknown;
37 elsif V < 1.0 then
38 return Off;
39 else
40 return On;
41 end if;
42 end Real_To_Activation_State;
43
4 procedure Activation_Examples is
5 S : Activation_State;
6 begin
7 S := "Off";
8 Put_Line ("String: Off => "
9 & Activation_State'Image (S));
10
11 S := 1;
12 Put_Line ("Integer: 1 => "
13 & Activation_State'Image (S));
14
15 S := 1.5;
16 Put_Line ("Real: 1.5 => "
17 & Activation_State'Image (S));
18 end Activation_Examples;
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Activation_
↪States
MD5: 186b7b898e4c16bfd8dcd683e8f0379d
Runtime output
In this example, we're extending the declaration of the Activation_State type to include
string and real literals. For string literals, we use the To_Activation_State function, which
converts:
• the "Off" string to Off,
• the "On" string to On, and
• any other string to Unknown.
For real literals, we use the Real_To_Activation_State function, which converts:
• any negative number to Unknown,
• a value in the interval [0, 1) to Off, and
• a value equal or above 1.0 to On.
Note that the string parameter of To_Activation_State function — which converts string
literals — is of Wide_Wide_String type, and not of String type, as it's the case for the
other conversion functions.
In the Activation_Examples procedure, we show how we can initialize an object of Acti-
vation_State type with all kinds of literals (string, integer and real literals).
With the definition of the Activation_State type that we've seen in the complete example,
we can initialize an object of this type with an enumeration literal or a string, as both forms
are defined in the type specification:
4 procedure Using_String_Literal is
5 S1 : constant Activation_State := On;
6 S2 : constant Activation_State := "On";
7 begin
8 Put_Line (Activation_State'Image (S1));
9 Put_Line (Activation_State'Image (S2));
10 end Using_String_Literal;
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Activation_
↪States
MD5: 6ca6aa79b88058801688fc2dfb186091
Runtime output
ON
ON
Note we need to be very careful when designing conversion functions. For example, the
use of string literals may limit the kind of checks that we can do. Consider the following
misspelling of the Off literal:
4 procedure Misspelling_Example is
5 S : constant Activation_State :=
6 Offf;
7 -- ^ Error: Off is misspelled.
8 begin
9 Put_Line (Activation_State'Image (S));
10 end Misspelling_Example;
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Activation_
↪States
MD5: ebc1036a58e460a9212106606461b014
Build output
As expected, the compiler detects this error. However, this error is accepted when using
the corresponding string literal:
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Activation_
↪States
MD5: 99f74c67712a9b55c146b9d57405e47f
Runtime output
UNKNOWN
function To_Activation_State
(S : Wide_Wide_String)
return Activation_State
with Pre => S = "Off" or
S = "On" or
S = "Unknown";
In this case, the precondition explicitly indicates which string literals are allowed for the
To_Activation_State type.
User-defined literals can also be used for more complex types, such as records. For exam-
ple:
14 end Silly_Records;
4 procedure Silly_Magic is
5 R1 : Silly;
6 begin
7 R1 := "Magic";
8 Put_Line (R1.X'Image & ", " & R1.Y'Image);
9 end Silly_Magic;
Project: Courses.Advanced_Ada.Data_Types.Types.User-Defined_Literals.Record_
↪Literals
MD5: 2a077045f058a8d5c09c43f66fc128be
Runtime output
42, 4.20000E+01
In this example, when we initialize an object of Silly type with a string, its components
are:
• set to 42 when using the "Magic" string; or
• simply set to zero when using any other string.
Obviously, this example isn't particularly useful. However, the goal is to show that this
approach is useful for more complex types where a string literal (or a numeric literal) might
simplify handling those types. Used-defined literals let you design types in ways that, oth-
erwise, would only be possible when using a preprocessor or a domain-specific language.
34 http://www.ada-auth.org/standards/22rm/html/RM-4-2-1.html
We have talked about the internal code of an enumeration in another section (page 294).
We may change this internal code by using a representation clause, which has the following
format:
The value of each code in a representation clause must be distinct. However, as you can
see above, we don't need to use sequential values — the values must, however, increase
for each enumeration.
We can rewrite the previous example using a representation clause:
15 end Days;
4 procedure Show_Days is
5 begin
6 for D in Day loop
7 Put_Line (Day'Image (D)
8 & " position = "
9 & Integer'Image (Day'Pos (D)));
10 Put_Line (Day'Image (D)
11 & " internal code = "
12 & Integer'Image
13 (Day'Enum_Rep (D)));
14 end loop;
15 end Show_Days;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Enumeration_
↪Representation_Clauses.Enumeration_Values
MD5: a70c3f8a967c355a4bf8f2d669f9c541
Runtime output
MON position = 0
MON internal code = 1
TUE position = 1
TUE internal code = 2
WED position = 2
WED internal code = 4
THU position = 3
THU internal code = 8
FRI position = 4
FRI internal code = 16
SAT position = 5
SAT internal code = 32
SUN position = 6
SUN internal code = 64
Now, the value of the internal code is the one that we've specified in the representation
clause instead of being equivalent to the value of the enumeration position.
In the example above, we're using binary values for each enumeration — basically viewing
the integer value as a bit-field and assigning one bit for each enumeration. As long as we
maintain an increasing order, we can use totally arbitrary values as well. For example:
15 end Days;
This section provides a glimpse on attributes and aspects used for data representation.
They are usually used for embedded applications because of strict requirements that are
often found there. Therefore, unless you have very specific requirements for your applica-
tion, in most cases, you won't need them. However, you should at least have a rudimentary
understanding of them. To read a thorough overview on this topic, please refer to the In-
troduction to Embedded Systems Programming (page 1117) course.
35 http://www.ada-auth.org/standards/22rm/html/RM-13-2.html
36 http://www.ada-auth.org/standards/22rm/html/RM-13-3.html
37 http://www.ada-auth.org/standards/22rm/html/RM-13-5-3.html
Sizes
Attribute Description
Size Size of the representation of a subtype or an object (in bits).
Object_Size Size of a component or an aliased object (in bits).
Compo- Size of a component of an array (in bits).
nent_Size
Storage_Size Number of storage elements reserved for an access type or a task
object.
For the first three attributes, the size is measured in bits. In the case of Storage_Size, the
size is measured in storage elements. Note that the size information depends your target
architecture. We'll discuss some examples to better understand the differences among
those attributes.
Important
A storage element is the smallest element we can use to store data in memory. As we'll
see soon, a storage element corresponds to a byte in many architectures.
The size of a storage element is represented by the System.Storage_Unit constant. In
other words, the storage unit corresponds to the number of bits used for a single storage
element.
In typical architectures, System.Storage_Unit is 8 bits. In this specific case, a storage
element is equal to a byte in memory. Note, however, that System.Storage_Unit might
have a value different than eight in certain architectures.
8 end Custom_Types;
5 procedure Show_Sizes is
6 V1 : UInt_7;
7 V2 : UInt_7_S32;
8 begin
9 Put_Line ("UInt_7'Size: "
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Sizes
MD5: e0da7cd23dc6989bea3d2902221f033e
Build output
Runtime output
UInt_7'Size: 7
UInt_7'Object_Size: 8
V1'Size: 8
UInt_7_S32'Size: 32
UInt_7_S32'Object_Size: 32
V2'Size: 32
UInt_7'Size: 7
UInt_7'Object_Size: 8
V1'Size: 8
UInt_7_S32'Size: 32
UInt_7_S32'Object_Size: 32
V2'Size: 32
When we use the Size attribute for a type T, we're retrieving the minimum number of bits
necessary to represent objects of that type. Note that this is not the same as the actual
size of an object of type T because the compiler will select an object size that is appropriate
for the target architecture.
In the example above, the size of the UInt_7 is 7 bits, while the most appropriate size to
store objects of this type in the memory of our target architecture is 8 bits. To be more
specific, the range of UInt_7 (0 .. 127) can be perfectly represented in 7 bits. However,
most target architectures don't offer 7-bit registers or 7-bit memory storage, so 8 bits is the
most appropriate size in this case.
We can retrieve the size of an object of type T by using the Object_Size. Alternatively, we
can use the Size attribute directly on objects of type T to retrieve their actual size — in our
example, we write V1'Size to retrieve the size of V1.
In the example above, we've used both the Size attribute (for example, UInt_7'Size) and
the Size aspect (with Size => 32). While the size attribute is a function that returns the
size, the size aspect is a request to the compiler to verify that the expected size can be used
on the target platform. You can think of this attribute as a dialog between the developer
and the compiler:
(Developer) "I think that UInt_7_S32 should be stored using at least 32 bits. Do
you agree?"
(Ada compiler) "For the target platform that you selected, I can confirm that this
is indeed the case."
Depending on the target platform, however, the conversation might play out like this:
(Developer) "I think that UInt_7_S32 should be stored using at least 32 bits. Do
you agree?"
(Ada compiler) "For the target platform that you selected, I cannot possibly do it!
COMPILATION ERROR!"
Component size
Let's continue our discussion on sizes with an example that makes use of the Compo-
nent_Size attribute:
5 type UInt_7_Array is
6 array (Positive range <>) of UInt_7;
7
8 type UInt_7_Array_Comp_32 is
9 array (Positive range <>) of UInt_7
10 with Component_Size => 32;
11
12 end Custom_Types;
5 procedure Show_Sizes is
6 Arr_1 : UInt_7_Array (1 .. 20);
7 Arr_2 : UInt_7_Array_Comp_32 (1 .. 20);
8 begin
9 Put_Line
10 ("UInt_7_Array'Size: "
11 & UInt_7_Array'Size'Image);
12 Put_Line
13 ("UInt_7_Array'Object_Size: "
14 & UInt_7_Array'Object_Size'Image);
15 Put_Line
16 ("UInt_7_Array'Component_Size: "
17 & UInt_7_Array'Component_Size'Image);
18 Put_Line
19 ("Arr_1'Component_Size: "
20 & Arr_1'Component_Size'Image);
(continues on next page)
26 Put_Line
27 ("UInt_7_Array_Comp_32'Object_Size: "
28 & UInt_7_Array_Comp_32'Size'Image);
29 Put_Line
30 ("UInt_7_Array_Comp_32'Object_Size: "
31 & UInt_7_Array_Comp_32'Object_Size'Image);
32 Put_Line
33 ("UInt_7_Array_Comp_32'Component_Size: "
34 &
35 UInt_7_Array_Comp_32'Component_Size'Image);
36 Put_Line
37 ("Arr_2'Component_Size: "
38 & Arr_2'Component_Size'Image);
39 Put_Line
40 ("Arr_2'Size: "
41 & Arr_2'Size'Image);
42 New_Line;
43 end Show_Sizes;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Sizes
MD5: e316bcb827e014075dfbf044935827ae
Build output
Runtime output
UInt_7_Array'Size: 17179869176
UInt_7_Array'Object_Size: 17179869176
UInt_7_Array'Component_Size: 8
Arr_1'Component_Size: 8
Arr_1'Size: 160
UInt_7_Array_Comp_32'Object_Size: 68719476704
UInt_7_Array_Comp_32'Object_Size: 68719476704
UInt_7_Array_Comp_32'Component_Size: 32
Arr_2'Component_Size: 32
Arr_2'Size: 640
UInt_7_Array'Size: 17179869176
UInt_7_Array'Object_Size: 17179869176
UInt_7_Array'Component_Size: 8
Arr_1'Component_Size: 8
Arr_1'Size: 160
UInt_7_Array_Comp_32'Size: 68719476704
UInt_7_Array_Comp_32'Object_Size: 68719476704
UInt_7_Array_Comp_32'Component_Size: 32
(continues on next page)
Here, the value we get for Component_Size of the UInt_7_Array type is 8 bits, which
matches the UInt_7'Object_Size — as we've seen in the previous subsection. In gen-
eral, we expect the component size to match the object size of the underlying type.
However, we might have component sizes that aren't equal to the object size of the com-
ponent's type. For example, in the declaration of the UInt_7_Array_Comp_32 type, we're
using the Component_Size aspect to query whether the size of each component can be 32
bits:
type UInt_7_Array_Comp_32 is
array (Positive range <>) of UInt_7
with Component_Size => 32;
If the code compiles, we see this value when we use the Component_Size attribute. In this
case, even though UInt_7'Object_Size is 8 bits, the component size of the array type
(UInt_7_Array_Comp_32'Component_Size) is 32 bits.
Note that we can use the Component_Size attribute with data types, as well as with actual
objects of that data type. Therefore, we can write UInt_7_Array'Component_Size and
Arr_1'Component_Size, for example.
This big number (17179869176 bits) for UInt_7_Array'Size and
UInt_7_Array'Object_Size might be surprising for you. This is due to the fact that
Ada is reporting the size of the UInt_7_Array type for the case when the complete
range is used. Considering that we specified a positive range in the declaration of the
UInt_7_Array type, the maximum length on this machine is 231 - 1. The object size of
an array type is calculated by multiplying the maximum length by the component size.
Therefore, the object size of the UInt_7_Array type corresponds to the multiplication of
231 - 1 components (maximum length) by 8 bits (component size).
Storage size
To complete our discussion on sizes, let's look at this example of storage sizes:
7 end Custom_Types;
6 procedure Show_Sizes is
7 AV1, AV2 : UInt_7_Access;
8 begin
9 Put_Line
10 ("UInt_7_Access'Storage_Size: "
(continues on next page)
17 Put_Line
18 ("UInt_7'Size: "
19 & UInt_7'Size'Image);
20 Put_Line
21 ("UInt_7_Access'Size: "
22 & UInt_7_Access'Size'Image);
23 Put_Line
24 ("UInt_7_Access'Object_Size: "
25 & UInt_7_Access'Object_Size'Image);
26 Put_Line
27 ("AV1'Size: "
28 & AV1'Size'Image);
29 New_Line;
30
37 Put_Line
38 ("AV1.all'Size: "
39 & AV1.all'Size'Image);
40 New_Line;
41 end Show_Sizes;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Sizes
MD5: 5e652ee25b8550ac331f3ce98e24f7ba
Runtime output
UInt_7_Access'Storage_Size: 0
UInt_7_Access'Storage_Size (bits): 0
UInt_7'Size: 7
UInt_7_Access'Size: 64
UInt_7_Access'Object_Size: 64
AV1'Size: 64
Allocating AV1...
Allocating AV2...
AV1.all'Size: 8
UInt_7_Access'Storage_Size: 0
UInt_7_Access'Storage_Size (bits): 0
UInt_7'Size: 7
UInt_7_Access'Size: 64
UInt_7_Access'Object_Size: 64
(continues on next page)
Allocating AV1...
Allocating AV2...
AV1.all'Size: 8
As we've mentioned earlier on, Storage_Size corresponds to the number of storage ele-
ments reserved for an access type or a task object. In this case, we see that the storage
size of the UInt_7_Access type is zero. This is because we haven't indicated that mem-
ory should be reserved for this data type. Thus, the compiler doesn't reserve memory and
simply sets the size to zero.
Because Storage_Size gives us the number of storage elements, we have to multiply this
value by System.Storage_Unit to get the total storage size in bits. (In this particular ex-
ample, however, the multiplication doesn't make any difference, as the number of storage
elements is zero.)
Note that the size of our original data type UInt_7 is 7 bits, while the size of its correspond-
ing access type UInt_7_Access (and the access object AV1) is 64 bits. This is due to the
fact that the access type doesn't contain an object, but rather memory information about
an object. You can retrieve the size of an object allocated via new by first dereferencing it
— in our example, we do this by writing AV1.all'Size.
Now, let's use the Storage_Size aspect to actually reserve memory for this data type:
8 end Custom_Types;
6 procedure Show_Sizes is
7 RAV1, RAV2 : UInt_7_Reserved_Access;
8 begin
9 Put_Line
10 ("UInt_7_Reserved_Access'Storage_Size: "
11 & UInt_7_Reserved_Access'Storage_Size'Image);
12
13 Put_Line
14 ("UInt_7_Reserved_Access'Storage_Size (bits): "
15 & Integer'Image
16 (UInt_7_Reserved_Access'Storage_Size
17 * System.Storage_Unit));
18
19 Put_Line
20 ("UInt_7_Reserved_Access'Size: "
21 & UInt_7_Reserved_Access'Size'Image);
22 Put_Line
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Sizes
MD5: 6ac085d8467a61ba4f9cd138c024442d
Runtime output
UInt_7_Reserved_Access'Storage_Size: 8
UInt_7_Reserved_Access'Storage_Size (bits): 64
UInt_7_Reserved_Access'Size: 64
UInt_7_Reserved_Access'Object_Size: 64
RAV1'Size: 64
Allocating RAV1...
Allocating RAV2...
UInt_7_Reserved_Access'Storage_Size: 8
UInt_7_Reserved_Access'Storage_Size (bits): 64
UInt_7_Reserved_Access'Size: 64
UInt_7_Reserved_Access'Object_Size: 64
RAV1'Size: 64
Allocating RAV1...
Allocating RAV2...
Since each storage element corresponds to one byte (8 bits) in this architecture, we're
reserving a maximum of 64 bits (or 8 bytes) for the UInt_7_Reserved_Access type.
This example raises an exception at runtime — a storage error, to be more specific. This
is because the maximum reserved size is 64 bits, and the size of a single access object is
64 bits as well. Therefore, after the first allocation, the reserved storage space is already
consumed, so we cannot allocate a second access object.
This behavior might be quite limiting in many cases. However, for certain applications
where memory is very constrained, this might be exactly what we want to see. For example,
having an exception being raised when the allocated memory for this data type has reached
its limit might allow the application to have enough memory to at least handle the exception
gracefully.
Alignment
For many algorithms, it's important to ensure that we're using the appropriate alignment.
This can be done by using the Alignment attribute and the Alignment aspect. Let's look
at this example:
8 end Custom_Types;
5 procedure Show_Alignment is
6 V : constant UInt_7 := 0;
7 Aligned_V : constant Aligned_UInt_7 := 0;
8 begin
9 Put_Line
10 ("UInt_7'Alignment: "
11 & UInt_7'Alignment'Image);
12 Put_Line
13 ("UInt_7'Size: "
14 & UInt_7'Size'Image);
15 Put_Line
16 ("UInt_7'Object_Size: "
17 & UInt_7'Object_Size'Image);
18 Put_Line
19 ("V'Alignment: "
20 & V'Alignment'Image);
21 Put_Line
22 ("V'Size: "
23 & V'Size'Image);
24 New_Line;
25
26 Put_Line
27 ("Aligned_UInt_7'Alignment: "
28 & Aligned_UInt_7'Alignment'Image);
29 Put_Line
30 ("Aligned_UInt_7'Size: "
31 & Aligned_UInt_7'Size'Image);
32 Put_Line
33 ("Aligned_UInt_7'Object_Size: "
34 & Aligned_UInt_7'Object_Size'Image);
35 Put_Line
36 ("Aligned_V'Alignment: "
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Alignment
MD5: a2fea340559193c293ccaee226de2558
Runtime output
UInt_7'Alignment: 1
UInt_7'Size: 7
UInt_7'Object_Size: 8
V'Alignment: 1
V'Size: 8
Aligned_UInt_7'Alignment: 4
Aligned_UInt_7'Size: 7
Aligned_UInt_7'Object_Size: 32
Aligned_V'Alignment: 4
Aligned_V'Size: 32
UInt_7'Alignment: 1
UInt_7'Size: 7
UInt_7'Object_Size: 8
V'Alignment: 1
V'Size: 8
Aligned_UInt_7'Alignment: 4
Aligned_UInt_7'Size: 7
Aligned_UInt_7'Object_Size: 32
Aligned_V'Alignment: 4
Aligned_V'Size: 32
In this example, we're reusing the UInt_7 type that we've already been using in previous
examples. Because we haven't specified any alignment for the UInt_7 type, it has an
alignment of 1 storage unit (or 8 bits). However, in the declaration of the Aligned_UInt_7
type, we're using the Alignment aspect to request an alignment of 4 storage units (or 32
bits):
When using the Alignment attribute for the Aligned_UInt_7 type, we can confirm that its
alignment is indeed 4 storage units (bytes).
Note that we can use the Alignment attribute for both data types and objects — in the code
above, we're using UInt_7'Alignment and V'Alignment, for example.
Because of the alignment we're specifying for the Aligned_UInt_7 type, its size — indicated
by the Object_Size attribute — is 32 bits instead of 8 bits as for the UInt_7 type.
Note that you can also retrieve the alignment associated with a class using
S'Class'Alignment. For example:
3 procedure Show_Class_Alignment is
4
18 begin
19 Put_Line ("1D_Point'Alignment: "
20 & Point_1D'Alignment'Image);
21 Put_Line ("1D_Point'Class'Alignment: "
22 & Point_1D'Class'Alignment'Image);
23 Put_Line ("2D_Point'Alignment: "
24 & Point_2D'Alignment'Image);
25 Put_Line ("2D_Point'Class'Alignment: "
26 & Point_2D'Class'Alignment'Image);
27 Put_Line ("3D_Point'Alignment: "
28 & Point_3D'Alignment'Image);
29 Put_Line ("3D_Point'Class'Alignment: "
30 & Point_3D'Class'Alignment'Image);
31 end Show_Class_Alignment;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Class_Alignment
MD5: 4eb28d59439d1eb86cd23fb08acd3493
Runtime output
1D_Point'Alignment: 8
1D_Point'Class'Alignment: 8
2D_Point'Alignment: 16
2D_Point'Class'Alignment: 16
3D_Point'Alignment: 16
3D_Point'Class'Alignment: 16
Overlapping Storage
– A typical example is when both objects are exactly the same, so they obviously
share the same storage. For example, for array A, A'Has_Same_Storage (A) is
always True.
• the Overlaps_Storage attribute indicates whether two objects have at least one bit
in common.
– Note that, if two objects have the same storage, this implies that their storage
also overlaps. In other words, A'Has_Same_Storage (B) = True implies that
A'Overlaps_Storage (B) = True.
Let's look at this example:
3 type Int_Array is
4 array (Positive range <>) of Integer;
5
12 end Int_Array_Processing;
46 end Int_Array_Processing;
4 procedure Main is
5 A : Int_Array (1 .. 20) := (others => 3);
6 B : Int_Array (1 .. 20) := (others => 4);
7 begin
8 Process (A, A);
9 -- In-place processing:
10 -- sharing the exact same storage
11
MD5: 0f599163c6f24c3ef46ec6577b501c21
Build output
int_array_processing.adb:29:24: warning: "Y" may be referenced before it has a␣
↪value [enabled by default]
Runtime output
==== PROCESS ====
Info: X and Y have the same storage.
Info: X and Y overlap.
In-place processing...
Packed Representation
As we've seen previously, the minimum number of bits required to represent a data type
might be less than the actual number of bits used to store an object of that same type.
We've seen an example where UInt_7'Size was 7 bits, while UInt_7'Object_Size was 8
bits. The most extreme case is the one for the Boolean type: in this case, Boolean'Size is
1 bit, while Boolean'Object_Size might be 8 bits (or even more on certain architectures).
In such cases, we have 7 (or more) unused bits in memory for each object of Boolean type.
In other words, we're wasting memory. On the other hand, we're gaining speed of access
because we can directly access each element without having to first change its internal
representation back and forth. We'll come back to this point later.
The situation is even worse when implementing bit-fields, which can be declared as an array
of Boolean components. For example:
3 type Flags is
4 array (Positive range <>) of Boolean;
5
6 end Flag_Definitions;
4 procedure Show_Flags is
5 Flags_1 : Flags (1 .. 8);
6 begin
7 Put_Line ("Boolean'Size: "
8 & Boolean'Size'Image);
9 Put_Line ("Boolean'Object_Size: "
10 & Boolean'Object_Size'Image);
11 Put_Line ("Flags_1'Size: "
12 & Flags_1'Size'Image);
13 Put_Line ("Flags_1'Component_Size: "
14 & Flags_1'Component_Size'Image);
15 end Show_Flags;
MD5: 6fd7a913e3c6717e846c2e822c1cbad7
Build output
show_flags.adb:5:04: warning: variable "Flags_1" is read but never assigned [-
↪gnatwv]
Runtime output
Boolean'Size: 1
Boolean'Object_Size: 8
Flags_1'Size: 64
Flags_1'Component_Size: 8
In this example, we're declaring the Flags type as an array of Boolean components. As we
can see in this case, although the size of the Boolean type is just 1 bit, an object of this
type has a size of 8 bits. Consequently, each component of the Flags type has a size of 8
bits. Moreover, an array with 8 components of Boolean type — such as the Flags_1 array
— has a size of 64 bits.
Therefore, having a way to compact the representation — so that we can store multiple
objects without wasting storage space — may help us improving memory usage. This is
actually possible by using the Pack aspect. For example, we could extend the previous
example and declare a Packed_Flags type that makes use of this aspect:
3 type Flags is
4 array (Positive range <>) of Boolean;
5
6 type Packed_Flags is
7 array (Positive range <>) of Boolean
8 with Pack;
9
10 end Flag_Definitions;
4 procedure Show_Packed_Flags is
5 Flags_1 : Flags (1 .. 8);
6 Flags_2 : Packed_Flags (1 .. 8);
7 begin
8 Put_Line ("Boolean'Size: "
9 & Boolean'Size'Image);
10 Put_Line ("Boolean'Object_Size: "
11 & Boolean'Object_Size'Image);
12 Put_Line ("Flags_1'Size: "
13 & Flags_1'Size'Image);
14 Put_Line ("Flags_1'Component_Size: "
15 & Flags_1'Component_Size'Image);
16 Put_Line ("Flags_2'Size: "
17 & Flags_2'Size'Image);
18 Put_Line ("Flags_2'Component_Size: "
19 & Flags_2'Component_Size'Image);
20 end Show_Packed_Flags;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Packed_Flags
MD5: c71cf68dc8bc41d0df2a5e3eb61b51fd
Build output
Runtime output
Boolean'Size: 1
Boolean'Object_Size: 8
Flags_1'Size: 64
Flags_1'Component_Size: 8
Flags_2'Size: 8
Flags_2'Component_Size: 1
Boolean'Size: 1
Boolean'Object_Size: 8
Flags_1'Size: 64
Flags_1'Component_Size: 8
Flags_2'Size: 8
Flags_2'Component_Size: 1
In this example, we're declaring the Flags_2 array of Packed_Flags type. Its size is 8 bits —
instead of the 64 bits required for the Flags_1 array. Because the array type Packed_Flags
is packed, we can now effectively use this type to store an object of Boolean type using
just 1 bit of the memory, as indicated by the Flags_2'Component_Size attribute.
In many cases, we need to convert between a normal representation (such as the one used
for the Flags_1 array above) to a packed representation (such as the one for the Flags_2
array). In many programming languages, this conversion may require writing custom code
with manual bit-shifting and bit-masking to get the proper target representation. In Ada,
however, we just need to indicate the actual type conversion, and the compiler takes care
of generating code containing bit-shifting and bit-masking to performs the type conversion.
Let's modify the previous example and introduce this type conversion:
3 type Flags is
4 array (Positive range <>) of Boolean;
5
6 type Packed_Flags is
7 array (Positive range <>) of Boolean
8 with Pack;
9
14 end Flag_Definitions;
4 procedure Show_Flag_Conversion is
5 Flags_1 : Flags (1 .. 8);
6 Flags_2 : Packed_Flags (1 .. 8);
7 begin
8 Flags_1 := Default_Flags;
9 Flags_2 := Packed_Flags (Flags_1);
10
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Data_Representation.
↪Flag_Conversion
MD5: faff2079f6779097b6e0f7cd6cd48612
Runtime output
1: TRUE, TRUE
2: TRUE, TRUE
3: FALSE, FALSE
4: TRUE, TRUE
5: FALSE, FALSE
6: FALSE, FALSE
7: TRUE, TRUE
8: TRUE, TRUE
In this extended example, we're now declaring Default_Flags as an array of constant flags,
which we use to initialize Flags_1.
The actual conversion happens with Flags_2 := Packed_Flags (Flags_1). Here, the type
conversion Packed_Flags() indicates that we're converting from the normal representation
(used for the Flags type) to the packed representation (used for Packed_Flags type). We
don't need to write more code than that to perform the correct type conversion.
Also, by using the same strategy, we could read information from a packed representation.
For example:
In this case, we use Flags() to convert from a packed representation to the normal repre-
sentation.
We elaborate on the topic of converting between data representations in the section on
changing data representation (page 376).
Trade-offs
As indicated previously, when we're using a packed representation (vs. using a standard
unpacked representation), we're trading off speed of access for less memory consumption.
The following table summarizes this:
On one hand, we have better memory usage when we apply packed representations be-
cause we may save many bits for each object. On the other hand, there's a cost associated
with accessing those packed objects because they need to be unpacked before we can
actually access them. In fact, the compiler generates code — using bit-shifting and bit-
masking — that converts a packed representation into an unpacked representation, which
we can then access. Also, when storing a packed object, the compiler generates code that
converts the unpacked representation of the object into the packed representation.
This packing and unpacking mechanism has a performance cost associated with it, which
results in less speed of access for packed objects. As usual in those circumstances, be-
fore using packed representation, we should assess whether memory constraints are more
important than speed in our target architecture.
In this section, we discuss how to use record representation clauses to specify how a record
is represented in memory. Our goal is to provide a brief introduction into the topic. If you're
interested in more details, you can find a thorough discussion about record representation
clauses in the Introduction to Embedded Systems Programming (page 1117) course.
Let's start with the simple approach of declaring a record type without providing further
information. In this case, we're basically asking the compiler to select a reasonable repre-
sentation for that record in the memory of our target architecture.
Let's see a simple example:
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record;
7
8 end P;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Rep_Clauses_1
MD5: 88171257118810bb7e02cea60ffb1ad9
Considering a typical 64-bit PC architecture with 8-bit storage units, and Integer defined
as a 32-bit type, we get this memory representation:
position 0 1 2 3 4 5 6 7
component A B
Each storage unit is a position in memory. In the graph above, the numbers on the top (0,
1, 2, ...) represent those positions for record R.
In addition, we can show the bits that are used for components A and B:
position 0 1 2 3 4 5 6 7
bits #0 .. 7 #8 .. #15 #16 .. #23 #24 .. #31 #0 .. 7 #8 .. #15 #16 .. #23 #24 .. #31
component A B
The memory representation we see in the graph above can be described in Ada using rep-
resentation clauses, as you can see in the code starting at the for R use record line in the
code example below — we'll discuss the syntax and further details right after this example.
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record;
7
16 end P;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Rep_Clauses_2
MD5: b6be86ae7e1a5c2e7d981fe37bad49ed
Here, we're specifying that the A component is stored in the bits #0 up to #31 starting
at position #0. Note that the position itself doesn't represent an absolute address in the
device's memory; instead, it's relative to the memory space reserved for that record. The
B component has the same 32-bit range, but starts at position #4.
This is a generalized view of the syntax:
We can retrieve information about the start position, and the first and last bits of a compo-
nent by using the storage place attributes:
• Position, which retrieves the start position of a component;
• First_Bit, which retrieves the first bit of a component;
• Last_Bit, which retrieves the last bit of a component.
Note, however, that these attributes can only be used with actual records, and not with
record types.
We can revisit the previous example and verify how the compiler represents the R type in
memory:
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record;
7
8 end P;
4 with P; use P;
5
6 procedure Show_Storage is
7 R1 : R;
8 begin
9 Put_Line ("R'Size: "
10 & R'Size'Image);
11 Put_Line ("R'Object_Size: "
12 & R'Object_Size'Image);
13 New_Line;
14
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Storage_Place_Attributes
MD5: 05a402585ce71eb47cf972e68c02835e
Build output
Runtime output
R'Size: 64
R'Object_Size: 64
System.Storage_Unit: 8
R1.A'Position : 0
R1.A'First_Bit : 0
R1.A'Last_Bit : 31
R1.B'Position : 4
R1.B'First_Bit : 0
R1.B'Last_Bit : 31
First of all, we see that the size of the R type is 64 bits, which can be explained by those
two 32-bit integer components. Then, we see that components A and B start at positions
#0 and #4, and each one makes use of bits in the range from #0 to #31. This matches the
graph we've seen above.
We can use representation clauses to change the way the compiler handles memory for
a record type. For example, let's say we want to have an empty storage unit between
components A and B. We can use a representation clause where we specify that component
B starts at position #5 instead of #4, leaving an empty byte after component A and before
component B:
position 0 1 2 3 4 5 6 7 8
bits #0 .. 7 #8 .. #15 #16 .. #23 #24 .. #31 #0 .. 7 #8 .. #15 #16 .. #23 #24 .. #31
component A B
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record;
7
13 end P;
3 with P; use P;
4
5 procedure Show_Empty_Byte is
6 begin
7 Put_Line ("R'Size: "
8 & R'Size'Image);
9 Put_Line ("R'Object_Size: "
10 & R'Object_Size'Image);
11 end Show_Empty_Byte;
MD5: c616e534e95a06f2e8b3052a3e8a9aab
Runtime output
R'Size: 72
R'Object_Size: 96
When running the application above, we see that, due to the extra byte in the record repre-
sentation, the sizes increase. On a typical 64-bit PC, R'Size is now 76 bits, which reflects
the additional eight bits that we introduced between components A and B. Depending on
the target architecture, you may also see that R'Object_Size is now 96 bits, which is the
size the compiler selects as the most appropriate for this record type. As we've mentioned
in the previous section, we can use aspects to request a specific size to the compiler. In
this case, we could use the Object_Size aspect:
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record
7 with Object_Size => 72;
8
14 end P;
3 with P; use P;
4
5 procedure Show_Empty_Byte is
6 begin
7 Put_Line ("R'Size: "
8 & R'Size'Image);
9 Put_Line ("R'Object_Size: "
10 & R'Object_Size'Image);
11 end Show_Empty_Byte;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Rep_Clauses_Empty_Byte
MD5: 9d7bae2b2aabeda4bc03752544cee9b9
Runtime output
R'Size: 72
R'Object_Size: 72
If the code compiles, R'Size and R'Object_Size should now have the same value.
In some cases, you might want to modify the memory representation of a record without
impacting existing code. For example, you might want to use a record type that was de-
clared in a package that you're not allowed to change. Also, you would like to modify its
memory representation in your application. A nice strategy is to derive a type and use a
representation clause for the derived type.
We can apply this strategy on our previous example. Let's say we would like to use record
type R from package P in our application, but we're not allowed to modify package P — or
the record type, for that matter. In this case, we could simply derive R as R_New and use a
representation clause for R_New. This is exactly what we do in the specification of the child
package P.Rep:
3 type R is record
4 A : Integer;
5 B : Integer;
6 end record;
7
8 end P;
11 end P.Rep;
3 with P; use P;
4 with P.Rep; use P.Rep;
5
6 procedure Show_Empty_Byte is
7 begin
8 Put_Line ("R'Size: "
9 & R'Size'Image);
10 Put_Line ("R'Object_Size: "
11 & R'Object_Size'Image);
12
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Derived_Rep_Clauses_Empty_Byte
MD5: 3a1e0837f8bd8250f20fc7b274b869d5
Runtime output
R'Size: 64
R'Object_Size: 64
R_New'Size: 72
R_New'Object_Size: 72
When running this example, we see that the R type retains the memory representation
selected by the compiler for the target architecture, while the R_New has the memory rep-
resentation that we specified.
bit 0 1 2 3 4 5 6 7
component S (reserved) Error V1
Here, S is the current status, Error is a flag, and V1 contains a value. Due to the fact that
we can use representation clauses to describe individual bits of a register as records, the
implementation becomes as simple as this:
20 end P;
3 with P; use P;
4
5 procedure Show_Simple_Reg is
6 begin
7 Put_Line ("Simple_Reg'Size: "
8 & Simple_Reg'Size'Image);
9 Put_Line ("Simple_Reg'Object_Size: "
10 & Simple_Reg'Object_Size'Image);
11 end Show_Simple_Reg;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Rep_Clauses_Simple_Reg
MD5: cbac444336572460062f922767c226a5
Runtime output
Simple_Reg'Size: 8
Simple_Reg'Object_Size: 8
As we can see in the declaration of the Simple_Reg type, each component represents a field
from our register, and it has a fixed location (which matches the register representation we
see in the graph above). Any operation on the register is as simple as accessing the record
component. For example:
3 with P; use P;
4
5 procedure Show_Simple_Reg is
6 Default : constant Simple_Reg :=
7 (S => Ready,
8 Error => False,
9 V1 => 0);
10
11 R : Simple_Reg := Default;
12 begin
13 Put_Line ("R.S: " & R.S'Image);
14
15 R.V1 := 4;
16
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Record_Representation_
↪Storage_Clauses.Rep_Clauses_Simple_Reg
MD5: e442396e43d6609c1c837165bbc21641
Runtime output
R.S: READY
R.V1: 4
As we can see in the example, to retrieve the current status of the register, we just have
to write R.S. To update the V1 field of the register with the value 4, we just have to write
R.V1 := 4. No extra code — such as bit-masking or bit-shifting — is needed here.
In other languages
Some programming languages require that developers use complicated, error-prone ap-
proaches — which may include manually bit-shifting and bit-masking variables — to retrieve
information from or store information to individual bits or registers. In Ada, however, this is
efficiently handled by the compiler, so that developers only need to correctly describe the
register mapping using representation clauses.
Note: This section was originally written by Robert Dewar and published as Gem #27:
Changing Data Representation40 and Gem #2841 .
A powerful feature of Ada is the ability to specify the exact data layout. This is particularly
important when you have an external device or program that requires a very specific format.
Some examples are:
15 end Communication;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.Com_Packet
MD5: cbd7f5547c5b0458853ac21d03aa41f8
Build output
which lays out the fields of a record, and in the case of Val, forces a biased representation
in which all zero bits represents 100. Another example is:
8 end Array_Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.Array_Rep
MD5: 7eb17fc2cd415acb7c53a363fa336807
40 https://www.adacore.com/gems/gem-27
41 https://www.adacore.com/gems/gem-28
which forces the components to take only 3 bits, crossing byte boundaries as needed. A
final example is:
8 end Enumeration_Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.Enum_Rep
MD5: 3c3e9f4ae11e9bb2482588d27ba43c30
which allows specified values for an enumeration type, instead of the efficient default values
of 0, 1, 2.
In all these cases, we might use these representation clauses to match external speci-
fications, which can be very useful. The disadvantage of such layouts is that they are
inefficient, and accessing individual components, or, in the case of the enumeration type,
looping through the values can increase space and time requirements for the program code.
One approach that is often effective is to read or write the data in question in this specified
form, but internally in the program represent the data in the normal default layout, allowing
efficient access, and do all internal computations with this more efficient form.
To follow this approach, you will need to convert between the efficient format and the spec-
ified format. Ada provides a very convenient method for doing this, as described in RM 13.6
"Change of Representation"42 .
The idea is to use type derivation, where one type has the specified format and the other
has the normal default format. For instance for the array case above, we would write:
9 end Array_Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.Array_Rep
MD5: d4e90f6ef8ff81771980771356eab235
Now we read and write the data using the External_Arr type. When we want to convert
to the efficient form, Arr, we simply use a type conversion.
42 http://www.ada-auth.org/standards/22rm/html/RM-13-6.html
4 procedure Using_Array_For_IO is
5 Input_Data : External_Arr;
6 Work_Data : Arr;
7 Output_Data : External_Arr;
8 begin
9 -- (read data into Input_Data)
10
20 end Using_Array_For_IO;
MD5: 88efe4b8a7f07e0c32f11131d6eafbc1
Build output
using_array_for_io.adb:5:04: warning: variable "Input_Data" is read but never␣
↪assigned [-gnatwv]
Using this approach, the quite complex task of copying all the data of the array from one
form to another, with all the necessary masking and shift operations, is completely auto-
matic.
Similar code can be used in the record and enumeration type cases. It is even possible to
specify two different representations for the two types, and convert from one form to the
other, as in:
13 end Enumeration_Representation;
There are two restrictions that must be kept in mind when using this feature. First, you have
to use a derived type. You can't put representation clauses on subtypes, which means that
the conversion must always be explicit. Second, there is a rule RM 13.143 (10) that restricts
the placement of interesting representation clauses:
10 For an untagged derived type, no type-related representation items are al-
lowed if the parent type is a by-reference type, or has any user-defined primitive
subprograms.
All the representation clauses that are interesting from the point of view of change of rep-
resentation are "type related", so for example, the following sequence would be illegal:
11 end Array_Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.Array_Rep_2
MD5: 70201932d40e3fb356bc1d8ab188f2df
Build output
Why these restrictions? Well, the answer is a little complex, and has to do with efficiency
considerations, which we will address below.
Restrictions
In the previous subsection, we discussed the use of derived types and representation
clauses to achieve automatic change of representation. More accurately, this feature is
not completely automatic, since it requires you to write an explicit conversion. In fact there
is a principle behind the design here which says that a change of representation should
never occur implicitly behind the back of the programmer without such an explicit request
by means of a type conversion.
The reason for that is that the change of representation operation can be very expensive,
since in general it can require component by component copying, changing the represen-
tation on each component.
43 http://www.ada-auth.org/standards/22rm/html/RM-13-1.html
Let's have a look at the -gnatG expanded code to see what is hidden under the covers here.
For example, the conversion Arr (Input_Data) from the previous example generates the
following expanded code:
B26b : declare
[subtype p__TarrD1 is integer range 1 .. 16]
R25b : p__TarrD1 := 1;
begin
for L24b in 1 .. 16 loop
[subtype p__arr___XP3 is
system__unsigned_types__long_long_unsigned range 0 ..
16#FFFF_FFFF_FFFF#]
work_data := p__arr___XP3!((work_data and not shift_left!(
16#7#, 3 * (integer(L24b - 1)))) or shift_left!(p__arr___XP3!
(input_data (R25b)), 3 * (integer(L24b - 1))));
R25b := p__TarrD1'succ(R25b);
end loop;
end B26b;
That's pretty horrible! In fact, we could have simplified it for this section, but we have left
it in its original form, so that you can see why it is nice to let the compiler generate all this
stuff so you don't have to worry about it yourself.
Given that the conversion can be pretty inefficient, you don't want to convert backwards
and forwards more than you have to, and the whole approach is only worthwhile if we'll be
doing extensive computations involving the value.
The expense of the conversion explains two aspects of this feature that are not obvious.
First, why do we require derived types instead of just allowing subtypes to have different
representations, avoiding the need for an explicit conversion?
The answer is precisely that the conversions are expensive, and you don't want them hap-
pening behind your back. So if you write the explicit conversion, you get all the gobbledy-
gook listed above, but you can be sure that this never happens unless you explicitly ask for
it.
This also explains the restriction we mentioned in previous subsection from RM 13.144 (10):
10 For an untagged derived type, no type-related representation items are al-
lowed if the parent type is a by-reference type, or has any user-defined primitive
subprograms.
It turns out this restriction is all about avoiding implicit changes of representation. Let's
have a look at how type derivation works when there are primitive subprograms defined at
the point of derivation. Consider this example:
10 end My_Ints;
44 http://www.ada-auth.org/standards/22rm/html/RM-13-1.html
8 end My_Ints;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.My_Int
MD5: a29401698307998288f02b349d04d1d2
Now when we do the type derivation, we inherit the function Odd for My_Int_2. But where
does this function come from? We haven't written it explicitly, so the compiler somehow
materializes this new implicit function. How does it do that?
We might think that a complete new function is created including a body in which My_Int_2
replaces My_Int_1, but that would be impractical and expensive. The actual mechanism
avoids the need to do this by use of implicit type conversions. Suppose after the above
declarations, we write:
3 procedure Using_My_Int is
4 Var : My_Int_2;
5 begin
6
13 end Using_My_Int;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Changing_Data_
↪Representation.My_Int
MD5: f68272d55e68687b7102885313c7831b
Build output
3 procedure Using_My_Int is
4 Var : My_Int_2;
5 begin
(continues on next page)
14 end Using_My_Int;
MD5: b3d0053c61412a2b985cd580b645e048
Build output
using_my_int.adb:4:04: warning: variable "Var" is read but never assigned [-gnatwv]
This implicit conversion is a nice trick, it means that we can get the effect of inheriting a
new operation without actually having to create it. Furthermore, in a case like this, the type
conversion generates no code, since My_Int_1 and My_Int_2 have the same representa-
tion.
But the whole point is that they might not have the same representation if one of them
had a representation clause that made the representations different, and in this case the
implicit conversion inserted by the compiler could be expensive, perhaps generating the
junk we quoted above for the Arr case. Since we never want that to happen implicitly,
there is a rule to prevent it.
The business of forbidding by-reference types (which includes all tagged types) is also
driven by this consideration. If the representations are the same, it is fine to pass by refer-
ence, even in the presence of the conversion, but if there was a change of representation,
it would force a copy, which would violate the by-reference requirement.
So to summarize this section, on the one hand Ada gives you a very convenient way to trig-
ger these complex conversions between different representations. On the other hand, Ada
guarantees that you never get these potentially expensive conversions happening unless
you explicitly ask for them.
When receiving data from external sources, we're subjected to problems such as trans-
mission errors. If not handled properly, erroneous data can lead to major issues in an
application.
One of those issues originates from the fact that transmission errors might lead to invalid
information stored in memory. When proper checks are active, using invalid information is
detected at runtime and an exception is raised at this point, which might then be handled
by the application.
Instead of relying on exception handling, however, we could instead ensure that the in-
formation we're about to use is valid. We can do this by using the Valid attribute. For
example, if we have a variable Var, we can verify that the value stored in Var is valid by
writing Var'Valid, which returns a Boolean value. Therefore, if the value of Var isn't valid,
Var'Valid returns False, so we can have code that handles this situation before we ac-
tually make use of Var. In other words, instead of handling a potential exception in other
parts of the application, we can proactively verify that input information is correct and avoid
that an exception is raised.
9 F : File_Type;
10 begin
11 Create (F, Out_File, File_Name);
12 Write (F, 1);
13 Write (F, 2);
14 Write (F, 4);
15 Write (F, 3);
16 Write (F, 2);
17 Write (F, 10);
18 Close (F);
19 end Create_Test_File;
3 package States is
4
15 procedure Read_Display_States
16 (File_Name : String);
17
18 end States;
5 procedure Read_Display_States
(continues on next page)
10 F : State_Sequential_IO.File_Type;
11 S : State;
12
24 begin
25 Open (F, In_File, File_Name);
26
32 Close (F);
33 end Read_Display_States;
34
35 end States;
4 procedure Show_States_From_File is
5 File_Name : constant String := "data.bin";
6 begin
7 Create_Test_File (File_Name);
8 Read_Display_States (File_Name);
9 end Show_States_From_File;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Valid_Attribute.Valid_
↪States
MD5: f7af2946ebe663932494448a0d3d3020
Runtime output
OFF
ON
WAITING
Invalid value detected!
ON
Invalid value detected!
Let's start our discussion on this example with the States package, which contains the
declaration of the State type. This type is a simple enumeration containing three states:
Off, On and Waiting. We're assigning specific integer values for this type by declaring an
enumeration representation clause. Note that we're using the Size aspect to request that
objects of this type have the same size as the Integer type. This becomes important later
on when parsing data from the file.
In the Create_Test_File procedure, we create a file containing integer values, which is
parsed later by the Read_Display_States procedure. The Create_Test_File procedure
doesn't contain any reference to the State type, so we're not constrained to just writing
information that is valid for this type. On the contrary, this procedure makes use of the
Integer type, so we can write any integer value to the file. We use this strategy to write
both valid and invalid values of State to the file. This allows us to simulate an environment
where transmission errors occur.
We call the Read_Display_States procedure to read information from the file and display
each state stored in the file. In the main loop of this procedure, we call Read to read a
state from the file and store it in the S variable. We then call the nested Display_State
procedure to display the actual state stored in S. The most important line of code in the
Display_State procedure is the one that uses the Valid attribute:
if S'Valid then
In this line, we're verifying that the S variable contains a valid state before displaying the
actual information from S. If the value stored in S isn't valid, we can handle the issue accord-
ingly. In this case, we're simply displaying a message indicating that an invalid value was
detected. If we didn't have this check, the Constraint_Error exception would be raised
when trying to use invalid data stored in S — this would happen, for example, after reading
the integer value 3 from the input file.
In summary, using the Valid attribute is a good strategy we can employ when we know
that information stored in memory might be corrupted.
We've introduced variant records back in the Introduction to Ada course (page 104). In
simple terms, a variant record is a record with discriminants that allows for changing its
structure. Basically, it's a record containing a case.
The State_Or_Integer declaration in the States package below is an example of a variant
record:
18 procedure Display_State_Value
19 (V : State_Or_Integer);
20
21 end States;
5 procedure Display_State_Value
6 (V : State_Or_Integer)
7 is
8 begin
9 Put_Line ("State: " & V.S'Image);
10 Put_Line ("Value: " & V.I'Image);
11 end Display_State_Value;
12
13 end States;
MD5: fa72f52a4396a2e66931ff6932c567fc
As mentioned in the previous course, if you try to access a component that is not valid for
your record, a Constraint_Error exception is raised. For example, in the implementation
of the Display_State_Value procedure, we're trying to retrieve the value of the integer
component (I) of the V record. When calling this procedure, the Constraint_Error ex-
ception is raised as expected because Use_Enum is set to True, so that the I component is
invalid — only the S component is valid in this case.
3 procedure Show_Variant_Rec_Error is
4 V : State_Or_Integer (Use_Enum => True);
5 begin
6 V.S := On;
7 Display_State_Value (V);
8 end Show_Variant_Rec_Error;
MD5: b8cf215dd55bfdec6950df35c7bc19b9
Runtime output
State: ON
In addition to not being able to read the value of a component that isn't valid, assigning a
value to a component that isn't valid also raises an exception at runtime. In this example,
we cannot assign to V.I:
3 procedure Show_Variant_Rec_Error is
4 V : State_Or_Integer (Use_Enum => True);
5 begin
6 V.I := 4;
7 -- Error: V.I cannot be accessed because
8 -- Use_Enum is set to True.
9 end Show_Variant_Rec_Error;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.State_
↪Or_Integer
MD5: 985a84faccc3d590ac767e914bea0c1d
Build output
Runtime output
We may circumvent this limitation by using the Unchecked_Union aspect. For example, we
can derive a new type from State_Or_Integer and use this aspect in its declaration. We
do this in the declaration of the Unchecked_State_Or_Integer type below.
18 type Unchecked_State_Or_Integer
19 (Use_Enum : Boolean) is new
20 State_Or_Integer (Use_Enum)
(continues on next page)
23 procedure Display_State_Value
24 (V : Unchecked_State_Or_Integer);
25
26 end States;
5 procedure Display_State_Value
6 (V : Unchecked_State_Or_Integer)
7 is
8 begin
9 Put_Line ("State: " & V.S'Image);
10 Put_Line ("Value: " & V.I'Image);
11 end Display_State_Value;
12
13 end States;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.
↪Unchecked_State_Or_Integer
MD5: e97271a24aab23d2db450308401667ac
Because we now use the Unchecked_State_Or_Integer type for the input parameter of the
Display_State_Value procedure, no exception is raised at runtime, as both components
are now accessible. For example:
3 procedure Show_Unchecked_Union is
4 V : State_Or_Integer (Use_Enum => True);
5 begin
6 V.S := On;
7 Display_State_Value
8 (Unchecked_State_Or_Integer (V));
9 end Show_Unchecked_Union;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.
↪Unchecked_State_Or_Integer
MD5: 331cc1ab6709ab7e0062d64c55a75a6c
Runtime output
State: ON
Value: 2
Note that, in the call to the Display_State_Value procedure, we first need to convert the
V argument from the State_Or_Integer to the Unchecked_State_Or_Integer type.
Also, we can assign to any of the components of a record that has the Unchecked_Union
aspect. In our example, we can now assign to both the S and the I components of the V
record:
3 procedure Show_Unchecked_Union is
4 V : Unchecked_State_Or_Integer
5 (Use_Enum => True);
6 begin
7 V := (Use_Enum => True, S => On);
8 Display_State_Value (V);
9
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.
↪Unchecked_State_Or_Integer
MD5: bb472e91c5e7b7e63d6246dbcf5226a0
Runtime output
State: ON
Value: 2
State: WAITING
Value: 4
In the example above, we're use an aggregate in the assignments to V. By doing so, we
avoid that Use_Enum is set to the wrong component. For example:
3 procedure Show_Unchecked_Union is
4 V : Unchecked_State_Or_Integer
5 (Use_Enum => True);
6 begin
7 V.S := On;
8 Display_State_Value (V);
9
10 V.I := 4;
11 -- Error: cannot directly assign to V.I,
12 -- as Use_Enum is set to True.
13
14 Display_State_Value (V);
15 end Show_Unchecked_Union;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.
↪Unchecked_State_Or_Integer
MD5: 74ac11a3effdafd3959fface295a86da
Build output
Runtime output
State: ON
Value: 2
Here, even though the record has the Unchecked_Union attribute, we cannot directly assign
to the I component because Use_Enum is set to True, so only the S is accessible. We can,
however, read its value, as we do in the Display_State_Value procedure.
Be aware that, due to the fact the union is not checked, we might write invalid data to the
record. In the example below, we initialize the I component with 3, which is a valid integer
value, but results in an invalid value for the S component, as the value 3 cannot be mapped
to the representation of the State type.
3 procedure Show_Unchecked_Union is
4 V : Unchecked_State_Or_Integer
5 (Use_Enum => True);
6 begin
7 V := (Use_Enum => False, I => 3);
8 Display_State_Value (V);
9 end Show_Unchecked_Union;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Unchecked_Union.
↪Unchecked_State_Or_Integer
MD5: f63e64df137cfc3c29e41f784306f0e4
Runtime output
To mitigate this problem, we could use the Valid attribute — discussed in the previous
section — for the S component before trying to use its value in the implementation of the
Display_State_Value procedure:
5 procedure Display_State_Value
6 (V : Unchecked_State_Or_Integer)
7 is
8 begin
9 if V.S'Valid then
10 Put_Line ("State: " & V.S'Image);
11 else
12 Put_Line ("State: <invalid>");
13 end if;
14 Put_Line ("Value: " & V.I'Image);
15 end Display_State_Value;
16
17 end States;
3 procedure Show_Unchecked_Union is
4 V : Unchecked_State_Or_Integer
5 (Use_Enum => True);
6 begin
7 V := (Use_Enum => False, I => 3);
8 Display_State_Value (V);
9 end Show_Unchecked_Union;
However, in general, you should avoid using the Unchecked_Union aspect due to the poten-
tial issues you might introduce into your application. In the majority of the cases, you don't
need it at all — except for special cases such as when interfacing with C code that makes
use of union types or solving very specific problems when doing low-level programming.
Ada has built-in support for handling both volatile and atomic data. Let's start by discussing
volatile objects.
Volatile
A volatile48 object can be described as an object in memory whose value may change
between two consecutive memory accesses of a process A — even if process A itself hasn't
changed the value. This situation may arise when an object in memory is being shared by
multiple threads. For example, a thread B may modify the value of that object between two
read accesses of a thread A. Another typical example is the one of memory-mapped I/O49 ,
where the hardware might be constantly changing the value of an object in memory.
Because the value of a volatile object may be constantly changing, a compiler cannot gen-
erate code to store the value of that object in a register and then use the value from the
register in subsequent operations. Storing into a register is avoided because, if the value
is stored there, it would be outdated if another process had changed the volatile object in
the meantime. Instead, the compiler generates code in such a way that the process must
read the value of the volatile object from memory for each access.
Let's look at a simple example:
46 http://www.ada-auth.org/standards/22rm/html/RM-B-3-3.html
47 http://www.ada-auth.org/standards/22rm/html/RM-C-6.html
48 https://en.wikipedia.org/wiki/Volatile_(computer_programming)
49 https://en.wikipedia.org/wiki/Memory-mapped_I/O
3 procedure Show_Volatile_Object is
4 Val : Long_Float with Volatile;
5 begin
6 Val := 0.0;
7 for I in 0 .. 999 loop
8 Val := Val + 2.0 * Long_Float (I);
9 end loop;
10
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Volatile_Object_Ada
MD5: aa1e276e64e69813bfc3e3ef39f3dd47
Runtime output
Val: 9.99000000000000E+05
In this example, Val has the Volatile aspect, which makes the object volatile. We can
also use the Volatile aspect in type declarations. For example:
6 end Shared_Var_Types;
4 procedure Show_Volatile_Type is
5 Val : Volatile_Long_Float;
6 begin
7 Val := 0.0;
8 for I in 0 .. 999 loop
9 Val := Val + 2.0 * Volatile_Long_Float (I);
10 end loop;
11
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Volatile_Type
MD5: 0d31156d47b2edcfb94debd016c8bb87
Runtime output
Val: 9.99000000000000E+05
3 procedure Show_Volatile_Array_Components is
4 Arr : array (1 .. 2) of Long_Float
5 with Volatile_Components;
6 begin
7 Arr := (others => 0.0);
8
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Volatile_Array_Components
MD5: 05b3ee20f08c5a85f5872727a61c148d
Runtime output
Note that it's possible to use the Volatile aspect for the array declaration as well:
3 private
4 Arr : array (1 .. 2) of Long_Float
5 with Volatile;
6
7 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Volatile_Array
MD5: c9b7b9f94f1fac295753c7e7b9426fb2
Note that, if the Volatile aspect is specified for an object, then the Volatile_Components
aspect is also specified automatically — if it makes sense in the context, of course. In the
example above, even though Volatile_Components isn't specified in the declaration of the
Arr array , it's automatically set as well.
Independent
When you write code to access a single object in memory, you might actually be accessing
multiple objects at once. For example, when you declare types that make use of represen-
tation clauses — as we've seen in previous sections —, you might be accessing multiple
objects that are grouped together in a single storage unit. For example, if you have compo-
nents A and B stored in the same storage unit, you cannot update A without actually writing
(the same value) to B. Those objects aren't independently addressable because, in order to
access one of them, we have to actually address multiple objects at once.
When an object is independently addressable, we call it an independent object. In this
case, we make sure that, when accessing that object, we won't be simultaneously accessing
another object. As a consequence, this feature limits the way objects can be represented
in memory, as we'll see next.
To indicate that an object is independent, we use the Independent aspect:
5 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Object
MD5: d90fef37584ca8802b8a3e3858c0095b
11 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Type
MD5: 7bcbee5b73067149b14c4b1b061f803c
In this example, we're declaring the Independent_Boolean type and using it in the decla-
ration of the Flag record type. Let's now derive the Flags type and use a representation
clause for the derived type:
14 end Shared_Var_Types.Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Type
MD5: bb9d5badf33401660e7e20a7cd612dab
Build output
As you can see when trying to compile this example, the representation clause that we
used for Rep_Flags isn't following these limitations:
1. The size of each independent component must be a multiple of a storage unit.
2. The start position of each independent component must be a multiple of a storage
unit.
For example, for architectures that have a storage unit of one byte — such as standard
desktop computers —, this means that the size and the position of independent components
must be a multiple of a byte. Let's correct the issues in the code above by:
• setting the size of each independent component to correspond to Storage_Unit —
using a range between 0 and Storage_Unit - 1 —, and
• setting the start position to zero.
This is the corrected version:
3 package Shared_Var_Types.Representation is
4
12 end Shared_Var_Types.Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Type
MD5: ed57e57cd746698909a4f7ce40a29dfc
Note that the representation that we're now using for Rep_Flags is most likely the repre-
sentation that the compiler would have chosen for this data type. We could, however, have
added an empty storage unit between F1 and F2 — by simply writing F2 at 2 ...:
3 package Shared_Var_Types.Representation is
4
12 end Shared_Var_Types.Representation;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Type
MD5: 71fedf8aac7c19bca1ba3b487efa9b17
As long as we follow the rules for independent objects, we're still allowed to use represen-
tation clauses that don't correspond to the one that the compiler might select.
For arrays, we can use the Independent_Components aspect:
6 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Independent_Components
MD5: b331d0a13adf45624b664839fe4ba42c
We've just seen in a previous example that some representation clauses might not work
with objects and types that have the Independent aspect. The same restrictions apply
when we use the Independent_Components aspect. For example, this aspect prevents that
array components are packed when the Pack aspect is used. Let's discuss the following
erroneous code example:
3 type Flags is
4 array (Positive range <>) of Boolean
5 with Independent_Components, Pack;
6
9 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Packed_Independent_Components
MD5: dbaff4f2559ef8a449dad251f42cddc0
Build output
As expected, this code doesn't compile. Here, we can have either independent compo-
nents, or packed components. We cannot have both at the same time because packed
components aren't independently addressable. The compiler warns us that the Pack as-
pect won't have any effect on independent components. When we use the Size aspect in
the declaration of F, we confirm this limitation. If we remove the Size aspect, however, the
code is compiled successfully because the compiler ignores the Pack aspect and allocates
a larger size for F:
3 type Flags is
4 array (Positive range <>) of Boolean
5 with Independent_Components, Pack;
6
7 end Shared_Var_Types;
6 procedure Show_Flags_Size is
7 F : Flags (1 .. 8);
8 begin
9 Put_Line ("Flags'Size: "
10 & F'Size'Image & " bits");
11 Put_Line ("Flags (1)'Size: "
12 & F (1)'Size'Image & " bits");
13 Put_Line ("# storage units: "
14 & Integer'Image
15 (F'Size /
16 System.Storage_Unit));
17 end Show_Flags_Size;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Packed_Independent_Components
MD5: b96f921b08b1d8207749517f833fc121
Build output
Runtime output
Flags'Size: 64 bits
Flags (1)'Size: 8 bits
# storage units: 8
As you can see in the output of the application, even though we specify the Pack aspect for
the Flags type, the compiler allocates eight storage units, one per each component of the
F array.
Atomic
An atomic object is an object that only accepts atomic reads and updates. The Ada standard
specifies that "for an atomic object (including an atomic component), all reads and updates
of the object as a whole are indivisible." In this case, the compiler must generate Assembly
code in such a way that reads and updates of an atomic object must be done in a single
instruction, so that no other instruction could execute on that same object before the read
or update completes.
In other contexts
Generally, we can say that operations are said to be atomic when they can be completed
without interruptions. This is an important requirement when we're performing operations
on objects in memory that are shared between multiple processes.
This definition of atomicity above is used, for example, when implementing databases.
However, for this section, we're using the term "atomic" differently. Here, it really means
that reads and updates must be performed with a single Assembly instruction.
For example, if we have a 32-bit object composed of four 8-bit bytes, the compiler cannot
generate code to read or update the object using four 8-bit store / load instructions, or even
two 16-bit store / load instructions. In this case, in order to maintain atomicity, the compiler
must generate code using one 32-bit store / load instruction.
Because of this strict definition, we might have objects for which the Atomic aspect cannot
be specified. Lots of machines support integer types that are larger than the native word-
sized integer. For example, a 16-bit machine probably supports both 16-bit and 32-bit
integers, but only 16-bit integer objects can be marked as atomic — or, more generally,
only objects that fit into at most 16 bits.
Atomicity may be important, for example, when dealing with shared hardware registers.
In fact, for certain architectures, the hardware may require that memory-mapped registers
are handled atomically. In Ada, we can use the Atomic aspect to indicate that an object is
atomic. This is how we can use the aspect to declare a shared hardware register:
3 package Shared_Var_Types is
4
5 private
6 R : Integer
7 with Atomic,
8 Address =>
9 System'To_Address (16#FFFF00A0#);
10
11 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Atomic_Object
MD5: 5c2d8e0a9615084c2a15f896c61adaa6
Note that the Address aspect allows for assigning a variable to a specific location in the
memory. In this example, we're using this aspect to specify the address of the memory-
mapped register.
Later on, we talk again about the Address aspect (page 403) and the GNAT-specific Sys-
tem'To_Address attribute (page 404).
In addition to atomic objects, we can declare atomic types — similar to what we've seen
before for volatile objects. For example:
3 package Shared_Var_Types is
4
8 private
9 R : Atomic_Integer
10 with Address =>
11 System'To_Address (16#FFFF00A0#);
12
13 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Atomic_Types
MD5: 009632ba0155d70def8281ba590f3d12
In this example, we're declaring the Atomic_Integer type, which is an atomic type. Objects
of this type — such as R in this example — are automatically atomic.
We can also declare atomic array components:
3 private
(continues on next page)
7 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Atomic_Array_Components
MD5: 7501bdf618621a822d451da8d731ef75
This example shows the declaration of the Arr array, which has atomic components — the
atomicity of its components is indicated by the Atomic_Components aspect.
Note that if an object is atomic, it is also volatile and independent. In other words, these
type declarations are equivalent:
11 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Shared_Variable_
↪Control.Atomic_Volatile_Independent
MD5: 3034c7a07698491f961d9b4fb74f03d8
12 end Shared_Var_Types;
25.2.8 Addresses
In other languages, such as C, the concept of pointers and addresses plays a prominent role.
(In fact, in C, many optimizations rely on the usage of pointer arithmetic.) The concept of
addresses does exist in Ada, but it's mainly reserved for very specific applications, mostly
related to low-level programming. In general, other approaches — such as using access
types — are more than sufficient. (We discuss access types (page 735) in another chapter.
Also, later on in that chapter, we discuss the relation between access types and addresses
(page 849).) In this section, we discuss some details about using addresses in Ada.
We make use of the Address type, which is defined in the System package, to handle ad-
dresses. In contrast to other programming languages (such as C or C++), an address in
Ada isn't an integer value: its definition depends on the compiler implementation, and it's
actually driven directly by the hardware. For now, let's consider it to usually be a private
type — this can be seen as an attempt to achieve application code portability, given the
variations in hardware that result in different definitions of what an address actually is.
The Address type has support for address comparison (page 405) and address arithmetic
(page 407) (also known as pointer arithmetic in C). We discuss these topics later in this
section. First, let's talk about the Address attribute and the Address aspect.
Address attribute
The Address attribute allows us to get the address of an object. For example:
3 procedure Use_Address is
4 I : aliased Integer := 5;
5 A : Address;
6 begin
7 A := I'Address;
8 end Use_Address;
50 http://www.ada-auth.org/standards/22rm/html/RM-13-7.html
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Address_
↪Attribute
MD5: 1ee71b7cd3ed278647eb72f383da877f
function System.Address_Image
(A : System.Address) return String;
We can use this function to display the address in an user message, for example:
4 procedure Show_Address_Attribute is
5 I : aliased Integer := 5;
6 begin
7 Put_Line ("Address : "
8 & System.Address_Image (I'Address));
9 end Show_Address_Attribute;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Show_
↪Address_Attribute
MD5: 72efddedc57701665594de5ee1939d3d
Runtime output
Address : 00007FFE2D912B24
51 http://www.ada-auth.org/standards/22rm/html/RM-13-3.html
52 http://www.ada-auth.org/standards/22rm/html/RM-13-7.html
Address aspect
Usually, we let the compiler select the address of an object in memory, or let it use a
register to store that object. However, we can specify the address of an object with the
Address aspect. In this case, the compiler won't select an address automatically, but use
the address that we're specifying. For example:
6 procedure Show_Address is
7
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Address_
↪Aspect
MD5: 6339c743b1ca2b1adf58c977540b43d5
Runtime output
I_Main'Address : 00007FFE7EB50814
I_Mapped'Address : 00007FFE7EB50814
3 procedure Simple_Overlay is
4 type State is (Off, State_1, State_2)
5 with Size => Integer'Size;
6
11 S : State;
12 I : Integer
13 with Address => S'Address, Import, Volatile;
14 begin
15 S := State_2;
16 Put_Line ("I = " & Integer'Image (I));
17 end Simple_Overlay;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Simple_
↪Overlay
MD5: a65057882518824d3ea173d193a7ae67
Runtime output
I = 64
Here, I is an overlay of S, as it uses S'Address. With this approach, we can either use the
enumeration directly (by using the S object of State type) or its integer representation (by
using the I variable).
3 package Shared_Var_Types is
4
5 private
6 R : Integer
7 with Atomic,
8 Address =>
9 System'To_Address (16#FFFF00A0#);
10
11 end Shared_Var_Types;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Show_Access_
↪Address
MD5: 5c2d8e0a9615084c2a15f896c61adaa6
In this case, R will refer to the address in memory that we're specifying (16#FFFF00A0# in
this case).
As explained in the GNAT Reference Manual53 , the System'To_Address attribute denotes
a function identical to To_Address (from the System.Storage_Elements package) except
that it is a static attribute. (We talk about the To_Address function (page 406) function later
on.)
53 https://gcc.gnu.org/onlinedocs/gnat_rm/Attribute-To_005fAddress.html
54 http://www.ada-auth.org/standards/22rm/html/RM-13-3.html
55 http://www.ada-auth.org/standards/22rm/html/RM-13-7.html
56 http://www.ada-auth.org/standards/22rm/html/RM-13-7-1.html
Address comparison
We can compare addresses using the common comparison operators. For example:
6 procedure Show_Address is
7
8 I, J : Integer;
9 begin
10 Put_Line ("I'Address : "
11 & System.Address_Image
12 (I'Address));
13 Put_Line ("J'Address : "
14 & System.Address_Image
15 (J'Address));
16
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Address_
↪Aspect
MD5: 24ddb7d05159f26ef3b2ff6bcc2691e8
Runtime output
I'Address : 00007FFD81B2F2FC
J'Address : 00007FFD81B2F2F8
I'Address > J'Address
In this example, we compare the address of the I object with the address of the J object
using the =, < and > operators.
57 http://www.ada-auth.org/standards/22rm/html/RM-13-7.html
3 with System.Storage_Elements;
4 use System.Storage_Elements;
5
6 with System.Address_Image;
7
10 procedure Show_Address is
11 I : Integer;
12 A1, A2 : Address;
13 IA : Integer_Address;
14 begin
15 A1 := I'Address;
16 IA := To_Integer (A1);
17 A2 := To_Address (IA);
18
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Pointer_
↪Arith_Ada
MD5: 69e053886fb8e8571d6c94247dc9f30f
Runtime output
A1 : 00007FFFB2EEB1AC
IA : 140736195375532
A2 : 00007FFFB2EEB1AC
Here, we retrieve the address of the I object and store it in the A1 address. Then, we convert
A1 to an integer address by calling To_Integer (and store it in IA). Finally, we convert this
integer address back to an actual address by calling To_Address.
58 http://www.ada-auth.org/standards/22rm/html/RM-13-7-1.html
Address arithmetic
Although Ada supports address arithmetic, which we discuss in this section, it should be
reserved for very specific applications such as low-level programming. However, even in
situations that require close access to the underlying hardware, using address arithmetic
might not be the approach you should consider — make sure to evaluate other options first!
Ada supports address arithmetic via the System.Storage_Elements package, which in-
cludes operators such as + and - for addresses. Let's see a code example where we iterate
over an array by incrementing an address that points to each component in memory:
3 with System.Storage_Elements;
4 use System.Storage_Elements;
5
6 with System.Address_Image;
7
10 procedure Show_Address is
11
21 begin
22 for I in Arr'Range loop
23 declare
24 Curr : Integer
25 with Address => A;
26 begin
27 Curr := I;
28 Put_Line ("Curr'Address : "
29 & System.Address_Image
30 (Curr'Address));
31 end;
32
33 --
34 -- Address arithmetic
35 --
36 A := A + Storage_Offset (Integer'Size)
37 / Storage_Unit;
38 -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 -- Moving to next component
40 end loop;
41
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Pointer_
↪Arith_Ada
MD5: 2c1cdd6874036fb9a527baae63a312d9
Runtime output
Curr'Address : 00007FFE9719DA20
Curr'Address : 00007FFE9719DA24
Curr'Address : 00007FFE9719DA28
Curr'Address : 00007FFE9719DA2C
Curr'Address : 00007FFE9719DA30
Curr'Address : 00007FFE9719DA34
Curr'Address : 00007FFE9719DA38
Curr'Address : 00007FFE9719DA3C
Curr'Address : 00007FFE9719DA40
Curr'Address : 00007FFE9719DA44
Arr ( 1) : 1
Arr ( 2) : 2
Arr ( 3) : 3
Arr ( 4) : 4
Arr ( 5) : 5
Arr ( 6) : 6
Arr ( 7) : 7
Arr ( 8) : 8
Arr ( 9) : 9
Arr ( 10) : 10
In this example, we initialize the address A by retrieving the address of the first component
of the array Arr. (Note that we could have written Arr(1)'Address instead of Arr'Address.
In any case, the language guarantees that Arr'Address gives us the address of the first
component, i.e. Arr'Address = Arr(1)'Address.)
Then, in the loop, we declare an overlay Curr using the current value of the A address.
We can then operate on this overlay — here, we assign I to Curr. Finally, in the loop, we
increment address A and make it point to the next component in the Arr array — to do so,
we calculate the size of an Integer component in storage units. (For details on storage
units, see the section on storage size attribute (page 353).)
In other languages
The code example above corresponds (more or less) to the following C code:
8 int *a = arr;
9 /* int *a = &arr[0]; */
10
22 return 0;
23 }
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Addresses.Pointer_
↪Arith_C
MD5: 7aa709a4d7ed6ce2346dbabc853e28c0
Runtime output
While pointer arithmetic is very common in C, using address arithmetic in Ada is far from
common, and it should be only used when it's really necessary to do so.
59 http://www.ada-auth.org/standards/22rm/html/RM-13-3.html
60 http://www.ada-auth.org/standards/22rm/html/RM-13-7-1.html
As we know, we can use the Image attribute of a type to get a string associated with this
type. This is useful for example when we want to display a user message for an enumeration
type:
3 procedure Show_Enumeration_Image is
4
5 type Months is
6 (January, February, March, April,
7 May, June, July, August, September,
8 October, November, December);
9
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Discarding_Names.
↪Enumeration_Image
MD5: 3863c5e06641d96b59edb9e76daa7560
Runtime output
Month: JANUARY
3 procedure Show_Enumeration_Image is
4
5 type Months is
6 (January, February, March, April,
7 May, June, July, August, September,
8 October, November, December);
9
31 begin
32 Put_Line ("Month: "
33 & Months_Image (M));
34 end Show_Enumeration_Image;
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Discarding_Names.
↪Enumeration_Image
MD5: 2db86044d2045bd9d4c3998cca36d51c
Runtime output
Month: JANUARY
Here, the Months_Image function associates a string with each month of the Months enu-
meration. As expected, the compiler needs to store the strings used in the Months_Image
function when compiling this code. Similarly, the compiler needs to store strings for the
Months enumeration for the Image attribute.
Sometimes, we don't need to call the Image attribute for a type. In this case, we could
save some storage by eliminating the strings associated with the type. Here, we can use
the Discard_Names aspect to request the compiler to reduce — as much as possible — the
amount of storage used for storing names for this type. Let's see an example:
4 type Months is
5 (January, February, March, April,
6 May, June, July, August, September,
7 October, November, December)
8 with Discard_Names;
9
Project: Courses.Advanced_Ada.Data_Types.Type_Representation.Discarding_Names.
↪Discard_Names
MD5: 7891caac459a4be2096d443ca3190036
In this example, the compiler attempts to not store strings associated with the Months type
duration compilation.
Note that the Discard_Names aspect is available for enumerations, exceptions, and tagged
types.
we see that the application displays "0" instead of "JANUARY". This is because GNAT doesn't
store the strings associated with the Months type when we use the Discard_Names aspect
for the Months type. (Therefore, the Months'Image attribute doesn't have that information.)
Instead, the compiler uses the integer value of the enumeration, so that Months'Image
returns the corresponding string for this integer value.
25.3 Records
As mentioned in the Introduction to Ada (page 65) course, record components can have
default initial values. Also, we've seen that other kinds of types can have default values
(page 334).
In the Ada Reference Manual, we refer to these default initial values as "default expressions
of record components." The term default expression indicates that we can use any kind of
expression for the default initialization of record components — which includes subprogram
calls for example:
10 end Show_Default_Initialization;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Simple_
↪Example
MD5: 6d06be7f087513b669ba5481d6ee5004
In this example, the A component is initialized by default by a call to the Init procedure.
61 http://www.ada-auth.org/standards/22rm/html/RM-C-5.html
62 http://www.ada-auth.org/standards/22rm/html/RM-3-8.html
Dependencies
Default expressions cannot depend on other components. For example, if we have two
components A and B, we cannot initialize B based on the value that A has:
11 end Show_Default_Initialization_Dependency;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.No_
↪Dependency
MD5: ca23cbd7e4a54d0b9c6974aed0ee77c8
Build output
In this example, we cannot initialize the B component based on the value of the A compo-
nent. (In fact, the syntax Rec.A as a way to refer to the A component is only allowed in
predicates, not in the record component declaration.)
Initialization Order
The default initialization of record components is performed in arbitrary order. In fact, the
order is decided by the compiler, so we don't have control over it.
Let's see an example:
12 end Simple_Recs;
13 end Simple_Recs;
3 procedure Show_Initialization_Order is
4 R : Rec;
5 begin
6 null;
7 end Show_Initialization_Order;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.
↪Initialization_Order
MD5: e3ab92ea9b2a99815cea8c2ea11cbbfb
Runtime output
A: 1
B: 2
A: 1
B: 2
However, the compiler is allowed to rearrange the operations, so this output is possible as
well:
B: 2
A: 1
Therefore, we must write the default expression of each individual record components in
such a way that the resulting initialization value is always correct, independently of the
order that those expressions are evaluated.
Evaluation
According to the Annotated Ada Reference Manual, the "default expression of a record com-
ponent is only evaluated upon the creation of a default-initialized object of the record type."
This means that the default expression is by itself not evaluated when we declare the record
type, but when we create an object of this type. It follows from this rule that the default
is only evaluated when necessary, i.e,, when an explicit initial value is not specified in the
object declaration.
Let's see an example:
4 procedure Show_Initialization_Order is
5 begin
6 Put_Line ("Some processing first...");
7 Put_Line
8 ("Now, let's declare an object "
9 & "of the record type Rec...");
10
11 declare
12 R : Rec;
13 begin
14 Put_Line
15 ("An object of Rec type has "
16 & "just been created.");
17 end;
18
19 end Show_Initialization_Order;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.
↪Initialization_Order
MD5: 126e3edfe4cb8033f40b939ff9922958
Runtime output
Here, we only see the information displayed by the Init function — which is called to
initialize the A and B components of the R record — during the object creation. In other
words, the default expressions Init ("A", 1) and Init ("B", 2) are not evaluated when
we declare the R type, but when we create an object of this type.
63 http://www.ada-auth.org/standards/22aarm/html/AA-3-8.html
Note: This subsection was originally written by Robert A. Duff and published as Gem #12:
Limited Types in Ada 200564 .
4 type T is private;
5 private
6 type T is
7 record
8 Color : Color_Enum := Red;
9 Is_Gnarly : Boolean := False;
10 Count : Natural;
11 end record;
12
13 procedure Do_Something;
14 end Type_Defaults;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Default_
↪Init
MD5: 218154278081f89595534bc02e34539b
If we want to say, "make Count equal 100, but initialize Color and Is_Gnarly to their
defaults", we can do this:
3 Object_100 : constant T :=
4 (Color => <>,
5 Is_Gnarly => <>,
6 Count => 100);
7
10 end Type_Defaults;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Default_
↪Init
MD5: e64f8881ee74b90dd6058ca8961aae31
Historically
Prior to Ada 2005, the following style was common:
64 https://www.adacore.com/gems/ada-gem-12
3 Object_100 : constant T :=
4 (Color => Red,
5 Is_Gnarly => False,
6 Count => 100);
7
10 end Type_Defaults;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Default_
↪Init
MD5: c1ddfae75d7f0c691356027903a6d144
Here, we only wanted Object_100 to be a default-initialized T, with Count equal to 100. It's
a little bit annoying that we had to write the default values Red and False twice. What if
we change our mind about Red, and forget to change it in all the relevant places? Since
Ada 2005, the <> notation comes to the rescue, as we've just seen.
On the other hand, if we want to say, "make Count equal 100, but initialize all other com-
ponents, including the ones we might add next week, to their defaults", we can do this:
8 end Type_Defaults;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Default_
↪Init
MD5: 93f5d71ae80ff0ebad54f2569539f536
Note that if we add a component Glorp : Integer; to type T, then the others case leaves
Glorp undefined just as this code would do:
3 procedure Do_Something is
4 Object_100 : T;
5 begin
6 Object_100.Count := 100;
7 end Do_Something;
8
9 end Type_Defaults;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Default_
↪Init
MD5: 6d328318e2695516794df33466fa5283
Therefore, you should be careful and think twice before using others.
Advanced Usages
5 private
6
13 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Default_Initialization.Per_Object_
↪Expressions
MD5: 92591ea482db2b009b8eeafe633ca6cd
In this section, we discuss how to use incomplete types (page 305) to declare mutually
dependent types. Let's start with this example:
3 type T1 is record
4 B : T2;
5 end record;
6
7 type T2 is record
8 A : T1;
9 end record;
10
11 end Mutually_Dependent;
Project: Courses.Advanced_Ada.Data_Types.Records.Mutually_Dependent_Types.Mutually_
↪Dependent
MD5: ffa8d6ab83a1172dcbae0978952dacb2
Build output
When you try to compile this example, you get a compilation error. The first problem with
this code is that, in the declaration of the T1 record, the compiler doesn't know anything
about T2. We could solve this by declaring an incomplete type (type T2;) before the
declaration of T1. This, however, doesn't solve all the problems in the code: the compiler
still doesn't know the size of T2, so we cannot create a component of this type. We could,
instead, declare an access type and use it here. By doing this, even though the compiler
doesn't know the size of T2, it knows the size of an access type designating T2, so the
record component can be of such an access type.
To summarize, in order to solve the compilation error above, we need to:
• use at least one incomplete type;
• declare at least one component as an access to an object.
For example, we could declare an incomplete type T2 and then declare the component B of
the T1 record as an access to T2. This is the corrected version:
3 type T2;
4 type T2_Access is access T2;
5
6 type T1 is record
7 B : T2_Access;
8 end record;
9
10 type T2 is record
11 A : T1;
12 end record;
13
14 end Mutually_Dependent;
Project: Courses.Advanced_Ada.Data_Types.Records.Mutually_Dependent_Types.Mutually_
↪Dependent
MD5: 1ae10638624a97fa18b9d8f96bfa74ed
We could strive for consistency and declare two incomplete types and two accesses, but
this isn't strictly necessary in this case. Here's the adapted code:
3 type T1;
4 type T1_Access is access T1;
5
6 type T2;
7 type T2_Access is access T2;
(continues on next page)
9 type T1 is record
10 B : T2_Access;
11 end record;
12
13 type T2 is record
14 A : T1_Access;
15 end record;
16
17 end Mutually_Dependent;
Project: Courses.Advanced_Ada.Data_Types.Records.Mutually_Dependent_Types.Mutually_
↪Dependent
MD5: 9a9899cd0dd2525bd27d67d6629a0071
Later on, we'll see that these code examples can be written using anonymous access types
(page 877).
A null record is a record that doesn't have any components. Consequently, it cannot store
any information. When declaring a null record, we simply write null instead of declaring
actual components, as we usually do for records. For example:
7 end Null_Recs;
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Null_Record
MD5: 3c82da822710342354134fa71a03452a
Note that the syntax can be simplified to is null record, which is much more common
than the previous form:
5 end Null_Recs;
65 http://www.ada-auth.org/standards/22rm/html/RM-3-10-1.html
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Null_Record
MD5: 1da1746ce5b0a237276272d2b620e282
Although a null record doesn't have components, we can still specify subprograms for it.
For example, we could specify an addition operation for it:
8 end Null_Recs;
11 end Null_Recs;
3 procedure Show_Null_Rec is
4 A, B : Null_Record;
5 begin
6 B := A + A;
7 A := A + B;
8 end Show_Null_Rec;
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Null_Record
MD5: 3a1c2fbae75541dfb0b2ff4c14d22039
66 http://www.ada-auth.org/standards/22rm/html/RM-4-3-1.html
Simple Prototyping
A null record doesn't provide much functionality on itself, as we're not storing any informa-
tion in it. However, it's far from being useless. For example, we can make use of null records
to design an API, which we can then use in an application without having to implement the
actual functionality of the API. This allows us to design a prototype without having to think
about all the implementation details of the API in the first stage.
Consider this example:
5 function Create
6 (Active : Boolean)
7 return Device;
8
9 procedure Reset
10 (D : out Device) is null;
11
12 procedure Process
13 (D : in out Device) is null;
14
15 procedure Activate
16 (D : in out Device) is null;
17
18 procedure Deactivate
19 (D : in out Device) is null;
20
21 private
22
29 end Devices;
4 procedure Show_Device is
5 A : Device;
6 begin
7 Put_Line ("Creating device...");
8 A := Create (Active => True);
9
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Device
MD5: 7d2fce20ac33607f7081381b307a564a
Runtime output
Creating device...
Processing on device...
Deactivating device...
Activating device...
Resetting device...
In the Devices package, we're declaring the Device type and its primitive subprograms:
Create, Reset, Process, Activate and Deactivate. This is the API that we use in our
prototype. Note that, although the Device type is declared as a private type, it's still defined
as a null record in the full view.
In this example, the Create function, implemented as an expression function in the private
part, simply returns a null record. As expected, this null record returned by Create matches
the definition of the Device type.
All procedures associated with the Device type are implemented as null procedures, which
means they don't actually have an implementation nor have any effect. We'll discuss this
topic later on in the course (page 653).
In the Show_Device procedure — which is an application that implements our prototype —,
we declare an object of Device type and call all subprograms associated with that type.
Because we're either using expression functions or null procedures in the specification of
the Devices package, we don't have a package body for it (as there's nothing to be imple-
mented). We could, however, move those user messages from the Show_Devices proce-
dure to a dummy implementation of the Devices package. This is the adapted code:
16 end Devices;
42 end Devices;
3 procedure Show_Device is
4 A : Device;
5 begin
6 A := Create (Active => True);
7 Process (A);
8 Deactivate (A);
9 Activate (A);
10 Reset (A);
11 end Show_Device;
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Device
MD5: 1a21b41f3847f6c132ccbc9696ab7689
Runtime output
Creating device...
Deactivating device...
Resetting device...
Activating device...
Processing on device...
As we changed the specification of the Devices package to not use null procedures, we
now need a corresponding package body for it. In this package body, we implement the
operations on the Device type, which actually just display a user message indicating which
operation is being called.
Let's focus on this updated version of the Show_Device procedure. Now that we've removed
all those calls to Put_Line from this procedure and just have the calls to operations asso-
ciated with the Device type, it becomes more apparent that, even though Device is just a
null record, we can design an application with a sequence of various commands operating
on it. Also, when we just read the source-code of the Show_Device procedure, there's no
clear indication that the Device type doesn't actually hold any information.
As we've just seen, we can use null records like any other type and create complex proto-
types with them. We could, for instance, design an application that makes use of many null
records, or even have types that depend on or derive from null records. Let's see a simple
example:
15 end Many_Devices;
3 procedure Show_Derived_Device is
4 A : Device;
5 B : Derived_Device;
6 C : Device_Config;
7 begin
8 A := Create (Config => C);
9 B := Create (Config => C);
(continues on next page)
11 Process (B);
12 end Show_Derived_Device;
In this example, the Create function has a null record parameter (of Device_Config type)
and returns a null record (of Device type). Also, we derive the Derived_Device type from
the Device type. Consequently, Derived_Device is also a null record (since it's derived
from a null record). In the Show_Derived_Device procedure, we declare objects of those
types (A, B and C) and call primitive subprograms to operate on them.
This example shows that, even though the types we've declared are just null records, they
can still be used to represent dependencies in our application.
Let's focus again on the previous example. After we have an initial prototype, we can start
implementing some of the functionality needed for the Device type. For example, we can
store information about the current activation state in the record:
16 private
17
22 end Devices;
42 end Devices;
4 procedure Show_Device is
5 A : Device;
6 begin
7 A := Create (Active => True);
8 Process (A);
9 Deactivate (A);
10 Activate (A);
11 Reset (A);
12 end Show_Device;
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Device
MD5: 348ce0c110b47a6b6fd1c9fe73ef0558
Build output
Runtime output
Creating device...
Deactivating device...
Resetting device...
Activating device...
Processing on device...
Now, the Device record contains an Active component, which is used in the updated ver-
sions of Create, Activate and Deactivate.
Note that we haven't done any change to the implementation of the Show_Device proce-
dure: it's still the same application as before. As we've been hinting in the beginning, using
null records makes it easy for us to first create a prototype — as we did in the Show_Device
procedure — and postpone the API implementation to a later phase of the project.
3 type Tagged_Null_Record is
4 tagged null record;
5
6 type Abstract_Tagged_Null_Record is
7 abstract tagged null record;
8
9 end Null_Recs;
Project: Courses.Advanced_Ada.Data_Types.Records.Null_Records.Tagged_Null_Record
MD5: 918572d2c50911b84c80a9c601b75439
As we see in this example, a type can be tagged, or even abstract tagged. We discuss
abstract types later on in the course.
As expected, in addition to deriving from tagged types, we can also extend them. For
example:
10 private
11
26 end Devices;
In this example, we derive Derived_Device from the Device type and extend it with the
Active component. (Because we have a type extension, we also need to override the
Create function.)
Since we're now introducing elements from object-oriented programming, we could con-
sider using interfaces instead of null records. We'll discuss this topic later on in the course.
In record type declarations, we might want to define a component that makes use of a name
that refers to a discriminant of the record type, or to the record type itself. An expression
where we use such a name is called a per-object expression.
The term "per-object" comes from the fact that, in the component definition, we're referring
to a piece of information that will be known just when creating an object of that type. For
example, if the per-object expression refers to a discriminant of a type T, the actual value of
that discriminant will only be specified when we declare an object of type T. Therefore, the
component definition is specific for that individual object — but not necessarily for other
objects of the same type, as we might use different values for the discriminant.
The constraint that contains a per-object expression is called a per-object constraint. The
actual constraint of that component isn't completely known when we declare the record
type, but only later on when an object of that type is created. (Note that the syntax of a
constraint includes the parentheses or the keyword range.)
In addition to referring to discriminants, per-object expressions can also refer to the record
type itself, as we'll see later.
Let's start with a simple record declaration:
5 private
6
7 type Integer_Array is
8 array (Positive range <>) of Integer;
9
22 Top : Natural := 0;
23 end record;
24
25 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_Expressions.Per_Object_
↪Expression
MD5: e4012454ea886fd429d82159b8d344b7
In this example, we see the Stack record type with a discriminant S. In the declaration
of the Arr component of the that type, S is a per-object expression, as it refers to the S
discriminant. Also, (1 .. S) is a per-object constraint.
Let's look at another example using anonymous access types (page 853):
3 type T is private;
4
8 private
9
28 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_Expressions.Per_Object_
↪Expression_Access_Discriminant
MD5: 8b404688be1e103773c28a6977785836
Let's focus on the T_Processor type from this example. The Selected_T discriminant is
being used in the definition of the E component. The per-object constraint is (Selected_T).
Finally, per-object expressions can also refer to the record type we're declaring. For exam-
ple:
5 private
6
22 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_Expressions.Per_Object_
↪Expression_Access_Discriminant
MD5: a67b3034008fdf2a8c5fd1b6da769128
In this example, when we write T'Access within the declaration of the T record type, the
actual value for the Access attribute will be known when an object of T type is created. In
that sense, T'Access is a per-object expression — (T'Access) is the corresponding per-
object constraint.
Note that T'Access is referring to the type within a type definition. This is generally treated
as a reference to the object being created, the so-called current instance.
Relevant topics
• 3.8 Record Types67
67 http://www.ada-auth.org/standards/22rm/html/RM-3-8.html
Default value
We can also use per-object expressions to calculate the default value of a record component:
5 private
6
12 S : Natural := D'Size;
13 -- ^^^^^^
14 -- Per-object expression
15 end record;
16
17 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_Expressions.Per_Object_
↪Expression_Default_Value
MD5: 70454b0b116094a02b897d8d1d0080fb
Here, we calculate the default value of V using the per-object expression D - 1, and the
default of value of S using the per-object D'Size.
The default expression for a component of a discriminated record can be an arbitrary per-
object expression. (This contrasts with important restrictions (page 433) that exist for per-
object constraints, as we discuss later on.) Such expressions might include function calls
or uses of any defined operator. For this reason, the following code example is accepted by
the compiler:
5 private
6
7 type Integer_Array is
8 array (Positive range <>) of Integer;
9
13 Top : Natural := 0;
14
15 Overflow_Warning : Positive
16 := S * 9 / 10;
17 -- ^^^^^^^^^^
18 -- Per-object expression
19 -- using computation for
20 -- the default expression.
21 end record
(continues on next page)
37 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_Expressions.Per_Object_
↪Expression_Computation
MD5: 6783568fd3e76a85ca7c1cc65ba023c5
In this example, we can identify multiple per-object expressions that use a computation: S
* 9 / 10, (S + 1) / 2, and S - 1.
Restrictions
3 type Bit_Field is
4 array (Positive range <>) of Boolean
5 with Pack;
6
7 type T is record
8 Arr : Bit_Field (1 .. T'Size);
9 -- ^^^^^^
10 -- ERROR: per-object range constraint
11 -- using the Size attribute
12 -- is illegal.
13 end record;
14
15 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_
↪Expressions.Per_Object_Expression_Range_Constraint
MD5: c2ac9588c1d1adac8c584a0e36a81342
Build output
5 private
6
7 type Integer_Array is
8 array (Natural range <>) of Integer;
9
19 end Rec_Per_Object_Expressions;
Project: Courses.Advanced_Ada.Data_Types.Records.Per_Object_
↪Expressions.Per_Object_Expression_Range_Computation
MD5: 1224bb63f7953743d84a258226c35c50
Build output
In this example, using the computation S - 1 to specify the range of Arr isn't
permitted. (Note that, as we've seen before (page 432), this restriction doesn't
apply when the computation is used in a per-object expression that calculates the
default value of a component.)
2. We can only use access attributes (T'Access and T'Unchecked_Access) in per-object
constraints.
25.4 Aggregates
3 with Ada.Containers.Vectors;
4
5 procedure Show_Container_Aggregate is
6
10 V : constant Float_Vec.Vector :=
11 [1.0, 2.0, 3.0];
12
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Container_Aggregates.Simple_
↪Container_Aggregate
MD5: ef13386fef0b7be0b3ea999a7752d5f1
In this example, [1.0, 2.0, 3.0] is a container aggregate that we use to initialize a vector
V.
We can specify container aggregates in three forms:
• as a null container aggregate, which indicates a container without any elements and
is represented by the [] syntax;
• as a positional container aggregate, where the elements are simply listed in a se-
quence (such as [1, 2]);
• as a named container aggregate, where a key is indicated for each element of the list
(such as [1 => 10, 2 => 15]).
Let's look at a complete example:
3 with Ada.Containers.Vectors;
4
5 procedure Show_Container_Aggregate is
6
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Container_Aggregates.Simple_
↪Container_Aggregate
MD5: 15ed6370377423044368a5d56402e940
In this example, we see the three forms of container aggregates. The difference between
positional and named container aggregates is that:
• for positional container aggregates, the vector index is implied by its position;
while
• for named container aggregates, the index (or key) of each element is explicitly indi-
cated.
Also, the named container aggregate in this example (Named_V) is using an index as the
name (i.e. it's an indexed aggregate). Another option is to use non-indexed aggregates,
where we use actual keys — as we do in maps. For example:
3 with Ada.Containers.Vectors;
4 with Ada.Containers.Indefinite_Hashed_Maps;
5 with Ada.Strings.Hash;
6
7 procedure Show_Named_Container_Aggregate is
8
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Container_Aggregates.Named_
↪Container_Aggregate
MD5: 2eabf312c243856dcb2d6884f71e19e2
In this example, Indexed_Named_V and Keyed_Named_V are both initialized with a named
container aggregate. However:
• the container aggregate for Indexed_Named_V is an indexed aggregate, so we use an
index for each element;
while
• the container aggregate for Keyed_Named_V has a key for each element.
Later on, we'll talk about the Aggregate aspect, which allows for defining custom container
aggregates for any record type.
We've already seen record aggregates in the Introduction to Ada (page 66) course, so this
is just a brief overview on the topic.
As we already know, record aggregates can have positional and named component associ-
ations. For example, consider this package:
9 end Points;
18 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: fd01961cf1da9b48d2a6150da30f7377
3 procedure Show_Record_Aggregates is
4 P : Point_3D;
5 begin
6 -- Positional component association
7 P := (0, 1, 2);
8
9 Display (P);
10
16 Display (P);
17 end Show_Record_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: fc4cff950e31a633ab4e2ae3d21ddc7b
Runtime output
(X => 0,
Y => 1,
Z => 2)
(X => 3,
Y => 4,
Z => 5)
3 procedure Show_Record_Aggregates is
4 P : Point_3D;
5 begin
6 -- Positional and named component associations
7 P := (3, 4,
8 Z => 5);
9
10 Display (P);
11 end Show_Record_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: 493a2a87b4b28dfb0882ad73acf84710
Runtime output
(X => 3,
Y => 4,
Z => 5)
In this case, only the Z component has a named association, while the other components
have a positional association.
Note that a positional association cannot follow a named association, so we cannot write
P := (3, Y => 4, 5);, for example. Once we start using a named association for a
component, we have to continue using it for the remaining components.
In addition, we can choose multiple components at once and assign the same value to
them. For that, we use the | syntax:
3 procedure Show_Record_Aggregates is
4 P : Point_3D;
5 begin
6 -- Multiple component selection
7 P := (X | Y => 5,
8 Z => 6);
9
10 Display (P);
11 end Show_Record_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: a4fde562fb60d290caf46d86b13e694b
Runtime output
(X => 5,
Y => 5,
Z => 6)
<>
We can use the <> syntax to tell the compiler to use the default value for specific compo-
nents. However, if there's no default value for specific components, that component isn't
initialized to a known value. For example:
3 procedure Show_Record_Aggregates is
4 P : Point_3D;
5 begin
6 P := (0, 1, 2);
7 Display (P);
8
9 -- Specifying X component.
10 P := (X => 42,
11 Y => <>,
12 Z => <>);
13 Display (P);
14
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: 25145e7cba5a566c518ac4218e550899
Runtime output
(X => 0,
Y => 1,
Z => 2)
(X => 42,
(continues on next page)
69 http://www.ada-auth.org/standards/22rm/html/RM-4-3-1.html
Here, as the components of Point_3D don't have a default value, those components that
have <> are not initialized:
• when we write (X => 42, Y => <>, Z => <>), only X is initialized;
• when we write (X => <>, Y => 10, Z => 20) instead, only X is uninitialized.
If the components of Point_3D had default values, those would have been used. For ex-
ample, we may change the type declaration of Point_3D and use default values for each
component:
11 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: 8a716db129e6f231c4003b77d8b61ea3
Then, writing <> makes use of those default values we've just specified:
3 procedure Show_Record_Aggregates is
4 P : Point_3D := (0, 0, 0);
5 begin
6 -- Using default value for
7 -- all components
8 P := (X => <>,
9 Y => <>,
(continues on next page)
MD5: e64c6fe4e4b3dbaa084d9b97b4fb971f
Runtime output
(X => 10,
Y => 20,
Z => 30)
Now, as expected, the default values of each component (10, 20 and 30) are used when
we write <>.
Similarly, we can specify a default value for the type of each component. For example, let's
declare a Point_Value type with a default value — using the Default_Value aspect — and
use it in the Point_3D record type:
14 end Points;
18 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregate_Default_Value
MD5: 508d7f5e7d02da1677485f7d588847f6
Then, writing <> makes use of the default value of the Point_Value type:
3 procedure Show_Record_Aggregates is
4 P : Point_3D := (0.0, 0.0, 0.0);
5 begin
6 -- Using default value of Point_Value
7 -- for all components
8 P := (X => <>,
9 Y => <>,
10 Z => <>);
11 Display (P);
12 end Show_Record_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregate_Default_Value
MD5: 895799077af4a295c250480c32954a2c
Runtime output
(X => 9.99000E+01,
Y => 9.99000E+01,
Z => 9.99000E+01)
In this case, the default value of the Point_Value type (99.9) is used for all components
when we write <>.
others
Also, we can use the others selector to assign a value to all components that aren't explic-
itly mentioned in the aggregate. For example:
3 procedure Show_Record_Aggregates is
4 P : Point_3D;
5 begin
6 -- Specifying X component;
7 -- using 42 for all
8 -- other components.
9 P := (X => 42,
10 others => 100);
11 Display (P);
12
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Pos_Named_
↪Rec_Aggregates
MD5: 3146363eb36ab4485c7755794fb78bbc
Runtime output
(X => 42,
Y => 100,
Z => 100)
(X => 256,
Y => 256,
Z => 256)
When we write P := (X => 42, others => 100), we're assigning 42 to X and 100 to all
other components (Y and Z in this case). Also, when we write P := (others => 256), all
components have the same value (256).
Note that writing a specific value in others — such as (others => 256) — only works when
all components have the same type. In this example, all components of Point_3D have the
same type: Integer. If we had components with different types in the components selected
by others, say Integer and Float, then (others => 256) would trigger a compilation error.
For example, consider this package:
8 end Custom_Records;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregates_Others
MD5: 875e470aa2cbc5fcfefae649ed5528f6
If we had written an aggregate such as (others => 256) for an object of type Inte-
ger_Float, the value (256) would be OK for components A and B, but not for components
Y and Z:
3 procedure Show_Record_Aggregates_Others is
4 Dummy : Integer_Float;
5 begin
6 -- ERROR: components selected by
7 -- others must be of same
8 -- type.
9 Dummy := (others => 256);
10 end Show_Record_Aggregates_Others;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregates_Others
MD5: d543ee07e24caf63384ab0d140054be2
Build output
We can fix this compilation error by making sure that others only refers to components of
the same type:
3 procedure Show_Record_Aggregates_Others is
4 Dummy : Integer_Float;
5 begin
6 -- OK: components selected by
7 -- others have Integer type.
8 Dummy := (Y | Z => 256.0,
9 others => 256);
10 end Show_Record_Aggregates_Others;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregates_Others
MD5: d01977a49e08d2c6cb6b7788581ed56f
In any case, writing (others => <>) is always accepted by the compiler because it simply
selects the default value of each component, so the type of those values is unambiguous:
3 procedure Show_Record_Aggregates_Others is
4 Dummy : Integer_Float;
5 begin
6 Dummy := (others => <>);
7 end Show_Record_Aggregates_Others;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregates_Others
MD5: db9b72ffc933436e76305887276eeafd
This code compiles because <> uses the appropriate default value of each component.
Record discriminants
When a record type has discriminants, they must appear as components of an aggregate
of that type. For example, consider this package:
18 end Points;
9 case P.D is
10 when Dim_1 =>
11 Put_Line (" (X => "
12 & Integer'Image (P.X1)
13 & ")");
14 when Dim_2 =>
15 Put_Line (" (X => "
16 & Integer'Image (P.X2)
17 & ",");
18 Put_Line (" Y => "
19 & Integer'Image (P.Y2)
20 & ")");
21 when Dim_3 =>
22 Put_Line (" (X => "
23 & Integer'Image (P.X3)
24 & ",");
25 Put_Line (" Y => "
26 & Integer'Image (P.Y3)
27 & ",");
28 Put_Line (" Z => "
29 & Integer'Image (P.Z3)
30 & ")");
31 end case;
32 end Display;
33
34 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregate_Discriminant
MD5: bd71322a65ca50e1eefa0aedd407931a
To write aggregates of the Point type, we have to specify the D discriminant as a component
of the aggregate. The discriminant must be included in the aggregate — and must be static
— because the compiler must be able to examine the aggregate to determine if it is both
complete and consistent. All components must be accounted for one way or another, as
usual — but, in addition, references to those components whose existence depends on
the discriminant's values must be consistent with the actual discriminant value used in
the aggregate. For example, for type Point, an aggregate can only reference the X3, Y3,
and Z3 components when Dim_3 is specified for the discriminant D; otherwise, those three
components don't exist in that aggregate. Also, the discriminant D must be the first one if
we use positional component association. For example:
3 procedure Show_Rec_Aggregate_Discriminant is
4 -- Positional component association
5 P1 : constant Point := (Dim_1, 0);
6
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Record_Aggregates.Rec_
↪Aggregate_Discriminant
MD5: d487e0c68ea69c3e0f2adb8ac958e31d
Runtime output
DIM_1
(X => 0)
DIM_2
(X => 3,
Y => 4)
DIM_3
(X => 3,
Y => 4,
Z => 5)
As we see in this example, we can use any component association in the aggregate, as long
as we make sure that the discriminants of the type appear as components — and are the
first components in the case of positional component association.
Note: This section was originally written by Robert A. Duff and published as Gem #1:
Limited Types in Ada 200570 .
One interesting feature of Ada are the full coverage rules for aggregates. For example,
suppose we have a record type:
4 package Persons is
5 type Years is new Natural;
6
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Full_Coverage_Rules_Aggregates.
↪Full_Coverage_Rules
MD5: 7755bffa8b4473c425ae5075e9c478e9
6 procedure Show_Aggregate_Init is
7
8 X : constant Person :=
9 (Name =>
10 To_Unbounded_String ("John Doe"),
11 Age => 25);
12 begin
13 null;
14 end Show_Aggregate_Init;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Full_Coverage_Rules_Aggregates.
↪Full_Coverage_Rules
MD5: 681e665b76265eff4c4d870ec011ba37
The full coverage rules say that every component of Person must be accounted for in the
aggregate. If we later modify type Person by adding a component:
70 https://www.adacore.com/gems/gem-1
4 package Persons is
5 type Years is new Natural;
6
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Full_Coverage_Rules_Aggregates.
↪Full_Coverage_Rules
MD5: 5fc5b93748d92932bfc9e0f15c0228b7
and we forget to modify X accordingly, the compiler will remind us. Case statements also
have full coverage rules, which serve a similar purpose.
Of course, we can defeat the full coverage rules by using others (usually for array ag-
gregates (page 450) and case statements, but occasionally useful for record aggregates
(page 437)):
6 procedure Show_Aggregate_Init_Others is
7
8 X : constant Person :=
9 (Name =>
10 To_Unbounded_String ("John Doe"),
11 others => 25);
12 begin
13 null;
14 end Show_Aggregate_Init_Others;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Full_Coverage_Rules_Aggregates.
↪Full_Coverage_Rules
MD5: 6d26de8dd6820682cb9150dcbb40f106
According to the Ada RM, others here means precisely the same thing as Age | Shoe_Size.
But that's wrong: what others really means is "all the other components, including the ones
we might add next week or next year". That means you shouldn't use others unless you're
pretty sure it should apply to all the cases that haven't been invented yet.
Later on, we'll discuss full coverage rules for limited types (page 951).
We've already discussed array aggregates in the Introduction to Ada (page 71) course.
Therefore, this section just presents some details about this topic.
Note: The array aggregate syntax using brackets (e.g.: [1, 2, 3]), which we mention in
this section, was introduced in Ada 2022.
Similar to record aggregates (page 437), array aggregates can be positional or named.
Consider this package:
7 end Points;
20 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: 7ed70d1c9685bc36900e1713619f3321
71 http://www.ada-auth.org/standards/22rm/html/RM-4-3-3.html
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 -- Positional component association
9 P := [0, 1, 2];
10
11 Display (P);
12
18 Display (P);
19 end Show_Array_Aggregates;
MD5: 5913ef6f43ea873de4e3f0760265de4b
Runtime output
(X => 0,
Y => 1,
Z => 2)
(X => 3,
Y => 4,
Z => 5)
In this example, we assign a positional array aggregate ([1, 2, 3]) to P. Then, we assign
a named array aggregate ([1 => 3, 2 => 4, 3 => 5]) to P. In this case, the names are
the indices of the components we're assigning to.
We can also assign array aggregates to slices:
5 procedure Show_Array_Aggregates is
6 P : Point_3D := [others => 0];
7 begin
8 -- Positional component association
9 P (2 .. 3) := [1, 2];
10
11 Display (P);
12
17 Display (P);
18 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: 8b36bd7638bd765f45693b78c5c7b872
Runtime output
(X => 0,
Y => 1,
Z => 2)
(X => 0,
Y => 3,
Z => 4)
Note that, when using a named array aggregate, the index (name) that we use in the
aggregate doesn't have to match the slice. In this example, we're assigning the component
from index 1 of the aggregate to the component of index 2 of the array P (and so on).
Historically
In the first versions of Ada, we could only write array aggregates using parentheses.
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 -- Positional component association
9 P := (0, 1, 2);
10
11 Display (P);
12
18 Display (P);
19 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.
↪Array_Aggregates
MD5: 3d9f1fda006f1d566ae2743240568879
Runtime output
(X => 0,
Y => 1,
Z => 2)
(continues on next page)
This syntax is considered obsolescent since Ada 2022: brackets ([1, 2, 3])
should be used instead.
We can also write null array aggregates: []. As the name implies, this kind of array aggre-
gate doesn't have any components.
Consider this package:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
8 end Integer_Arrays;
12 Put_Line ("(");
13 for I in A'Range loop
14 Put (" "
15 & I'Image
16 & " => "
17 & A (I)'Image);
18 if I /= A'Last then
19 Put_Line (",");
20 else
21 New_Line;
22 end if;
23 end loop;
24 Put_Line (")");
25 end Display;
26
27 end Integer_Arrays;
MD5: 412ebe9de1dfb9157f5379d31162554d
5 procedure Show_Array_Aggregates is
6 N : constant Integer_Array := [];
7 begin
8 Display (N);
9 end Show_Array_Aggregates;
MD5: 8cdb9a004ea16f716bf2e2ad5a65358e
Runtime output
Length = 0
(
)
In this example, when we call the Display procedure, we confirm that N doesn't have any
components.
|, <>, others
We've seen the following syntactic elements when we were discussing record aggregates
(page 437): |, <> and others. We can apply them to array aggregates as well:
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 -- All components have a value of zero.
9 P := [others => 0];
10
11 Display (P);
12
18 Display (P);
19
26 Display (P);
27 end Show_Array_Aggregates;
MD5: 053d4f162cc676b61d8e8a720321d40f
Runtime output
(X => 0,
Y => 0,
Z => 0)
(X => 3,
Y => 3,
Z => 4)
(X => 1659340856,
Y => 5,
Z => 5)
In this example, we use the |, <> and others elements in a very similar way as we did with
record aggregates. (See the comments in the code example for more details.)
Note that, as for record aggregates, the <> makes use of the default value (if it is available).
We discuss this topic in more details later on (page 464).
..
We can also use the range syntax (..) with array aggregates:
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 -- All components have a value of zero.
9 P := [1 .. 3 => 0];
10
11 Display (P);
12
18 Display (P);
19
26 Display (P);
27 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: bb36de6dcddf4b0bdcd5aa730f0988b1
Runtime output
(X => 0,
Y => 0,
Z => 0)
(X => 3,
Y => 3,
Z => 4)
(X => 931530824,
Y => 5,
Z => 5)
This example is a variation of the previous one. However, in this case, we're using ranges
instead of the | and others syntax.
Missing components
All aggregate components must have an associated value. If we don't specify a value for a
certain component, an exception is raised:
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 P := [1 => 4];
9 -- ERROR: value of components at indices
10 -- 2 and 3 are missing
11
12 Display (P);
13 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: 40d3a65f7fc0602782e548385ae07769
Build output
Runtime output
We can use others to specify a value to all components that haven't been explicitly men-
tioned in the aggregate:
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 P := [1 => 4, others => 0];
9 -- OK: unspecified components have a
10 -- value of zero
11
12 Display (P);
13 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: 63b60de44e7c08eeae19a6a9117818f5
Runtime output
(X => 4,
Y => 0,
Z => 0)
However, others can only be used when the range is known — compilation fails otherwise:
5 procedure Show_Array_Aggregates is
6 N1 : Integer_Array := [others => 0];
7 -- ERROR: range is unknown
8
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates_2
MD5: 65b457e017a4eca6051aac777cc429f4
Build output
We can use an iterated component association to specify an aggregate. This is the general
syntax:
5 procedure Show_Array_Aggregates is
6 P : Point_3D;
7 begin
8 -- All components have a value of zero
9 P := [for I in 1 .. 3 => 0];
10
11 Display (P);
12
20 Display (P);
21
28 Display (P);
29 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates
MD5: f11b3119e3fc1ece08f0b01d7e02576d
Runtime output
(X => 0,
Y => 0,
Z => 0)
(X => 3,
Y => 3,
Z => 4)
(X => 99,
Y => 2,
Z => 3)
5 procedure Show_Array_Aggregates is
6 P : Point_3D := [for I in Point_3D'Range => I];
7 begin
8 -- Each component is doubled
9 P := [for E of P => E * 2];
10
11 Display (P);
12
17 Display (P);
18 end Show_Array_Aggregates;
MD5: b8c1878c1fa516005d1861f1a37c4fb0
Runtime output
(X => 2,
Y => 4,
Z => 6)
(X => 3,
(continues on next page)
So far, we've discussed one-dimensional array aggregates. We can also use the same
constructs when dealing with multidimensional arrays. Consider, for example, this package:
9 end Matrices;
27 begin
28 Put_Line ("Length (1) = "
29 & M'Length (1)'Image);
30 Put_Line ("Length (2) = "
(continues on next page)
33 Put_Line ("(");
34 for I in M'Range (1) loop
35 Display_Row (M, I);
36 if I /= M'Last (1) then
37 Put_Line (",");
38 else
39 New_Line;
40 end if;
41 end loop;
42 Put_Line (")");
43
44 end Display;
45
46 end Matrices;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Matrix_
↪Aggregates
MD5: 748c7c695dfef43d7d4926edf5ddd3ae
5 procedure Show_Array_Aggregates is
6 M : Matrix (1 .. 2, 1 .. 3);
7 begin
8 -- Positional component association
9 M := [[0, 1, 2],
10 [3, 4, 5]];
11
12 Display (M);
13
22 Display (M);
23
24 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Matrix_
↪Aggregates
MD5: 78e1fad3b90c4f4d0f9d45f299e5ae10
Runtime output
Length (1) = 2
Length (2) = 3
(
(
1 => 0,
2 => 1,
3 => 2
),
(
1 => 3,
2 => 4,
3 => 5
)
)
Length (1) = 2
Length (2) = 3
(
(
1 => 3,
2 => 4,
3 => 5
),
(
1 => 6,
2 => 7,
3 => 8
)
)
The first aggregate we use in this example is [[0, 1, 2], [3, 4, 5]]. Here, [0, 1,
2] and [3, 4, 5] are subaggregates of the multidimensional aggregate. Subaggregates
don't have a type themselves, but are rather just considered part of a multidimensional
aggregate (which, of course, has an array type). In this sense, a subaggregate such as [0,
1, 2] is different from a one-dimensional aggregate (such as [0, 1, 2]), even though
they are written in the same way.
Strings in subaggregates
In the case of matrices using characters, we can use strings in the corresponding array
aggregates. Consider this package:
9 end String_Lists;
19 begin
20 Put_Line ("Length (1) = "
21 & SL'Length (1)'Image);
22 Put_Line ("Length (2) = "
23 & SL'Length (2)'Image);
24
25 Put_Line ("(");
26 for I in SL'Range (1) loop
27 Display_Row (SL, I);
28 if I /= SL'Last (1) then
29 Put_Line (",");
30 else
31 New_Line;
32 end if;
33 end loop;
34 Put_Line (")");
35 end Display;
36
37 end String_Lists;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.String_
↪Aggregates
MD5: 87b2e593cab823218a39c07d85f40c22
Then, when assigning to an object SL of String_List type, we can use strings in the ag-
gregates:
5 procedure Show_Array_Aggregates is
6 SL : String_List (1 .. 2, 1 .. 3);
7 begin
8 -- Positional component association
9 SL := ["ABC",
10 "DEF"];
11
12 Display (SL);
13
22 Display (SL);
23
29 Display (SL);
30 end Show_Array_Aggregates;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.String_
↪Aggregates
MD5: 82e376269e3be935d5cbd66202f26ec7
Runtime output
Length (1) = 2
Length (2) = 3
(
(ABC),
(DEF)
)
Length (1) = 2
Length (2) = 3
(
(ABC),
(DEF)
)
Length (1) = 2
Length (2) = 3
(
(XYZ),
( )
)
In the first assignment to SL, we have the aggregate ["ABC", "DEF"], which uses strings
as subaggregates. (Of course, we can use a named aggregate and assign characters to the
individual components.)
As we indicated earlier, the <> syntax sets a component to its default value — if such
a default value is available. If a default value isn't defined, however, the component will
remain uninitialized, so that the behavior is undefined. Let's look at more complex example
to illustrate this situation. Consider this package, for example:
11 type Point_3D_Array is
12 array (Positive range <>) of Point_3D;
13
16 end Points;
37 end Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Rec_Array_
↪Aggregates
MD5: ffaf3745621a30362c6aadaec2c3cef2
5 procedure Show_Record_Aggregates is
6 PA : Point_3D_Array (1 .. 2);
7 begin
8 PA := [ (X => 3,
9 Y => 4,
10 Z => 5),
11 (X => 6,
12 Y => 7,
13 Z => 8) ];
14 Display (PA);
15
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Rec_Array_
↪Aggregates
MD5: 1dee9505222fe9837cd5aa3bf119ee3a
Runtime output
(
1 =>
(X => 3,
Y => 4,
Z => 5),
2 =>
(X => 6,
Y => 7,
Z => 8)
)
(
1 =>
(X => 0,
Y => 0,
Z => 1674107533),
2 =>
(X => 32582,
Y => 1675622080,
Z => 32582)
)
Because the record components (of the Point_3D type) don't have default values, they
remain uninitialized when we write [1 => <>, 2 => <>]. (In fact, you may see garbage in
the values displayed by the Display procedure.)
When a default value is specified, it is used whenever <> is specified. For example, we
could use a type that has the Default_Value aspect in its specification:
6 type Integer_Array is
7 array (Positive range <>) of Value;
8
11 end Integer_Arrays;
5 procedure Show_Array_Aggregates is
6 N : Integer_Array (1 .. 4);
7 begin
8 N := [for I in N'Range => Value (I)];
9 Display (N);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates_2
MD5: 17641d696172b052925d5549f53b9712
Runtime output
Length = 4
(
1 => 1,
2 => 2,
3 => 3,
4 => 4
)
Length = 4
(
1 => 99,
2 => 99,
3 => 99,
4 => 99
)
When writing an aggregate for the Point_3D type, any component that has <> gets the
default value of the Point type (99):
5 type Integer_Array is
6 array (Positive range <>) of Value
7 with Default_Component_Value => 9999;
8
11 end Integer_Arrays;
5 procedure Show_Array_Aggregates is
6 N : Integer_Array (1 .. 4);
7 begin
8 N := [for I in N'Range => Value (I)];
9 Display (N);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates_2
MD5: c6b38711937a1a7bbb92ddb4c207404e
Runtime output
Length = 4
(
1 => 1,
2 => 2,
3 => 3,
4 => 4
)
Length = 4
(
1 => 9999,
2 => 9999,
3 => 9999,
4 => 9999
)
In this case, when writing <> for a component, the value specified in the De-
fault_Component_Value aspect is used.
Finally, we might want to use both Default_Value (which we discussed previously
(page 334)) and Default_Component_Value aspects at the same time. In this case, the
value specified in the Default_Component_Value aspect has higher priority:
6 type Integer_Array is
7 array (Positive range <>) of Value
8 with Default_Component_Value => 9999;
9
12 end Integer_Arrays;
5 procedure Show_Array_Aggregates is
6 N : Integer_Array (1 .. 4);
7 begin
8 N := [for I in N'Range => Value (I)];
9 Display (N);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Array_Aggregates.Array_
↪Aggregates_2
MD5: c5b6d45576d59e2d3ba1634953c58b02
Runtime output
Length = 4
(
1 => 1,
2 => 2,
3 => 3,
4 => 4
)
Length = 4
(
1 => 9999,
2 => 9999,
3 => 9999,
4 => 9999
)
Extension aggregates provide a convenient way to express an aggregate for a type that
extends — adds components to — some existing type (the "ancestor"). Although mainly a
matter of convenience, an extension aggregate is essential when we want to express an
aggregate for an extension of a private ancestor type, that is, when we don't have compile-
time visibility to the ancestor type's components.
Before we discuss extension aggregates in more detail, though, let's start with a simple
use-case. Let's say we have:
• an object A of tagged type T1, and
• an object B of tagged type T2, which extends T1.
We can initialize object B by:
• copying the T1 specific information from A to B, and
• initializing the T2 specific components of B.
We can translate the description above to the following code:
A : T1;
B : T2;
begin
T1 (B) := A;
B.Extended_Component_1 := Some_Value;
-- [...]
Here, we use T1 (B) to select the ancestor view of object B, and we copy all the information
from A to this part of B. Then, we initialize the remaining components of B. We'll elaborate
on this kind of assignments later on.
Example: Points
To present a more concrete example, let's start with a package that defines one, two and
three-dimensional point types:
21 end Points;
23 end Points;
MD5: 0acc05ae2310ab4ba038dfdb6bae0495
Let's now focus on the Show_Points procedure below, where we initialize a two-dimensional
point using a one-dimensional point.
3 procedure Show_Points is
4 P_1D : Point_1D;
5 P_2D : Point_2D;
6 begin
7 P_1D := (X => 0.5);
8 Display (P_1D);
9
13 P_2D.Y := 0.7;
14
15 Display (P_2D);
16 end Show_Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: 68ae6fa8e6f779aebea97085bd75e082
Runtime output
(X => 5.00000E-01)
(X => 5.00000E-01, Y => 7.00000E-01)
In this example, we're initializing P_2D using the information stored in P_1D. By writing
Point_1D (P_2D) on the left side of the assignment, we specify that we want to limit our
focus on the Point_1D view of the P_2D object. Then, we assign P_1D to the Point_1D
view of the P_2D object. This assignment initializes the X component of the P_2D object.
The Point_2D specific components are not changed by this assignment. (In other words,
this is equivalent to just writing P_2D.X := P_1D.X, as the Point_1D type only has the X
component.) Finally, in the next line, we initialize the Y component with 0.7.
Note that, in the assignment to P_1D, we use a record aggregate. Extension aggregates
are similar to record aggregates, but they include the with keyword — for example: (Obj1
with Y => 0.5). This allows us to assign to an object with information from another object
Obj1 of a parent type and, in the same expression, set the value of the Y component of the
type extension.
Let's rewrite the previous Show_Points procedure using extension aggregates:
3 procedure Show_Points is
4 P_1D : Point_1D;
5 P_2D : Point_2D;
6 begin
7 P_1D := (X => 0.5);
8 Display (P_1D);
9
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: 4d03f6a565126b602d6f21fe5ee6dd27
Runtime output
(X => 5.00000E-01)
(X => 5.00000E-01, Y => 7.00000E-01)
When we write P_2D := (P_1D with Y => 0.7), we're initializing P_2D using:
• the information from the P_1D object — of Point_1D type, which is an ancestor of the
Point_2D type —, and
• the information from the record component association list for the remaining com-
ponents of the Point_2D type. (In this case, the only remaining component of the
Point_2D type is Y.)
We could also specify the type of the extension aggregate. For example, in the previous as-
signment to P_2D, we could write Point_2D'(...) to indicate that we expect the Point_2D
type for the extension aggregate.
Also, we don't have to use named association in extension aggregates. We could just use
positional association instead. Therefore, we could simplify the assignment to P_2D in the
previous example by just writing:
We can use extension aggregates for descendants of the Point_2D type as well. For exam-
ple, let's extend our previous code example by declaring an object of Point_3D type (called
P_3D) and use extension aggregates in assignments to this object:
3 procedure Show_Points is
4 P_1D : Point_1D;
5 P_2D : Point_2D;
6 P_3D : Point_3D;
7 begin
8 P_1D := (X => 0.5);
9 Display (P_1D);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: 2ec6831557c43f697bffce8496962b53
Runtime output
(X => 5.00000E-01)
(X => 5.00000E-01, Y => 7.00000E-01)
(X => 5.00000E-01, Y => 7.00000E-01, Z => 3.00000E-01)
(X => 5.00000E-01, Y => 1.00000E-01, Z => 1.00000E-01)
In the first assignment to P_3D in the example above, we're initializing this object with
information from P_2D and specifying the value of the Z component. Then, in the next
assignment to the P_3D object, we're using an aggregate with information from P_1 and
specifying values for the Y and Z components. (Just as a reminder, we can write Y | Z =>
0.1 to assign 0.1 to both Y and Z components.)
with others
Other versions of extension aggregates are possible as well. For example, we can combine
keywords and write with others to focus on all remaining components of an extension
aggregate.
3 procedure Show_Points is
4 P_1D : Point_1D;
5 P_2D : Point_2D;
6 P_3D : Point_3D;
7 begin
8 P_1D := (X => 0.5);
9 P_2D := (P_1D with Y => 0.7);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: 0594586fc59ead106258cef8682927e9
Runtime output
In this example, the first assignment to P_3D has an aggregate with information from P_1D,
while the remaining components — in this case, Y and Z — are just set to 0.6.
Continuing with this example, in the next assignment to P_3D, we're using information from
P_2 in the extension aggregate. This covers the Point_2D part of the P_3D object — compo-
nents X and Y, to be more specific. The Point_3D specific components of P_3D — component
Z in this case — receive their corresponding default value. In this specific case, however,
we haven't specified a default value for component Z in the declaration of the Point_3D
type, so we cannot rely on any specific value being assigned to that component when using
others => <>.
We can also use extension aggregates with null records. Let's focus on the P_3D_Ext object
of Point_3D_Ext type. This object is declared in the Show_Points procedure of the next
code example.
6 end Points.Extensions;
4 procedure Show_Points is
5 P_3D : Point_3D;
6 P_3D_Ext : Point_3D_Ext;
7 begin
8 P_3D := (X => 0.0, Y => 0.5, Z => 0.4);
9
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: 8ec3ddb3a1f2a6e550ac4d622e97124c
Runtime output
In the examples above, we've been initializing objects of descendent types by using objects
of ascending types in extension aggregates. We could, however, do the opposite and initial-
ize objects of ascending types using objects of descendent type in extension aggregates.
Consider this code example:
3 procedure Show_Points is
4 P_2D : Point_2D;
5 P_3D : Point_3D;
6 begin
7 P_3D := (X => 0.5, Y => 0.7, Z => 0.3);
8 Display (P_3D);
9
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Extension_Aggregates.Extension_
↪Aggregate_Points
MD5: ae5e88a36c58b1eb495d5ba8752e50e7
Runtime output
Here, we're using Point_1D (P_3D) to select the Point_1D view of an object of Point_3D
type. At this point, we have specified the Point_1D part of the aggregate, so we still have
to specify the remaining components of the Point_2D type — the Y component, to be more
specific. When we do that, we get the appropriate aggregate for the Point_2D type. In
summary, by carefully selecting the appropriate view, we're able to initialize an object of
ascending type (Point_2D), which contains less components, using an object of a descen-
dent type (Point_3D), which contains more components.
Previously, we've discussed extension aggregates (page 472), which are used to assign an
object Obj_From of a tagged type to an object Obj_To of a descendent type.
We may want also to assign an object Obj_From of to an object Obj_To of the same type, but
change some of the components in this assignment. To do this, we use delta aggregates.
17 end Points;
12 end Points;
5 procedure Show_Points is
6 P1, P2, P3 : Point_3D;
7 begin
8 P1 := (X => 0.5, Y => 0.7, Z => 0.3);
9 Display (P1);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
(continues on next page)
Runtime output
Here, we assign P1 to P2, but change the X component. Also, we assign P1 to P3, but change
the X and Y components.
We can use class-wide types with delta aggregates. Consider this example:
5 procedure Show_Points is
6
7 P_3D : Point_3D;
8
13 begin
14 P_3D := [X => 0.1, Y => 0.2, Z => 0.3];
15 Display (P_3D);
16
20 end Show_Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
↪Aggregates_Tagged
MD5: 30e62d564d1b35829a5002223966c101
Runtime output
In this example, the Reset function returns an object of Point_2D'Class where all compo-
nents of Point_2D'Class type are zero. We call the Reset function for the P_3D object of
Point_3D type, so that only the Z component remains untouched.
Note that we use the syntax X | Y in the body of the Reset function and assign the same
value to both components.
3 procedure Show_Points is
4
5 P_3D : Point_3D;
6
7 procedure Reset
8 (P_2D : in out Point_2D'Class) is
9 begin
10 Point_2D (P_2D) := (others => 0.0);
11 end Reset;
12
13 begin
14 P_3D := (X => 0.1, Y => 0.2, Z => 0.3);
15 Display (P_3D);
16
17 Reset (P_3D);
18 Display (P_3D);
19
20 end Show_Points;
The examples above use tagged types. We can also use delta aggregates with non-tagged
types. Let's rewrite the Points package and convert Point_3D to a non-tagged record type.
11 end Points;
12 end Points;
5 procedure Show_Points is
6 P1, P2, P3 : Point_3D;
7 begin
8 P1 := (X => 0.5, Y => 0.7, Z => 0.3);
9 Display (P1);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
↪Aggregates_Non_Tagged
MD5: 71a3b76ee1988ddea7246d0b8f897865
Runtime output
In this example, Point_3D is a non-tagged type. Note that we haven't changed anything in
the Show_Points procedure: it still works as it did with tagged types.
We can use delta aggregates for arrays. Let's change the declaration of Point_3D and use
an array to represent a 3-dimensional point:
3 type Float_Array is
4 array (Positive range <>) of Float;
5
10 end Points;
16 end Points;
5 procedure Show_Points is
6 P1, P2, P3 : Point_3D;
7 begin
8 P1 := [0.5, 0.7, 0.3];
9 Display (P1);
10
18 Display (P3);
19 end Show_Points;
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
↪Aggregates_Array
MD5: d32ba51746d7db9cd30f183e64ab0017
Runtime output
The implementation of Show_Points in this example is very similar to the version where
use a record type. In this case, we:
• assign P1 to P2, but change the first component, and
• we assign P1 to P3, but change the first and second components.
Using slices
In the assignment to P3, we can either specify each component of the delta individually or
use a slice: both forms are equivalent. Also, we can use slices to assign the same number
to multiple components:
5 procedure Show_Points is
6 P1, P3 : Point_3D;
7 begin
8 P1 := [0.5, 0.7, 0.3];
9 Display (P1);
10
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
↪Aggregates_Array
MD5: 6d1db1634c42a885f7bfce7f7eecc359
Runtime output
In this example, we're assigning P1 to P3, but resetting all components of the array starting
by the second one.
Multiple components
3 type Float_Array is
4 array (Positive range <>) of Float;
5
8 end Float_Arrays;
8 Put ("(");
9 for I in P'Range loop
10 Put (I'Image
11 & " => "
12 & P (I)'Image);
13 end loop;
14 Put_Line (")");
15
16 end Display;
17
18 end Float_Arrays;
5 procedure Show_Multiple_Delta_Slices is
6
9 begin
10 P1 := [1.0, 2.0, 3.0, 4.0, 5.0];
11 Display (P1);
12
Project: Courses.Advanced_Ada.Data_Types.Aggregates.Delta_Aggregates.Delta_
↪Aggregates_Array
MD5: 4c2860616777428618d1100280699ec2
Runtime output
In this example, we have two arrays P1 and P2 of Float_Array type. We assign P1 to P2,
but change:
• the second to the last-but-two components to 0.0, and
• the last-but-one and last components to 0.2.
73 http://www.ada-auth.org/standards/22rm/html/RM-4-3-4.html
25.5 Arrays
In the Introduction to Ada course (page 78), we've seen that we can declare array types
whose bounds are not fixed: in that case, the bounds are provided when creating objects
of those types. For example:
3 type Measurements is
4 array (Positive range <>) of Float;
5 -- ^ Bounds are of type Positive,
6 -- but not known at this point.
7
8 end Measurement_Defs;
5 procedure Show_Measurements is
6 M : Measurements (1 .. 10);
7 -- ^ Providing bounds here!
8 begin
9 Put_Line ("First index: " & M'First'Image);
10 Put_Line ("Last index: " & M'Last'Image);
11 end Show_Measurements;
Project: Courses.Advanced_Ada.Data_Types.Arrays.Unconstrained_Arrays.Unconstrained_
↪Array_Example
MD5: a5cdc74dd61e36476431cf675452d1d5
Build output
Runtime output
First index: 1
Last index: 10
In this example, the Measurements array type from the Measurement_Defs package is un-
constrained. In the Show_Measurements procedure, we declare a constrained object (M) of
this type.
The Introduction to Ada course (page 79) also highlights the fact that the bounds are fixed
once an object is declared:
Although different instances of the same unconstrained array type can have dif-
ferent bounds, a specific instance has the same bounds throughout its lifetime.
This allows Ada to implement unconstrained arrays efficiently; instances can be
stored on the stack and do not require heap allocation as in languages like Java.
In the Show_Measurements procedure above, once we declare M, its bounds are fixed for
the whole lifetime of M. We cannot add another component to this array. In other words, M
will have 10 components for its whole lifetime.
If you need, however, the flexibility of increasing the length of an array, you could use
vectors instead. This is how we could rewrite the previous example using vectors:
4 package Measurement_Defs is
5
12 end Measurement_Defs;
5 procedure Show_Measurements is
6 use Measurement_Defs.Vectors;
7
Project: Courses.Advanced_Ada.Data_Types.Arrays.Unconstrained_Arrays.Unconstrained_
↪Array_Example
MD5: afec7a4b898392be4dd1f60e1519da88
Runtime output
First index: 1
Last index: 10
Adding element...
First index: 1
Last index: 11
So far, we've discussed unidimensional arrays, since they are very common in Ada. How-
ever, Ada also supports multidimensional arrays using the same facilities as for unidimen-
sional arrays. For example, we can use the First, Last, Range and Length attributes for
each dimension of a multidimensional array. This section presents more details on this
topic.
To create a multidimensional array, we simply separate the ranges of each dimension
with a comma. The following example presents the one-dimensional array A1, the two-
dimensional array A2 and the three-dimensional array A3:
75 http://www.ada-auth.org/standards/22rm/html/RM-3-6.html
76 http://www.ada-auth.org/standards/22rm/html/RM-A-18-2.html
Project: Courses.Advanced_Ada.Data_Types.Arrays.Multidimensional_Arrays.
↪Multidimensional_Arrays
MD5: 928243b293c67a078d729c3cac68bb92
The two-dimensional array A2 has 5 components in the first dimension and 10 components
in the second dimension. The three-dimensional array A3 has 2 components in the first
dimension, 5 components in the second dimension, and 10 components in the third dimen-
sion. Note that the ranges we've selected for A1, A2 and A3 are completely arbitrary. You
may select ranges for each dimension that are the most appropriate in the context of your
application. Also, the number of dimensions is not limited to three, so you could declare
higher-dimensional arrays if needed.
We can use the Length attribute to retrieve the length of each dimension. We use an integer
value in parentheses to specify which dimension we're referring to. For example, if we write
A'Length (2), we're referring to the length of the second dimension of a multidimensional
array A. Note that A'Length is equivalent to A'Length (1). The same equivalence applies
to other array-related attributes such as First, Last and Range.
Let's use the Length attribute for the arrays we declared in the Multidimen-
sional_Arrays_Decl package:
3 with Multidimensional_Arrays_Decl;
4 use Multidimensional_Arrays_Decl;
5
6 procedure Show_Multidimensional_Arrays is
7 begin
8 Put_Line ("A1'Length: "
9 & A1'Length'Image);
10 Put_Line ("A1'Length (1): "
11 & A1'Length (1)'Image);
12 Put_Line ("A2'Length (1): "
13 & A2'Length (1)'Image);
14 Put_Line ("A2'Length (2): "
15 & A2'Length (2)'Image);
16 Put_Line ("A3'Length (1): "
17 & A3'Length (1)'Image);
18 Put_Line ("A3'Length (2): "
19 & A3'Length (2)'Image);
20 Put_Line ("A3'Length (3): "
21 & A3'Length (3)'Image);
22 end Show_Multidimensional_Arrays;
Project: Courses.Advanced_Ada.Data_Types.Arrays.Multidimensional_Arrays.
↪Multidimensional_Arrays
MD5: 70b9b8df7e46302b92613fa484ef71ca
Runtime output
A1'Length: 10
A1'Length (1): 10
A2'Length (1): 5
A2'Length (2): 10
A3'Length (1): 2
A3'Length (2): 5
A3'Length (3): 10
As this simple example shows, we can easily retrieve the length of each dimension. Also,
as we've just mentioned, A1'Length is equal to A1'Length (1).
Let's consider an application where we make hourly measurements for the first 12 hours
of the day, on each day of the week. We can create a two-dimensional array type called
Measurements to store this data. Also, we can have three procedures for this array:
• Show_Indices, which presents the indices (days and hours) of the two-dimensional
array;
• Show_Values, which presents the values stored in the array; and
• Reset, which resets each value of the array.
This is the complete code for this application:
3 type Days is
4 (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
5
10 type Measurements is
11 array (Days, Hours) of Measurement;
12
19 end Measurement_Defs;
27 procedure Set_IO_Defaults is
28 begin
29 H_IO.Default_Width := 5;
30
31 M_IO.Default_Fore := 1;
32 M_IO.Default_Aft := 2;
33 M_IO.Default_Exp := 0;
34 end Set_IO_Defaults;
35 begin
36 Set_IO_Defaults;
37
61 end Measurement_Defs;
3 procedure Show_Measurements is
4 M : Measurements;
5 begin
6 Reset (M);
7 Show_Indices (M);
8 Show_Values (M);
9 end Show_Measurements;
MD5: bcffa3913007bd9152149ad9616842b8
Runtime output
---- Indices ----
MON 0 1 2 3 4 5 6 7 8 9 10 11
TUE 0 1 2 3 4 5 6 7 8 9 10 11
(continues on next page)
We recommend that you spend some time analyzing this example. Also, we'd like to high-
light the following aspects:
• We access a value from a multidimensional array by using commas to separate the
index values within the parentheses. For example: M (D, H) allows us to access the
value on day D and hour H from the multidimensional array M.
• To loop over the multidimensional array M, we write for D in M'Range (1) loop and
for H in M'Range (2) loop for the first and second dimensions, respectively.
• To reset all values of the multidimensional array, we use an aggregate with this form:
(others => (others => 0.0)).
Previously, we've discussed unconstrained arrays for the unidimensional case. It's possible
to declare unconstrained multidimensional arrays as well. For example:
10 end Multidimensional_Arrays_Decl;
MD5: 8637e93db355fddafa3ffa5ce453a0e1
Here, we're declaring the one-dimensional type F1, the two-dimensional type F2 and the
three-dimensional type F3.
77 http://www.ada-auth.org/standards/22rm/html/RM-3-6.html
As is the case with unidimensional arrays, we must specify the bounds when declaring
objects of unconstrained multidimensional array types:
3 with Multidimensional_Arrays_Decl;
4 use Multidimensional_Arrays_Decl;
5
6 procedure Show_Multidimensional_Arrays is
7 A1 : F1 (1 .. 2);
8 A2 : F2 (1 .. 4, 10 .. 20);
9 A3 : F3 (2 .. 3, 1 .. 5, 1 .. 2);
10 begin
11 Put_Line ("A1'Length (1): "
12 & A1'Length (1)'Image);
13 Put_Line ("A2'Length (1): "
14 & A2'Length (1)'Image);
15 Put_Line ("A2'Length (2): "
16 & A2'Length (2)'Image);
17 Put_Line ("A3'Length (1): "
18 & A3'Length (1)'Image);
19 Put_Line ("A3'Length (2): "
20 & A3'Length (2)'Image);
21 Put_Line ("A3'Length (3): "
22 & A3'Length (3)'Image);
23 end Show_Multidimensional_Arrays;
Project: Courses.Advanced_Ada.Data_Types.Arrays.Multidimensional_Arrays.
↪Unconstrained_Multidimensional_Arrays
MD5: 9fb007abbfe238345d80cb315bb834c9
Build output
Runtime output
A1'Length (1): 2
A2'Length (1): 4
A2'Length (2): 11
A3'Length (1): 2
A3'Length (2): 5
A3'Length (3): 2
Arrays of arrays
It's important to distinguish between multidimensional arrays and arrays of arrays. Both
are supported in Ada, but they're very distinct from each other. We can create an array of
an array by first specifying a one-dimensional array type T1, and then specifying another
one-dimensional array type T2 where each component of T2 is of T1 type:
3 type T1 is
4 array (Positive range <>) of Float;
5
6 type T2 is
7 array (Positive range <>) of T1 (1 .. 10);
8 -- ^^^^^^^
9 -- bounds must be set!
10
11 end Array_Of_Arrays_Decl;
Note that, in the declaration of T2, we must set the bounds for the T1 type. This is a major
difference to multidimensional arrays, which allow for unconstrained ranges in multiple
dimensions.
We can rewrite the previous application for measurements using arrays of arrays. This is
the adapted code:
3 type Days is
4 (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
5
10 type Hourly_Measurements is
11 array (Hours) of Measurement;
12
13 type Measurements is
14 array (Days) of Hourly_Measurements;
15
22 end Measurement_Defs;
12 for H in M (D)'First ..
13 M (D)'Last - 1
14 loop
15 Put (H'Image & " ");
16 end loop;
17 Put_Line (M (D)'Last'Image);
18 end loop;
19 end Show_Indices;
20
27 procedure Set_IO_Defaults is
28 begin
29 H_IO.Default_Width := 5;
30
31 M_IO.Default_Fore := 1;
32 M_IO.Default_Aft := 2;
33 M_IO.Default_Exp := 0;
34 end Set_IO_Defaults;
35 begin
36 Set_IO_Defaults;
37
61 end Measurement_Defs;
3 procedure Show_Measurements is
4 M : Measurements;
5 begin
6 Reset (M);
7 Show_Indices (M);
8 Show_Values (M);
9 end Show_Measurements;
Project: Courses.Advanced_Ada.Data_Types.Arrays.Array_Of_Arrays.Multidimensional_
↪Measurements
MD5: 5cb66bbb1890787b7c023406b2cafb4d
Runtime output
Again, we recommend that you spend some time analyzing this example and comparing it
to the previous version that uses multidimensional arrays. Also, we'd like to highlight the
following aspects:
• We access a value from an array of arrays by specifying the index of each array sep-
arately. For example: M (D) (H) allows us to access the value on day D and hour H
from the array of arrays M.
• To loop over an array of arrays M, we write for D in M'Range loop for the first level
of M and for H in M (D)'Range loop for the second level of M.
• Resetting all values of an array of arrays is very similar to how we do it for multidi-
mensional arrays. In fact, we can still use an aggregate with this form: (others =>
(others => 0.0)).
25.6 Strings
We've seen many source-code examples so far that includes strings. In most of them, we
were using the standard string type: String. This type is useful for the common use-
case of displaying messages or dealing with information in plain English. Here, we define
"plain English" as the use of the language that avoids French accents or German umlaut,
for example, and doesn't make use of any characters in non-Latin alphabets.
There are two additional string types in Ada: Wide_String, and Wide_Wide_String. These
types are particularly important when dealing with textual information in non-standard En-
glish, or in various other languages, non-Latin alphabets and special symbols.
These string types use different bit widths for their characters. This becomes more apparent
when looking at the type definitions:
type String is
array (Positive range <>) of Character;
type Wide_String is
array (Positive range <>) of Wide_Character;
type Wide_Wide_String is
array (Positive range <>) of
Wide_Wide_Character;
The following table shows the typical bit-width of each character of the string types:
Character 8 bits
Wide_Character 16 bits
Wide_Wide_Character 32 bits
3 procedure Show_Wide_Char_Types is
4 begin
5 Put_Line ("Character'Size: "
6 & Integer'Image
7 (Character'Size));
8 Put_Line ("Wide_Character'Size: "
9 & Integer'Image
10 (Wide_Character'Size));
11 Put_Line ("Wide_Wide_Character'Size: "
12 & Integer'Image
13 (Wide_Wide_Character'Size));
14 end Show_Wide_Char_Types;
Project: Courses.Advanced_Ada.Data_Types.Strings.Wide_Wide-Wide_Strings.Wide_Char_
↪Types
MD5: a0e9fb9e8d43e9fa707dc8c57f7562f8
Runtime output
Character'Size: 8
Wide_Character'Size: 16
Wide_Wide_Character'Size: 32
5 procedure Show_Wide_String_Types is
6 package TI renames Ada.Text_IO;
7 package WTI renames Ada.Wide_Text_IO;
8 package WWTI renames Ada.Wide_Wide_Text_IO;
9
Project: Courses.Advanced_Ada.Data_Types.Strings.Wide_Wide-Wide_Strings.Wide_
↪String_Types
MD5: 137816c6fd78add34287a72e45cf4fb7
Runtime output
String: hello
Length: 5
Size: 40
Component_Size: 8
------------------------
Wide string: hello
Length: 5
Size: 80
Component_Size: 16
------------------------
Wide-wide string: hello
Length: 5
Size: 160
Component_Size: 32
------------------------
Here, all strings (S, WS and WWS) have the same length of 5 characters. However, the size
of each character is different — thus, each string has a different overall size.
The recommendation is to use the String type when the textual information you're pro-
cessing is in standard English. In case any kind of internationalization is needed, using
Wide_Wide_String is probably the best choice, as it covers all possible use-cases.
Text I/O
Note that, in the previous example, we were using different versions of the Ada.Text_IO
package depending on the string type we were using:
• Ada.Text_IO for objects of String type,
• Ada.Wide_Text_IO for objects of Wide_String type,
• Ada.Wide_Wide_Text_IO for objects of Wide_Wide_String type.
In that example, we were also using package renaming to differentiate among those pack-
ages.
Similarly, there are different versions of text I/O packages for individual types. For example,
if we want to display the value of a Long_Integer variable based on the Wide_Wide_String
type, we can select the Ada.Long_Integer_Wide_Wide_Text_IO package. In fact, the list
of packages resulting from the combination of those types is quite long:
78 http://www.ada-auth.org/standards/22rm/html/RM-3-6-3.html
Integer
• Ada.Integer_Text_IO
• Ada.Integer_Wide_Text_IO
• Ada.Integer_Wide_Wide_Text_IO
Long_Integer
• Ada.Long_Integer_Text_IO
• Ada.Long_Integer_Wide_Text_IO
• Ada.Long_Integer_Wide_Wide_Text_IO
Long_Long_Integer
• Ada.Long_Long_Integer_Text_IO
• Ada.Long_Long_Integer_Wide_Text_IO
• Ada.Long_Long_Integer_Wide_Wide_Text_IO
Float
• Ada.Float_Text_IO
• Ada.Float_Wide_Text_IO
• Ada.Float_Wide_Wide_Text_IO
Long_Float
• Ada.Long_Float_Text_IO
• Ada.Long_Float_Wide_Text_IO
• Ada.Long_Float_Wide_Wide_Text_IO
Long_Long_Float
• Ada.Long_Long_Float_Text_IO
• Ada.Long_Long_Float_Wide_Text_IO
• Ada.Long_Long_Float_Wide_Wide_Text_IO
Also, there are different versions of the generic packages Integer_IO and Float_IO:
Integer types
• Ada.Text_IO.Integer_IO
• Ada.Wide_Text_IO.Integer_IO
• Ada.Wide_Wide_Text_IO.
Integer_IO
Real types
• Ada.Text_IO.Float_IO
• Ada.Wide_Text_IO.Float_IO
• Ada.Wide_Wide_Text_IO.Float_IO
As we've just seen, we have different versions of the Ada.Text_IO package. The same
applies to string handling packages. As we've seen in the Introduction to Ada course
(page 239), we can use the Ada.Strings.Fixed and Ada.Strings.Maps packages for string
handling. For other formats, we have these packages:
• Ada.Strings.Wide_Fixed,
• Ada.Strings.Wide_Wide_Fixed,
• Ada.Strings.Wide_Maps,
• Ada.Strings.Wide_Wide_Maps.
Let's look at this example (page 240) from the Introduction to Ada course, which we adapted
for wide-wide strings:
3 with Ada.Strings.Wide_Wide_Fixed;
4 use Ada.Strings.Wide_Wide_Fixed;
5
6 with Ada.Strings.Wide_Wide_Maps;
7 use Ada.Strings.Wide_Wide_Maps;
8
9 with Ada.Wide_Wide_Text_IO;
10 use Ada.Wide_Wide_Text_IO;
11
12 procedure Show_Find_Words is
13
14 S : constant Wide_Wide_String :=
15 "Hello" & 3 * " World";
16 F : Positive;
17 L : Natural;
18 I : Natural := 1;
19
20 Whitespace : constant
21 Wide_Wide_Character_Set :=
22 To_Set (' ');
23 begin
24 Put_Line ("String: " & S);
25 Put_Line ("String length: "
26 & Integer'Wide_Wide_Image
27 (S'Length));
28
38 exit when L = 0;
39
44 I := L + 1;
45 end loop;
46
47 end Show_Find_Words;
Project: Courses.Advanced_Ada.Data_Types.Strings.Wide_Wide-Wide_Strings.Wide_Wide_
↪String_Handling
MD5: 3b5a4d61e6dc5bd16e85f85580ad82ae
Runtime output
In this example, we're using the Find_Token procedure to find the words from the phrase
stored in the S constant. All the operations we're using here are similar to the ones for
String type, but making use of the Wide_Wide_String type instead. (We talk about the
Wide_Wide_Image attribute later on (page 514).)
We've seen in the Introduction to Ada course that other kinds of String types are available.
For example, we can use bounded (page 244) and unbounded strings (page 246) — those
correspond to the Bounded_String and Unbounded_String types.
Those kinds of string types are available for Wide_String, and Wide_Wide_String. The
following table shows the available types and corresponding packages:
84 http://www.ada-auth.org/standards/22rm/html/RM-A-4-6.html
85 http://www.ada-auth.org/standards/22rm/html/RM-A-4-7.html
86 http://www.ada-auth.org/standards/22rm/html/RM-A-4-8.html
Type Package
Bounded_Wide_String Ada.Strings.Wide_Bounded
Bounded_Wide_Wide_String Ada.Strings.Wide_Wide_Bounded
Unbounded_Wide_String Ada.Strings.Wide_Unbounded
Unbounded_Wide_Wide_String Ada.Strings.Wide_Wide_Unbounded
The same applies to text I/O for those strings. For the standard case, we have Ada.
Text_IO.Bounded_IO for the Bounded_String type and Ada.Text_IO.Unbounded_IO for
the Unbounded_String type.
For wider string types, we have:
Bounded_Wide_String Ada.Wide_Text_IO.Wide_Bounded_IO
Bounded_Wide_Wide_String Ada.Wide_Wide_Text_IO.Wide_Wide_Bounded_IO
Unbounded_Wide_String Ada.Wide_Text_IO.Wide_Unbounded_IO
Unbounded_Wide_Wide_String Ada.Wide_Wide_Text_IO.Wide_Wide_Unbounded_IO
4 with Ada.Wide_Wide_Text_IO.Wide_Wide_Unbounded_IO;
5 use Ada.Wide_Wide_Text_IO.Wide_Wide_Unbounded_IO;
6
7 procedure Show_Unbounded_Wide_Wide_String is
8 S : Unbounded_Wide_Wide_String
9 := To_Unbounded_Wide_Wide_String ("Hello");
10 begin
11 S := S & Wide_Wide_String'(" hello");
12 Put_Line ("Unbounded wide-wide string: " & S);
13 end Show_Unbounded_Wide_Wide_String;
MD5: 0d369270e2408b3f1cc8284c13fca806
Runtime output
Unbounded wide-wide string: Hello hello
In this example, we're declaring a variable S and initializing it with the word "Hello." Then,
we're concatenating it with " hello" and displaying it. All the operations we're using here
are similar to the ones for Unbounded_String type, but they've been adapted for the Un-
bounded_Wide_Wide_String type.
Unicode is one of the most widespread standards for encoding writing systems other than
the Latin alphabet. It defines a format called Unicode Transformation Format (UTF)90 in
various versions, which vary according to the underlying precision, support for backwards-
compatibility and other requirements.
A common UTF format is UTF-8, which encodes strings using up to four (8-bit) bytes and is
backwards-compatible with the ASCII format. While encoding of ASCII characters requires
only one byte, Chinese characters require three bytes, for example.
In Ada applications, UTF-8 strings are indicated by using the UTF_8_String from the Ada.
Strings.UTF_Encoding package. In order to encode from and to UTF-8 strings, we can use
the Encode and Decode functions. Those functions are specified in the child packages of the
Ada.Strings.UTF_Encoding package. We select the appropriate child package depending on
the string type we're using, as you can see in the following table:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
6 with Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
7 use Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
8
9 with Ada.Strings.Wide_Wide_Unbounded;
10 use Ada.Strings.Wide_Wide_Unbounded;
11
12 procedure Show_WW_UTF_String is
13
14 function To_UWWS
(continues on next page)
88 http://www.ada-auth.org/standards/22rm/html/RM-A-4-8.html
89 http://www.ada-auth.org/standards/22rm/html/RM-A-11.html
90 https://unicode.org/faq/utf_bom.html#gen2
91 http://www.ada-auth.org/standards/22rm/html/RM-A-4-11.html
19 function To_WWS
20 (Source : Unbounded_Wide_Wide_String)
21 return Wide_Wide_String
22 renames To_Wide_Wide_String;
23
24 Hello_World_Arabic : constant
25 UTF_8_String := ";عالم" يا مرحبا
26 WWS_Hello_World_Arabic : constant
27 Wide_Wide_String :=
28 Decode (Hello_World_Arabic);
29
30 UWWS : Unbounded_Wide_Wide_String;
31 begin
32 UWWS := "Hello World: "
33 & To_UWWS (WWS_Hello_World_Arabic);
34
35 Show_WW_String : declare
36 WWS : constant Wide_Wide_String :=
37 To_WWS (UWWS);
38 begin
39 Put_Line ("Wide_Wide_String Length: "
40 & WWS'Length'Image);
41 Put_Line ("Wide_Wide_String Size: "
42 & WWS'Size'Image);
43 end Show_WW_String;
44
45 Put_Line
46 ("---------------------------------------");
47 Put_Line
48 ("Converting Wide_Wide_String to UTF-8...");
49
50 Show_UTF_8_String : declare
51 S_UTF_8 : constant UTF_8_String :=
52 Encode (To_WWS (UWWS));
53 begin
54 Put_Line ("UTF-8 String: "
55 & S_UTF_8);
56 Put_Line ("UTF-8 String Length: "
57 & S_UTF_8'Length'Image);
58 Put_Line ("UTF-8 String Size: "
59 & S_UTF_8'Size'Image);
60 end Show_UTF_8_String;
61
62 end Show_WW_UTF_String;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.WW_UTF_String
MD5: cecfb420bb804f42e7a65b793abcbef5
Runtime output
Wide_Wide_String Length: 26
Wide_Wide_String Size: 832
---------------------------------------
Converting Wide_Wide_String to UTF-8...
UTF-8 String: Hello World: عالم يا مرحبا
(continues on next page)
As you can see when running the last code example from the previous subsection, we have
different sizes and lengths depending on the string type:
The size needed for storing the string when using the Wide_Wide_String type is bigger than
the one when using the UTF_8_String type. This is expected, as the Wide_Wide_String
uses 32-bit characters, while the UTF_8_String type uses 8-bit codes to store the string in
a more efficient way (memory-wise).
The length of the string using the Wide_Wide_String type is equivalent to the number
of symbols we have in the original string: 26 characters / symbols. When using UTF-8,
however, we may need more 8-bit codes to represent one symbol from the original string,
so we may end up with a length value that is bigger than the actual number of symbols
from the original string — as it is the case in this source-code example.
This difference in sizes might not always be the case. In fact, the sizes match when encoding
a symbol in UTF-8 that requires four 8-bit codes. For example:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
6 with Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
7 use Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
8
9 procedure Show_UTF_8 is
10
15 begin
16 Put_Line ("Wide_Wide_String Length: "
17 & Symbol_WWS'Length'Image);
18 Put_Line ("Wide_Wide_String Size: "
19 & Symbol_WWS'Size'Image);
20 Put_Line ("UTF-8 String Length: "
21 & Symbol_UTF_8'Length'Image);
22 Put_Line ("UTF-8 String Size: "
23 & Symbol_UTF_8'Size'Image);
24 New_Line;
25 Put_Line ("UTF-8 String: "
26 & Symbol_UTF_8);
27 end Show_UTF_8;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.UTF_8
MD5: 67653dfd377f04b32421cf09b25939fe
Runtime output
Wide_Wide_String Length: 1
Wide_Wide_String Size: 32
UTF-8 String Length: 4
UTF-8 String Size: 32
UTF-8 String: 𝚡
In this case, both strings — using the Wide_Wide_String type or the UTF_8_String type
— have the same size: 32 bits. (Here, we're using the 𝚡 symbol from the Mathematical
Alphanumeric Symbols block92 , not the standard "x" from the Basic Latin block93 .)
In the past, it was common to use different character sets in text files when writing in
different (human) languages. By default, Ada source-code files are expected to use the
Latin-1 coding, which is a 8-bit character set.
Nowadays, however, using UTF-8 coding for text files — including source-code files — is
very common. If your Ada code only uses standard ASCII characters, but you're saving it
in a UTF-8 coded file, there's no need to worry about character sets, as UTF-8 is backwards
compatible with ASCII.
However, you might want to use Unicode symbols in your Ada source code to declare con-
stants — as we did in the previous sections — and store the source code in a UTF-8 coded
file. In this case, you need be careful about how this file is parsed by the compiler.
Let's look at this source-code example:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
8 Symbols_UTF_8 : constant
9 UTF_8_String := "♥♫";
10
11 begin
12 Put_Line ("UTF_8_String: "
13 & Symbols_UTF_8);
14
18 end Show_UTF_8_Strings;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.UTF_8_Strings
MD5: fd1aaff161a33365d15adca5bea7b277
Runtime output
UTF_8_String: ♥♫
Length: 6
4 procedure Show_WWS_Strings is
5
9 Symbols_WWS : constant
10 Wide_Wide_String := "♥♫";
11
12 begin
13 WWTIO.Put_Line ("Wide_Wide_String: "
14 & Symbols_WWS);
(continues on next page)
94 https://en.wikipedia.org/wiki/UTF-8
19 end Show_WWS_Strings;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.WWS_Strings_W8
MD5: 1e5e38e62b412de48d3fa4271bb48bf1
Runtime output
Wide_Wide_String: ♥♫
Length: 2
In this case, as mentioned above, if we store this source code in a text file using UTF-
8 format, we need to ensure that the UTF-8 coded symbols are correctly interpreted by
the compiler when it parses the text file. Otherwise, we might get unexpected behavior.
(Interpreting the characters in UTF-8 format as Latin-1 format is certainly an example of
what we want to avoid here.)
4 procedure Show_WWS_Strings is
5
9 Symbols_WWS : constant
10 Wide_Wide_String := "♥♫";
11
12 begin
13 WWTIO.Put_Line ("Wide_Wide_String: "
14 & Symbols_WWS);
15
19 end Show_WWS_Strings;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.WWS_Strings_W8
MD5: 1e5e38e62b412de48d3fa4271bb48bf1
Runtime output
Wide_Wide_String: ♥♫
Length: 2
Because the Wide_Wide_String type has 32-bit characters. we expect the length of
the string to match the number of symbols that we're using. Indeed, when running the
Show_WWS_Strings procedure, we see that the Symbols_WWS string has a length of two
characters, which matches the number of characters of the "♥♫" string.
When we use the -gnatW8 switch, GNAT converts the UTF-8-coded string ("♥♫") to UTF-32
format, so we get two 32-bit characters. It then uses the UTF-32-coded string to initialize
the Symbols_WWS string.
If we don't use the -gnatW8 switch, however, we get wrong results. Let's look at the same
example again without the switch:
4 procedure Show_WWS_Strings is
5
9 Symbols_WWS : constant
10 Wide_Wide_String := "♥♫";
11
12 begin
13 WWTIO.Put_Line ("Wide_Wide_String: "
14 & Symbols_WWS);
15
19 end Show_WWS_Strings;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.WWS_Strings_No_W8
MD5: 1e5e38e62b412de48d3fa4271bb48bf1
Runtime output
Wide_Wide_String: ♥♫
Length: 6
Now, the "♥♫" string is being interpreted as a string of six 8-bit characters. (In other words,
the UTF-8-coded string isn't converted to the UTF-32 format.) Each of those 8-bit characters
is then stored in a 32-bit character of the Wide_Wide_String type. This explains why the
Show_WWS_Strings procedure reports a length of 6 components for the Symbols_WWS string.
In a previous code example, we were assuming that the format that we use for the source-
code file is UTF-8. This allows us to simply use Unicode symbols directly in strings:
This approach, however, might not be portable. For example, if the compiler uses a different
string encoding for source-code files, it might interpret that Unicode character as something
else — or just throw a compilation error.
If you're afraid that format mismatches might happen in your compilation environment, you
may want to write strings in your code in a completely portable fashion, which consists in
entering the exact sequence of codes in bytes — using the Character'Val function — for
the symbols you want to use.
We can reuse parts of the previous example and replace the UTF-8 character with the cor-
responding UTF-8 code:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
6 procedure Show_UTF_8 is
7
8 Symbol_UTF_8 : constant
9 UTF_8_String :=
10 Character'Val (16#e2#)
11 & Character'Val (16#98#)
12 & Character'Val (16#85#);
13
14 begin
15 Put_Line ("UTF-8 String: "
16 & Symbol_UTF_8);
17 end Show_UTF_8;
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.UTF_8
MD5: 8ff02bc1793c0c5ac1ff24f62941af73
Runtime output
UTF-8 String: ★
Here, we use a sequence of three calls to the Character'Val(code) function for the UTF-8
code that corresponds to the "★" symbol.
So far, we've discussed the UTF-8 encoding scheme. However, other encoding schemes
exist and are supported as well. In fact, the Ada.Strings.UTF_Encoding package defines
three encoding schemes:
For example, instead of using UTF-8 encoding, we can use UTF-16 encoding — either in
the big-endian or in the little-endian version. To convert between UTF-8 and UTF-16 en-
coding schemes, we can make use of the conversion functions from the Ada.Strings.
UTF_Encoding.Conversions package.
To declare a UTF-16 encoded string, we can use one of the following data types:
• the 8-bit-character based UTF_String type, or
• the 16-bit-character based UTF_16_Wide_String type.
When using the 8-bit version, though, we have to specify the input and output schemes
when converting between UTF-8 and UTF-16 encoding schemes.
Let's see a code example that makes use of both UTF_String and UTF_16_Wide_String
types:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
6 with Ada.Strings.UTF_Encoding.Conversions;
7 use Ada.Strings.UTF_Encoding.Conversions;
8
9 procedure Show_UTF16_Types is
10 Symbols_UTF_8 : constant
11 UTF_8_String := "♥♫";
12
13 Symbols_UTF_16 : constant
14 UTF_16_Wide_String :=
15 Convert (Symbols_UTF_8);
16 -- ^ Calling Convert for UTF_8_String
17 -- to UTF_16_Wide_String conversion.
18
19 Symbols_UTF_16BE : constant
20 UTF_String :=
21 Convert (Item => Symbols_UTF_8,
22 Input_Scheme => UTF_8,
23 Output_Scheme => UTF_16BE);
24 -- ^ Calling Convert for UTF_8_String
25 -- to UTF_String conversion in UTF-16BE
26 -- encoding.
27 begin
28 Put_Line ("UTF_8_String: "
29 & Symbols_UTF_8);
30
37 Put_Line
38 ("UTF_String / UTF_16BE: "
39 & Convert
40 (Item => Symbols_UTF_16BE,
41 Input_Scheme => UTF_16BE,
42 Output_Scheme => UTF_8));
43 end Show_UTF16_Types;
Runtime output
UTF_8_String: ♥♫
UTF_16_Wide_String: ♥♫
UTF_String / UTF_16BE: ♥♫
In this example, we're declaring a UTF-8 encoded string and storing it in the Symbols_UTF_8
constant. Then, we're calling the Convert functions to convert between UTF-8 and UTF-16
encoding schemes. We're using two versions of this function:
• the Convert function that returns an object of UTF_16_Wide_String type for an input
of UTF_8_String type, and
• the Convert function that returns an object of UTF_String type for an input of
UTF_8_String type.
– In this case, we need to specify the input and output schemes (see Input_Scheme
and Output_Scheme parameters in the code example).
Previously, we've seen that the Ada.Strings.UTF_Encoding.Wide_Wide_Strings package
offers functions to convert between UTF-8 and the Wide_Wide_String type. The same kind
of conversion functions exist for UTF-16 strings as well. Let's look at this code example:
3 with Ada.Strings.UTF_Encoding;
4 use Ada.Strings.UTF_Encoding;
5
6 with Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
7 use Ada.Strings.UTF_Encoding.Wide_Wide_Strings;
8
9 with Ada.Strings.UTF_Encoding.Conversions;
10 use Ada.Strings.UTF_Encoding.Conversions;
11
12 procedure Show_WW_UTF16_String is
13 Symbols_UTF_16 : constant
14 UTF_16_Wide_String :=
15 Wide_Character'Val (16#2665#) &
16 Wide_Character'Val (16#266B#);
17 -- ^ Calling Wide_Character'Val
18 -- to specify the UTF-16 BE code
19 -- for "♥" and "♫".
20
21 Symbols_WWS : constant
22 Wide_Wide_String :=
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Strings.String_Encoding.WW_UTF_16_String
MD5: 900af8f5c6aad7303c3e49c1c4a68d73
Runtime output
UTF_16_Wide_String: ♥♫
Wide_Wide_String: ♥♫
In this example, we're calling the Wide_Character'Val function to specify the UTF-16 BE
code of the "♥" and "♫" symbols. We're then using the Decode function to convert between
the UTF_16_Wide_String and the Wide_Wide_String types.
Overview
In the Introduction to Ada (page 13) course, we've seen that the Image attribute returns
a string that contains a textual representation of an object. For example, we write Inte-
ger'Image (V) to get a string for the integer variable V:
3 procedure Show_Simple_Image is
4 V : Integer;
5 begin
6 V := 10;
7 Put_Line ("V: " & Integer'Image (V));
8 end Show_Simple_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Simple_Image
MD5: e38f6f1a0808f12bd53c1f3cf4983353
Runtime output
V: 10
Naturally, we can use the Image attribute with other scalar types. For example:
3 procedure Show_Simple_Image is
4 type Status is (Unknown, Off, On);
5
6 V : Float;
7 S : Status;
8 begin
9 V := 10.0;
10 S := Unknown;
11
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Simple_Image
MD5: d3369518b610b7bf6c8dcefdecdb0c44
Runtime output
V: 1.00000E+01
S: UNKNOWN
In this example, we retrieve a string representing the floating-point variable V. Also, we use
Status'Image (V) to retrieve a string representing the textual version of the Status.
3 procedure Show_Simple_Image is
4 V : Integer;
5 begin
6 V := 10;
7 Put_Line ("V: " & V'Image);
8
9 -- Equivalent to:
10 -- Put_Line ("V: " & Integer'Image (V));
11 end Show_Simple_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Simple_Image
MD5: c8b2e458de47b403568dd795b3d3fc24
95 http://www.ada-auth.org/standards/22rm/html/RM-4-10.html
Runtime output
V: 10
In this example, the Integer'Image (V) and V'Image forms are equivalent.
Although we've been talking only about the Image attribute, it's important to mention that
each of the wider versions of the string types also has a corresponding Image attribute. In
fact, this is the attribute for each string type:
4 procedure Show_Wide_Wide_Image is
5 F : Float;
6 begin
7 F := 100.0;
8 Put_Line ("F = "
9 & F'Wide_Wide_Image);
10 end Show_Wide_Wide_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Wide_Wide_Image
MD5: ff542ef93286529343466c27935d5c21
Runtime output
F = 1.00000E+02
In the previous code examples, we were using the Image attribute with scalar types, but
it isn't restricted to those types. In fact, we can also use this attribute when dealing with
non-scalar types. For example:
11 private
12
21 end Simple_Records;
6 with Simple_Records;
7 use Simple_Records;
8
9 procedure Show_Non_Scalar_Image is
10
11 procedure Free is
12 new Ada.Unchecked_Deallocation
13 (Object => Rec,
14 Name => Rec_Access);
15
16 R_A : Rec_Access :=
17 new Rec'(Init);
18
19 N_R : Null_Rec :=
20 (null record);
21 begin
22 R_A := new Rec'(Init);
23 N_R := (null record);
24
29 Free (R_A);
30 Put_Line ("R_A: " & R_A'Image);
31 end Show_Non_Scalar_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Non_Scalar_Image
MD5: d7d15e96a03c882995262a5cfca5e771
Runtime output
In the Show_Non_Scalar_Image procedure from this example, we display the access value
of R_A and the contents of the dereferenced access object (R_A.all). Also, we see the
indication that N_R is a null record and R_A is null after the call to Free.
Historically
Since Ada 2022, the Image attribute is available for all types. Prior to this version of the
language, it was only available for scalar types. (For other kind of types, programmers had
to use the Image attribute for each component of a record, for example.)
In fact, prior to Ada 2022, the Image attribute was described in the 3.5 Scalar Types96 section
of the Ada Reference Manual, as it was only applied to those types. Now, it is part of the
new Image Attributes97 section.
5 procedure Show_Array_Image is
6
7 type Float_Array is
8 array (Positive range <>) of Float;
9
13 begin
14 FA_3C := [1.0, 3.0, 2.0];
15 FA_Null := [];
16
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Array_Image
MD5: 2d3fcdd5e57451f08185618d357b705f
Runtime output
FA_3C:
[ 1.00000E+00, 3.00000E+00, 2.00000E+00]
FA_Null:
[]
96 http://www.ada-auth.org/standards/22rm/html/RM-3-5.html
97 http://www.ada-auth.org/standards/22rm/html/RM-4-10.html
In this example, we display the values of the three components of the FA_3C array. Also,
we display the null array FA_Null.
In addition to untagged types, we can also use the Image attribute with tagged types. For
example:
11 private
12
30 end Simple_Records;
7 procedure Show_Tagged_Image is
8 R : constant Rec := Init;
9 R_Class : constant Rec'Class := Rec'(Init);
10 R_C : constant Rec_Child := Init;
11 begin
12 Put_Line ("R: " & R'Image);
13 Put_Line ("R_Class: " & R_Class'Image);
14 Put_Line ("R_A: " & R_C'Image);
15 end Show_Tagged_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Tagged_Image
MD5: 164bd17c99115acafb09c99f40c1578c
Runtime output
R: {SIMPLE_RECORDS.RECobject}
R_Class: SIMPLE_RECORDS.REC'{SIMPLE_RECORDS.RECobject}
R_A: {SIMPLE_RECORDS.REC_CHILDobject}
In the Show_Tagged_Image procedure from this example, we display the contents of the R
object of Rec type and the R_Class object of Rec'Class type. Also, we display the contents
of the R_C object of the Rec_Child type, which is derived from the Rec type.
We can also apply the Image attribute to protected objects and tasks:
5 private
6 V : Float := Float (I);
7 end Protected_Float;
8
13 task type T is
14 entry Start;
15 end T;
16
17 end Simple_Tasking;
5 end Protected_Float;
6
9 end Protected_Null;
10
11 task body T is
12 begin
13 accept Start;
14 end T;
15
16 end Simple_Tasking;
7 procedure Show_Protected_Task_Image is
8
9 PF : Protected_Float (0);
10 PN : Protected_Null;
11 T1 : T;
12
13 begin
14 Put_Line ("PF: " & PF'Image);
15 Put_Line ("PN: " & PN'Image);
16 Put_Line ("T1: " & T1'Image);
17
18 T1.Start;
19 end Show_Protected_Task_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Image_Attribute.Protected_Task_
↪Image
MD5: 9d8c667015878eb14e5b3950a70b86b1
Runtime output
In this example, we display information about the protected object PF, the componentless
protected object PN and the task T1.
Overview
In the previous section, we discussed many details about the Image attribute. In the code
examples from that section, we've seen the default behavior of this attribute: the string
returned by the calls to Image was always in the format defined by the Ada standard.
In some situations, however, we might want to customize the string that is returned by the
Image attribute of a type T. Ada allows us to do that via the Put_Image aspect. This is what
we have to do:
1. Specify the Put_Image aspect for the type T and indicate a procedure with a specific
parameter profile — let's say, for example, a procedure named P.
2. Implement the procedure P and write the information we want to use into a buffer (by
calling the routines defined for Root_Buffer_Type, such as the Put procedure).
We can see these steps performed in the code example below:
3 with Ada.Strings.Text_Buffers;
4
5 package Show_Put_Image is
6
11 use Ada.Strings.Text_Buffers;
12
13 procedure Put_Image_T
14 (Buffer : in out Root_Buffer_Type'Class;
15 Arg : T);
16
17 end Show_Put_Image;
3 procedure Put_Image_T
4 (Buffer : in out Root_Buffer_Type'Class;
5 Arg : T) is
6 pragma Unreferenced (Arg);
7 begin
8 -- Call Put with customized
9 -- information
10 Buffer.Put ("<custom info>");
11 end Put_Image_T;
12
13 end Show_Put_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Put_Image.Simple_Put_Image
MD5: cbdd77a9e6cc30f3604c0901536d87aa
In the Show_Put_Image package, we use the Put_Image aspect in the declaration of the
T type. There, we indicate that the Image attribute shall use the Put_Image_T procedure
instead of the default version.
In the body of the Put_Image_T procedure, we implement our custom version of the Image
attribute. We do that by calling the Put procedure with the information we want to provide
in the Image attribute. Here, we access a buffer of Root_Buffer_Type type, which is defined
in the Ada.Strings.Text_Buffers package. (We discuss more about this package later on
(page 527).)
98 http://www.ada-auth.org/standards/22rm/html/RM-4-10.html
Let's see a complete example in which we use the Put_Image aspect and write useful in-
formation to the buffer:
3 with Ada.Strings.Text_Buffers;
4
5 package Custom_Numerics is
6
17 use Ada.Strings.Text_Buffers;
18
19 procedure Put_Float_Integer
20 (Buffer : in out Root_Buffer_Type'Class;
21 Arg : Float_Integer);
22
23 end Custom_Numerics;
3 procedure Put_Float_Integer
4 (Buffer : in out Root_Buffer_Type'Class;
5 Arg : Float_Integer) is
6 begin
7 -- Call Wide_Wide_Put with customized
8 -- information
9 Buffer.Wide_Wide_Put
10 ("(F : " & Arg.F'Wide_Wide_Image & ", "
11 & "I : " & Arg.I'Wide_Wide_Image & ")");
12 end Put_Float_Integer;
13
14 end Custom_Numerics;
7 procedure Show_Put_Image is
8 V : Float_Integer;
9 begin
10 V := (F => 100.2,
11 I => 100);
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Strings.Put_Image.Put_Image_Custom_
↪Numerics
MD5: 18d31150d7a9ff9af0359495543c011f
Runtime output
V = (F : 1.00200E+02, I : 100)
In the Custom_Numerics package of this example, we specify the Put_Image aspect and
indicate the Put_Float_Integer procedure. In that procedure, we display the information
of components F and I. Then, in the Show_Put_Image procedure, we use the Image attribute
for the V variable and see the information in the exact format we specified. (If you like to
see the default version of the Put_Image instead, you may comment out the Put_Image
aspect part in the declaration of Float_Integer.)
Note that we cannot override the Image attribute directly — there's no Image aspect that
we could specify. However, as we've just seen, we can do this indirectly by using our own
version of the Put_Image procedure for a type T.
The Image attribute of a type T makes use of the procedure indicated in the Put_Image
aspect. Let's say we have the following declaration:
When we then use the T'Image attribute in our code, the custom Put_Image_T procedure is
automatically called. This is a simplified example of how the Image function is implemented:
function Image (V : T)
return String is
Buffer : Custom_Buffer;
-- ^ of Root_Buffer_Type'Class
begin
-- Calling Put_Image procedure
-- for type T
Put_Image_T (Buffer, V);
If the Put_Image aspect of type T isn't specified, the default version is used. (We've seen
the default version of various types in the previous section (page 512) about the Image
attribute.)
Types that were derived from untagged types (or null extensions) make use of the
Put_Image procedure that was specified for their parent type — either a custom proce-
dure indicated in the Put_Image aspect or the default one. Naturally, if a derived type has
the Put_Image aspect, the procedure indicated in the aspect is used instead. For example:
3 with Ada.Strings.Text_Buffers;
4
5 package Untagged_Put_Image is
6
7 use Ada.Strings.Text_Buffers;
8
12 procedure Put_Image_T
13 (Buffer : in out Root_Buffer_Type'Class;
14 Arg : T);
15
21 procedure Put_Image_T_Derived_2
22 (Buffer : in out Root_Buffer_Type'Class;
23 Arg : T_Derived_2);
24
25 end Untagged_Put_Image;
3 procedure Put_Image_T
4 (Buffer : in out Root_Buffer_Type'Class;
5 Arg : T) is
6 pragma Unreferenced (Arg);
7 begin
8 Buffer.Wide_Wide_Put ("Put_Image_T");
9 end Put_Image_T;
10
11 procedure Put_Image_T_Derived_2
12 (Buffer : in out Root_Buffer_Type'Class;
13 Arg : T_Derived_2) is
14 pragma Unreferenced (Arg);
15 begin
16 Buffer.Wide_Wide_Put
17 ("Put_Image_T_Derived_2");
18 end Put_Image_T_Derived_2;
19
20 end Untagged_Put_Image;
7 procedure Show_Untagged_Put_Image is
8 Obj_T : T;
9 Obj_T_Derived_1 : T_Derived_1;
10 Obj_T_Derived_2 : T_Derived_2;
11 begin
12 Put_Line ("T'Image : "
13 & Obj_T'Image);
14 Put_Line ("T_Derived_1'Image : "
15 & Obj_T_Derived_1'Image);
16 Put_Line ("T_Derived_2'Image : "
17 & Obj_T_Derived_2'Image);
18 end Show_Untagged_Put_Image;
Project: Courses.Advanced_Ada.Data_Types.Strings.Put_Image.Untagged_Put_Image
MD5: b0a115967ec5f2deaea19967d22266b4
Runtime output
T'Image : Put_Image_T
T_Derived_1'Image : Put_Image_T
T_Derived_2'Image : Put_Image_T_Derived_2
In this example, we declare the type T and its derived types T_Derived_1 and T_Derived_2.
When running this code, we see that:
• T_Derived_1 makes use of the Put_Image_T procedure from its parent.
– Note that, if we remove the Put_Image aspect from the declaration of T, the de-
fault version of the Put_Image procedure is used for both T and T_Derived_1
types.
• T_Derived_2 makes use of the Put_Image_T_Derived_2 procedure, which was indi-
cated in the Put_Image aspect of that type, instead of its parent's procedure.
Types that are derived from a tagged type may also inherit the Put_Image aspect. However,
there are a couple of small differences in comparison to untagged types, as we can see in
the following example:
3 with Ada.Strings.Text_Buffers;
4
5 package Tagged_Put_Image is
6
7 use Ada.Strings.Text_Buffers;
8
14 procedure Put_Image_T
15 (Buffer : in out Root_Buffer_Type'Class;
16 Arg : T);
17
29 procedure Put_Image_T_Child_3
30 (Buffer : in out Root_Buffer_Type'Class;
31 Arg : T_Child_3);
32
33 end Tagged_Put_Image;
3 procedure Put_Image_T
4 (Buffer : in out Root_Buffer_Type'Class;
5 Arg : T) is
6 pragma Unreferenced (Arg);
7 begin
8 Buffer.Wide_Wide_Put ("Put_Image_T");
9 end Put_Image_T;
10
11 procedure Put_Image_T_Child_3
12 (Buffer : in out Root_Buffer_Type'Class;
13 Arg : T_Child_3) is
14 pragma Unreferenced (Arg);
15 begin
16 Buffer.Wide_Wide_Put
17 ("Put_Image_T_Child_3");
18 end Put_Image_T_Child_3;
19
20 end Tagged_Put_Image;
7 procedure Show_Tagged_Put_Image is
8 Obj_T : T;
9 Obj_T_Child_1 : T_Child_1;
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Strings.Put_Image.Tagged_Put_Image
MD5: 74d29ea54f1ad79fea7de2ad7c1dcb31
Runtime output
T'Image : Put_Image_T
--------------------
T_Child_1'Image :
(Put_Image_T with I1 => 0)
--------------------
T_Child_2'Image :
(Put_Image_T)
--------------------
T_Child_3'Image : Put_Image_T_Child_3
--------------------
T'Class'Image : TAGGED_PUT_IMAGE.T_CHILD_1'
(Put_Image_T with I1 => 0)
In this example, we declare the type T and its derived types T_Child_1, T_Child_2 and
T_Child_3. When running this code, we see that:
• for both T_Child_1 and T_Child_2, the parent's Put_Image aspect (the Put_Image_T
procedure) is called and its information is combined with the information from the type
extension;
– The information from the parent's Put_Image_T procedure is presented in an ag-
gregate syntax — in this case, this results in (Put_Image_T).
– For the T_Child_1 type, the I1 component of the type extension is displayed
by calling a default version of the Put_Image procedure for that component —
(Put_Image_T with I1 => 0) is displayed.
– For the T_Child_2 type, no additional information is displayed because this type
has a null extension.
• for the T_Child_3 type, the Put_Image_T_Child_3 procedure, which was indicated in
the Put_Image aspect of the type, is used.
Finally, class-wide types (such as T'Class) include additional information. Here, the tag of
the specific derived type is displayed first — in this case, the tag of the T_Child_1 type —
and then the actual information for the derived type is displayed.
In the previous section (page 519), we've seen that the first parameter of the procedure
indicated in the Put_Image aspect has the Root_Buffer_Type'Class type, which is defined
in the Ada.Strings.Text_Buffers package. In this section, we talk more about this type
and additional procedures associated with this type.
Overview
We use the Root_Buffer_Type'Class type to implement a universal text buffer that is used
to store and retrieve information about data types. Because this text buffer isn't associated
with specific data types, it is universal — in the sense that we can really use it for any data
type, regardless of the characteristics of this type.
In theory, we could use Ada's universal text buffer to implement applications that actually
process text in some form — for example, when implementing a text editor. However, in
general, Ada programmers are only expected to make use of the Root_Buffer_Type'Class
type when implementing a procedure for the Put_Image aspect. For this reason, we
won't discuss any kind of type derivation — or any other kind of usages of this type —
in this section. Instead, we'll just focus on additional subprograms from the Ada.Strings.
Text_Buffers package.
Additional procedures
In the previous section, we used the Put procedure — and the related Wide_Put and
Wide_Wide_Put procedures — from the Ada.Strings.Text_Buffers package. In addition
to these procedures, the package also includes:
• the New_Line procedure, which writes a new line marker to the text buffer;
• the Increase_Indent procedure, which increases the indentation in the text buffer;
and
• the Decrease_Indent procedure, which decreases the indentation in the text buffer.
The Ada.Strings.Text_Buffers package also includes the Current_Indent function,
which retrieves the current indentation counter.
Let's revisit an example from the previous section and use the procedures mentioned above:
3 with Ada.Strings.Text_Buffers;
4
5 package Custom_Numerics is
6
17 use Ada.Strings.Text_Buffers;
18
19 procedure Put_Float_Integer
20 (Buffer : in out Root_Buffer_Type'Class;
21 Arg : Float_Integer);
22
23 end Custom_Numerics;
3 procedure Put_Float_Integer
4 (Buffer : in out Root_Buffer_Type'Class;
5 Arg : Float_Integer) is
6 begin
7 Buffer.Wide_Wide_Put ("(");
8 Buffer.New_Line;
9
10 Buffer.Increase_Indent;
11
12 Buffer.Wide_Wide_Put
13 ("F : "
14 & Arg.F'Wide_Wide_Image);
15 Buffer.New_Line;
16
17 Buffer.Wide_Wide_Put
18 ("I : "
19 & Arg.I'Wide_Wide_Image);
20
21 Buffer.Decrease_Indent;
22 Buffer.New_Line;
23
24 Buffer.Wide_Wide_Put (")");
25 end Put_Float_Integer;
26
27 end Custom_Numerics;
7 procedure Show_Put_Image is
8 V : Float_Integer;
9 begin
10 V := (F => 100.2,
11 I => 100);
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Strings.Universal_Text_Buffer.Put_Image_
↪Custom_Numerics
MD5: af95f9fe4064e8a9d7aebe14d7f561f7
Runtime output
V = (
F : 1.00200E+02
I : 100
)
In the body of the Put_Float_Integer procedure, we're using the New_Line, In-
crease_Indent and Decrease_Indent procedures to improve the format of the string re-
turned by the Float_Integer'Image attribute. Using these procedures, you can create any
kind of output format for your custom type.
25.7 Numerics
In the Introduction to Ada course, we've seen that Ada has two kinds of integer type: signed
(page 47) and modular (page 50) types. For example:
6 end Num_Types;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Modular_Types.Modular_1
MD5: 2dff9fe22c6bbe52f964befccf68debf
In this section, we discuss two attributes of modular types: Modulus and Mod. We also
discuss operations on modular types.
100 http://www.ada-auth.org/standards/22rm/html/RM-3-5-4.html
Modulus Attribute
The Modulus attribute returns the modulus of the modular type as a universal integer value.
Let's get the modulus of the 32-bit Modular type that we've declared in the Num_Types
package of the previous example:
5 procedure Show_Modular is
6 Modulus_Value : constant := Modular'Modulus;
7 begin
8 Put_Line (Modulus_Value'Image);
9 end Show_Modular;
Runtime output
4294967296
Mod Attribute
Note: This section was originally written by Robert A. Duff and published as Gem #26:
The Mod Attribute101 .
Operations on signed integers can overflow: if the result is outside the base range, Con-
straint_Error will be raised. In our previous example, we declared the Signed_Integer
type:
type Signed_Integer is range 1 .. 1_000_000;
5 procedure Show_Modular is
6 X : Modular;
7 begin
8 X := 1;
9 Put_Line (X'Image);
(continues on next page)
101 https://www.adacore.com/gems/gem-26
11 X := -X;
12 Put_Line (X'Image);
13 end Show_Modular;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Modular_Types.Modular_1
MD5: e9ac61d2e43585f002fe2b79544ef9d7
Runtime output
1
4294967295
5 procedure Show_Modular is
6 I : Integer := -1;
7 X : Modular := 1;
8 begin
9 X := Modular (I); -- raises Constraint_Error
10 Put_Line (X'Image);
11 end Show_Modular;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Modular_Types.Modular_1
MD5: e8e1a1924efcbe770c719c29547bb863
Build output
Runtime output
Runtime output
4294967295
The Mod attribute will correctly convert from any integer type to a given modular type, using
wraparound semantics.
Historically
In older versions of Ada — such as Ada 95 —, the only way to do this conversion is to use
Unchecked_Conversion, which is somewhat uncomfortable. Furthermore, if you're trying
to convert to a generic formal modular type, how do you know what size of signed integer
type to use? Note that Unchecked_Conversion might malfunction if the source and target
types are of different sizes.
The Mod attribute was added to Ada 2005 to solve this problem. Also, we can now safely
use this attribute in generics. For example:
11 end Mod_Attribute;
In this example, F will return the all-ones bit pattern, for whatever modular type is passed
to Formal_Modular.
Modular types are particularly useful for bit manipulation. For example, we can use the
and, or, xor and not operators for modular types.
Also, we can perform bit-shifting by multiplying or dividing a modular object with a power
of two. For example, if M is a variable of modular type, then M := M * 2 ** 3; shifts the
bits to the left by three bits. Likewise, M := M / 2 ** 3 shifts the bits to the right. Note that
the compiler selects the appropriate shifting operator when translating these operations to
machine code — no actual multiplication or division will be performed.
Let's see a simple implementation of the CRC-CCITT (0x1D0F) algorithm:
6 type Byte_Array is
7 array (Positive range <>) of Byte;
8
16 end Crc_Defs;
27 return Crc_A;
28 end Crc_CCITT;
(continues on next page)
45 begin
46 Byte_IO.Default_Width := 1;
47 Byte_IO.Default_Base := 16;
48 Crc_IO.Default_Width := 1;
49 Crc_IO.Default_Base := 16;
50 end Crc_Defs;
4 procedure Show_Crc is
5 AA : constant Byte_Array :=
6 (16#0#, 16#20#, 16#30#);
7 Crc_A : Crc;
8 begin
9 Crc_A := Crc_CCITT (AA);
10
Project: Courses.Advanced_Ada.Data_Types.Numerics.Modular_Types.Mod_Crc_CCITT_Ada
MD5: 9c66abfadcce92231295cbccad087912
Runtime output
In this example, the core of the algorithm is implemented in the Crc_CCITT function. There,
we use bit shifting — for instance, * 2 ** 8 and / 2 ** 8, which shift left and right,
respectively, by eight bits. We also use the xor operator.
Classification
We've already discussed basic characteristics of numeric literals in the Introduction to Ada
course — although we haven't used this terminology there. There are two kinds of numeric
literals in Ada: integer literals and real literals. They are distinguished by the absence or
presence of a radix point. For example:
3 procedure Real_Integer_Literals is
4 Integer_Literal : constant := 365;
5 Real_Literal : constant := 365.2564;
6 begin
7 Put_Line ("Integer Literal: "
8 & Integer_Literal'Image);
9 Put_Line ("Real Literal: "
10 & Real_Literal'Image);
11 end Real_Integer_Literals;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Real_Integer_
↪Literals
MD5: ba1cc348cad054f3ab86c05e051b40fa
Runtime output
Another classification takes the use of a base indicator into account. (Remember that, when
writing a literal such as 2#1011#, the base is the element before the first # sign.) So here
we distinguish between decimal literals and based literals. For example:
3 procedure Decimal_Based_Literals is
4
8 --
9 -- DECIMAL LITERALS
10 --
11
17 --
18 -- BASED LITERALS
19 --
20
24 Based_Real : constant :=
25 2#1_0110_1101.0100_0001_1010_0011_0111#;
26 Based_Real_Exp : constant :=
27 7#1.031_153_643#e3;
28 begin
29 F_IO.Default_Fore := 3;
30 F_IO.Default_Aft := 4;
31 F_IO.Default_Exp := 0;
32
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Decimal_Based_
↪Literals
MD5: bde8f422c3844826819348d18fb48a33
Runtime output
Dec_Integer: 365
Dec_Real: 365.2564
Dec_Real_Exp: 365.2564
Based_Integer: 365
Based_Integer_Exp: 365
Based_Real: 365.2564
Based_Real_Exp: 365.2564
Based literals use the base#number# format. Also, they aren't limited to simple integer
literals such as 16#16D#. In fact, we can use a radix point or an exponent in based literals,
as well as underscores. In addition, we can use any base from 2 up to 16. We discuss these
aspects further in the next section.
Note: This section was originally written by Franco Gasperoni and published as Gem #7:
The Beauty of Numeric Literals in Ada102 .
Ada provides a simple and elegant way of expressing numeric literals. One of those
simple, yet powerful aspects is the ability to use underscores to separate groups of digits.
For example, 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37510
is more readable and less error prone to type than 3.
14159265358979323846264338327950288419716939937510. Here's the complete code:
3 procedure Ada_Numeric_Literals is
4 Pi : constant :=
5 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37510;
6
7 Pi2 : constant :=
8 3.14159265358979323846264338327950288419716939937510;
9
10 Z : constant := Pi - Pi2;
11 pragma Assert (Z = 0.0);
12
13 use Ada.Text_IO;
14 begin
15 Put_Line ("Z = " & Float'Image (Z));
16 end Ada_Numeric_Literals;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Pi_Literals
MD5: 8f6516730fa98f08234b159488431aaf
Runtime output
Z = 0.00000E+00
Also, when using based literals, Ada allows any base from 2 to 16. Thus, we can write the
decimal number 136 in any one of the following notations:
3 procedure Ada_Numeric_Literals is
4 Bin_136 : constant := 2#1000_1000#;
5 Oct_136 : constant := 8#210#;
6 Dec_136 : constant := 10#136#;
7 Hex_136 : constant := 16#88#;
8 pragma Assert (Bin_136 = 136);
9 pragma Assert (Oct_136 = 136);
10 pragma Assert (Dec_136 = 136);
11 pragma Assert (Hex_136 = 136);
12
13 use Ada.Text_IO;
14
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Based_Literals
MD5: 0959ec5e4aafcde245c5a15597ac9b7e
Runtime output
Bin_136 = 136
Oct_136 = 136
Dec_136 = 136
Hex_136 = 136
In other languages
The rationale behind the method to specify based literals in the C programming language
is strange and unintuitive. Here, you have only three possible bases: 8, 10, and 16 (why no
base 2?). Furthermore, requiring that numbers in base 8 be preceded by a zero feels like a
bad joke on us programmers. For example, what values do 0210 and 210 represent in C?
When dealing with microcontrollers, we might encounter I/O devices that are memory
mapped. Here, we have the ability to write:
3 procedure Ada_Numeric_Literals is
4 Lights_On : constant := 2#1000_1000#;
5 Lights_Off : constant := 2#0111_0111#;
6
10 -- for Output_Devices'Address
11 -- use 16#DEAD_BEEF#;
12 -- ^^^^^^^^^^^^^^^^^^^^^^^^^^
13 -- Memory mapped Output
14
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Literal_Lights
MD5: c3e72b25366d8d815a1f425f2695ad0b
Runtime output
Of course, we can also use records with representation clauses (page 367) to do the above,
which is even more elegant.
The notion of base in Ada allows for exponents, which is particularly pleasant. For instance,
we can write:
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Literal_Binary
MD5: 98d971e0f170db570069f8868e442d6d
In based literals, the exponent — like the base — uses the regular decimal notation and
specifies the power of the base that the based literal should be multiplied with to obtain
the final value. For instance 2#1#e+10 = 1 x 210 = 1_024 (in base 10), whereas 16#F#e+2
= 15 x 162 = 15 x 256 = 3_840 (in base 10).
Based numbers apply equally well to real literals. We can, for instance, write:
Whether we write 3#0.1# or 1.0 / 3, or even 3#1.0#e-1, Ada allows us to specify exactly
rational numbers for which decimal literals cannot be written.
The last nice feature is that Ada has an open-ended set of integer and real types. As a result,
numeric literals in Ada do not carry with them their type as, for example, in C. The actual
type of the literal is determined from the context. This is particularly helpful in avoiding
overflows, underflows, and loss of precision.
In other languages
In C, a source of confusion can be the distinction between 32l and 321. Although both look
similar, they're actually very different from each other.
And this is not all: all constant computations done at compile time are done in infinite
precision, be they integer or real. This allows us to write constants with whatever size and
precision without having to worry about overflow or underflow. We can for instance write:
and be guaranteed that constant Zero has indeed value zero. This is very different from
writing:
One_Third_Approx : constant :=
0.33333333333333333333333333333;
Zero_Approx : constant :=
1.0 - 3.0 * One_Third_Approx;
where Zero_Approx is really 1.0e-29 — and that will show up in your numerical compu-
tations. The above is quite handy when we want to write fractions without any loss of
precision. Here's the complete code:
3 procedure Ada_Numeric_Literals is
4 One_Third : constant := 3#1.0#e-1;
5 -- same as 1.0/3.0
6
10 One_Third_Approx : constant :=
11 0.33333333333333333333333333333;
12 Zero_Approx : constant :=
13 1.0 - 3.0 * One_Third_Approx;
14
15 use Ada.Text_IO;
16
17 begin
18 Put_Line ("Zero = "
19 & Float'Image (Zero));
20 Put_Line ("Zero_Approx = "
21 & Float'Image (Zero_Approx));
22 end Ada_Numeric_Literals;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Literals
MD5: ee604245b34e8cb878a8ebdb21cd564e
Runtime output
Zero = 0.00000E+00
Zero_Approx = 1.00000E-29
5 procedure Ada_Numeric_Literals is
6
7 Big_Sum : constant := 1 +
8 Kilobyte +
9 Megabyte +
10 Gigabyte +
11 Terabyte +
12 Petabyte +
13 Exabyte +
14 Zettabyte;
15
22 use Ada.Text_IO;
23
24 begin
25 Put_Line ("Nil = "
26 & Integer'Image (Nil));
27 end Ada_Numeric_Literals;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Numeric_Literals.Literal_Binary
MD5: 7bda6442e68271d12bdb827b63f0d702
Runtime output
Nil = 0
103 http://www.ada-auth.org/standards/22rm/html/RM-3-5-8.html
104 http://www.ada-auth.org/standards/22rm/html/RM-A-5-3.html
Representation-oriented attributes
Attribute: Machine_Radix
3 procedure Show_Machine_Radix is
4 begin
5 Put_Line
6 ("Float'Machine_Radix: "
7 & Float'Machine_Radix'Image);
8 Put_Line
9 ("Long_Float'Machine_Radix: "
10 & Long_Float'Machine_Radix'Image);
11 Put_Line
12 ("Long_Long_Float'Machine_Radix: "
13 & Long_Long_Float'Machine_Radix'Image);
14 end Show_Machine_Radix;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Machine_
↪Radix
MD5: 88680df680f1db4ff803912850370551
Runtime output
Float'Machine_Radix: 2
Long_Float'Machine_Radix: 2
Long_Long_Float'Machine_Radix: 2
Attributes: Machine_Mantissa
Machine_Mantissa is an attribute that returns the number of bits reserved for the mantissa
of the floating-point type. For example:
3 procedure Show_Machine_Mantissa is
4 begin
5 Put_Line
6 ("Float'Machine_Mantissa: "
7 & Float'Machine_Mantissa'Image);
8 Put_Line
9 ("Long_Float'Machine_Mantissa: "
10 & Long_Float'Machine_Mantissa'Image);
11 Put_Line
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Machine_
↪Mantissa
MD5: da946a90a454c6e8f68cbff1ec54c7d3
Runtime output
Float'Machine_Mantissa: 24
Long_Float'Machine_Mantissa: 53
Long_Long_Float'Machine_Mantissa: 64
On a typical desktop PC, as indicated by Machine_Mantissa, we have 24 bits for the floating-
point mantissa of the Float type.
The Machine_Emin and Machine_Emax attributes return the minimum and maximum value,
respectively, of the machine exponent the floating-point type. Note that, in all cases, the
returned value is a universal integer. For example:
3 procedure Show_Machine_Emin_Emax is
4 begin
5 Put_Line
6 ("Float'Machine_Emin: "
7 & Float'Machine_Emin'Image);
8 Put_Line
9 ("Float'Machine_Emax: "
10 & Float'Machine_Emax'Image);
11 Put_Line
12 ("Long_Float'Machine_Emin: "
13 & Long_Float'Machine_Emin'Image);
14 Put_Line
15 ("Long_Float'Machine_Emax: "
16 & Long_Float'Machine_Emax'Image);
17 Put_Line
18 ("Long_Long_Float'Machine_Emin: "
19 & Long_Long_Float'Machine_Emin'Image);
20 Put_Line
21 ("Long_Long_Float'Machine_Emax: "
22 & Long_Long_Float'Machine_Emax'Image);
23 end Show_Machine_Emin_Emax;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Machine_
↪Emin_Emax
MD5: 9766e06faaf1fc2ca48dd0bc0461b247
Runtime output
Float'Machine_Emin: -125
Float'Machine_Emax: 128
Long_Float'Machine_Emin: -1021
Long_Float'Machine_Emax: 1024
Long_Long_Float'Machine_Emin: -16381
Long_Long_Float'Machine_Emax: 16384
Attribute: Digits
Digits is an attribute that returns the requested decimal precision of a floating-point sub-
type. Let's see an example:
3 procedure Show_Digits is
4 begin
5 Put_Line ("Float'Digits: "
6 & Float'Digits'Image);
7 Put_Line ("Long_Float'Digits: "
8 & Long_Float'Digits'Image);
9 Put_Line ("Long_Long_Float'Digits: "
10 & Long_Long_Float'Digits'Image);
11 end Show_Digits;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Digits
MD5: cd1c88054f7d54703760a852d08acb6d
Runtime output
Float'Digits: 6
Long_Float'Digits: 15
Long_Long_Float'Digits: 18
Here, the requested decimal precision of the Float type is six digits.
Note that we said that Digits is the requested level of precision, which is specified as
part of declaring a floating point type. We can retrieve the actual decimal precision with
Base'Digits. For example:
3 procedure Show_Base_Digits is
4 type Float_D3 is new Float digits 3;
5 begin
6 Put_Line ("Float_D3'Digits: "
7 & Float_D3'Digits'Image);
8 Put_Line ("Float_D3'Base'Digits: "
9 & Float_D3'Base'Digits'Image);
10 end Show_Base_Digits;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Base_Digits
MD5: a2deb352f93511ab2a39d41f0b3f9512
Runtime output
Float_D3'Digits: 3
Float_D3'Base'Digits: 6
The requested decimal precision of the Float_D3 type is three digits, while the actual dec-
imal precision is six digits (on a typical desktop PC).
In this section, we discuss attributes that return Boolean values indicating whether a feature
is available or not in the target architecture:
• Denorm is an attribute that indicates whether the target architecture uses denormal-
ized numbers105 .
• Signed_Zeros is an attribute that indicates whether the type uses a sign for zero
values, so it can represent both -0.0 and 0.0.
• Machine_Rounds is an attribute that indicates whether rounding-to-nearest is used,
rather than some other choice (such as rounding-toward-zero).
• Machine_Overflows is an attribute that indicates whether a Constraint_Error ex-
ception is (or is not) guaranteed to be raised when an operation with that type pro-
duces an overflow or divide-by-zero.
3 procedure Show_Boolean_Attributes is
4 begin
5 Put_Line
6 ("Float'Denorm: "
7 & Float'Denorm'Image);
8 Put_Line
9 ("Long_Float'Denorm: "
10 & Long_Float'Denorm'Image);
11 Put_Line
12 ("Long_Long_Float'Denorm: "
13 & Long_Long_Float'Denorm'Image);
14 Put_Line
(continues on next page)
105 https://en.wikipedia.org/wiki/Subnormal_number
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Machine_
↪Rounds_Overflows
MD5: b3f72c212cf00e697fe144a87eb72339
Runtime output
Float'Denorm: TRUE
Long_Float'Denorm: TRUE
Long_Long_Float'Denorm: TRUE
Float'Signed_Zeros: TRUE
Long_Float'Signed_Zeros: TRUE
Long_Long_Float'Signed_Zeros: TRUE
Float'Machine_Rounds: TRUE
Long_Float'Machine_Rounds: TRUE
Long_Long_Float'Machine_Rounds: TRUE
Float'Machine_Overflows: FALSE
Long_Float'Machine_Overflows: FALSE
Long_Long_Float'Machine_Overflows: FALSE
In this section, we discuss attributes that we can use to manipulate floating-point values.
3 procedure Show_Exponent_Fraction_Compose is
4 begin
5 Put_Line
6 ("Float'Fraction (1.0): "
7 & Float'Fraction (1.0)'Image);
8 Put_Line
9 ("Float'Fraction (0.25): "
10 & Float'Fraction (0.25)'Image);
11 Put_Line
12 ("Float'Fraction (1.0e-25): "
13 & Float'Fraction (1.0e-25)'Image);
14 Put_Line
15 ("Float'Exponent (1.0): "
16 & Float'Exponent (1.0)'Image);
17 Put_Line
18 ("Float'Exponent (0.25): "
19 & Float'Exponent (0.25)'Image);
20 Put_Line
21 ("Float'Exponent (1.0e-25): "
22 & Float'Exponent (1.0e-25)'Image);
23 Put_Line
24 ("Float'Compose (5.00000e-01, 1): "
25 & Float'Compose (5.00000e-01, 1)'Image);
26 Put_Line
27 ("Float'Compose (5.00000e-01, -1): "
28 & Float'Compose (5.00000e-01, -1)'Image);
29 Put_Line
30 ("Float'Compose (9.67141E-01, -83): "
31 & Float'Compose (9.67141E-01, -83)'Image);
32 end Show_Exponent_Fraction_Compose;
MD5: d2e61b6b9a7a50861145f6b65e9fac39
Runtime output
Float'Fraction (1.0): 5.00000E-01
Float'Fraction (0.25): 5.00000E-01
(continues on next page)
To understand this code example, we have to take this formula into account:
Value = Fraction x Machine_RadixExponent
Considering that the value of Float'Machine_Radix on a typical PC is two, we see that the
value 1.0 is composed by a fraction of 0.5 and a machine exponent of one. In other words:
0.5 x 21 = 1.0
For the value 0.25, we get a fraction of 0.5 and a machine exponent of -1, which is the
result of 0.5 x 2-1 = 0.25. We can use the Compose attribute to perform this calculation. For
example, Float'Compose (0.5, -1) = 0.25.
Note that Fraction is always between 0.5 and 0.999999 (i.e < 1.0), except for denormalized
numbers, where it can be < 0.5.
Attribute: Scaling
Scaling is an attribute that scales a floating-point value based on the machine radix and
a machine exponent passed to the function. For example:
3 procedure Show_Scaling is
4 begin
5 Put_Line ("Float'Scaling (0.25, 1): "
6 & Float'Scaling (0.25, 1)'Image);
7 Put_Line ("Float'Scaling (0.25, 2): "
8 & Float'Scaling (0.25, 2)'Image);
9 Put_Line ("Float'Scaling (0.25, 3): "
10 & Float'Scaling (0.25, 3)'Image);
11 end Show_Scaling;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Scaling
MD5: 9fa821d32911b74ee4b4fde3f3adafd8
Runtime output
Floor and Ceiling are attributes that returned the rounded-down or rounded-up value,
respectively, of a floating-point value. For example:
3 procedure Show_Floor_Ceiling is
4 begin
5 Put_Line ("Float'Floor (0.25): "
6 & Float'Floor (0.25)'Image);
7 Put_Line ("Float'Ceiling (0.25): "
8 & Float'Ceiling (0.25)'Image);
9 end Show_Floor_Ceiling;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Floor_
↪Ceiling
MD5: 1344d54ae86b9fd4831d5f078eb655d4
Runtime output
As we can see in this example, the rounded-down value (floor) of 0.25 is 0.0, while the
rounded-up value (ceiling) of 0.25 is 1.0.
Round-to-nearest attributes
In this section, we discuss three attributes used for rounding: Rounding, Unbi-
ased_Rounding, Machine_Rounding In all cases, the rounding attributes return the nearest
integer value (as a floating-point value). For example, the rounded value for 4.8 is 5.0
because 5 is the closest integer value.
Let's see a code example:
3 procedure Show_Roundings is
4 begin
5 Put_Line
6 ("Float'Rounding (0.5): "
7 & Float'Rounding (0.5)'Image);
8 Put_Line
9 ("Float'Rounding (1.5): "
10 & Float'Rounding (1.5)'Image);
11 Put_Line
12 ("Float'Rounding (4.5): "
13 & Float'Rounding (4.5)'Image);
14 Put_Line
15 ("Float'Rounding (-4.5): "
16 & Float'Rounding (-4.5)'Image);
17 Put_Line
18 ("Float'Unbiased_Rounding (0.5): "
(continues on next page)
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Rounding
MD5: 3f78165f092a163339cb9593ff15a50d
Runtime output
The difference between these attributes is the way they handle the case when a value is
exactly in between two integer values. For example, 4.5 could be rounded up to 5.0 or
rounded down to 4.0. This is the way each rounding attribute works in this case:
• Rounding rounds away from zero. Positive floating-point values are rounded up, while
negative floating-point values are rounded down when the value is between two inte-
ger values. For example:
– 4.5 is rounded-up to 5.0, i.e. Float'Rounding (4.5) = Float'Ceiling (4.5)
= 5.0.
– -4.5 is rounded-down to -5.0, i.e. Float'Rounding (-4.5) = Float'Floor (-4.
5) = -5.0.
• Unbiased_Rounding rounds toward the even integer. For example,
– Float'Unbiased_Rounding (0.5) = 0.0 because zero is the closest even integer,
while
– Float'Unbiased_Rounding (1.5) = 2.0 because two is the closest even integer.
• Machine_Rounding uses the most appropriate rounding instruction available on the
target platform. While this rounding attribute can potentially have the best perfor-
mance, its result may be non-portable. For example, whether the rounding of 4.5
becomes 4.0 or 5.0 depends on the target platform.
– If an algorithm depends on a specific rounding behavior, it's best to avoid the
Machine_Rounding attribute. On the other hand, if the rounding behavior won't
have a significant impact on the results, we can safely use this attribute.
The Truncation attribute returns the truncated value of a floating-point value, i.e. the value
corresponding to the integer part of a number rounded toward zero. This corresponds to
the number before the radix point. For example, the truncation of 1.55 is 1.0 because the
integer part of 1.55 is 1.
The Remainder attribute returns the remainder part of a division. For example,
Float'Remainder (1.25, 0.5) = 0.25. Let's briefly discuss the details of this opera-
tions. The result of the division 1.25 / 0.5 is 2.5. Here, 1.25 is the dividend and 0.5 is the
divisor. The quotient and remainder of this division are 2 and 0.25, respectively. (Here, the
quotient is an integer number, and the remainder is the floating-point part that remains.)
Note that the relation between quotient and remainder is defined in such a way that we
get the original dividend back when we use the formula: "quotient x divisor + remainder =
dividend". For the previous example, this means 2 x 0.5 + 0.25 = 1.25.
The Adjacent attribute is the next machine value towards another value. For example, on
a typical PC, the adjacent value of a small value — say, 1.0 x 10-83 — towards zero is +0.0,
while the adjacent value of this small value towards 1.0 is another small, but greater value
— in fact, it's 1.40130 x 10-45 . Note that the first parameter of the Adjacent attribute is
the value we want to analyze and the second parameter is the Towards value.
Let's see a code example:
3 procedure Show_Truncation_Remainder_Adjacent is
4 begin
5 Put_Line
6 ("Float'Truncation (1.55): "
7 & Float'Truncation (1.55)'Image);
8 Put_Line
9 ("Float'Truncation (-1.55): "
10 & Float'Truncation (-1.55)'Image);
11 Put_Line
12 ("Float'Remainder (1.25, 0.25): "
13 & Float'Remainder (1.25, 0.25)'Image);
14 Put_Line
15 ("Float'Remainder (1.25, 0.5): "
16 & Float'Remainder (1.25, 0.5)'Image);
17 Put_Line
18 ("Float'Remainder (1.25, 1.0): "
19 & Float'Remainder (1.25, 1.0)'Image);
20 Put_Line
21 ("Float'Remainder (1.25, 2.0): "
22 & Float'Remainder (1.25, 2.0)'Image);
23 Put_Line
24 ("Float'Adjacent (1.0e-83, 0.0): "
25 & Float'Adjacent (1.0e-83, 0.0)'Image);
26 Put_Line
27 ("Float'Adjacent (1.0e-83, 1.0): "
28 & Float'Adjacent (1.0e-83, 1.0)'Image);
29 end Show_Truncation_Remainder_Adjacent;
Copy_Sign is an attribute that returns a value where the sign of the second floating-point
argument is multiplied by the magnitude of the first floating-point argument. For example,
Float'Copy_Sign (1.0, -10.0) is -1.0. Here, the sign of the second argument (-10.0) is
multiplied by the magnitude of the first argument (1.0), so the result is -1.0.
Leading_Part is an attribute that returns the approximated version of the mantissa of
a value based on the specified number of leading bits for the mantissa. Let's see some
examples:
• Float'Leading_Part (3.1416, 1) is 2.0 because that's the value we can represent
with one leading bit.
– Note that Float'Fraction (2.0) = 0.5 — which can be represented with one
leading bit in the mantissa — and Float'Exponent (2.0) = 2.)
• If we increase the number of leading bits of the mantissa to two — by writing
Float'Leading_Part (3.1416, 2) —, we get 3.0 because that's the value we can
represent with two leading bits.
• If we increase again the number of leading bits to five — Float'Leading_Part (3.
1416, 5) —, we get 3.125.
– Note that, in this case Float'Fraction (3.125) = 0.78125 and Float'Exponent
(3.125) = 2.
– The binary mantissa is actually 2#110_0100_0000_0000_0000_0000#, which can
be represented with five leading bits as expected: 2#110_01#.
∗ We can get the binary mantissa by calculating Float'Fraction (3.125) *
Float (Float'Machine_Radix) ** (Float'Machine_Mantissa - 1) and
converting the result to binary format. The -1 value in the formula corresponds
to the sign bit.
Attention
In this explanation about the Leading_Part attribute, we're talking about leading bits.
Strictly speaking, however, this is actually a simplification, and it's only correct if Ma-
chine_Radix is equal to two — which is the case for most machines. Therefore, in most
cases, the explanation above is perfectly acceptable.
However, if Machine_Radix is not equal to two, we cannot use the term "bits" anymore,
but rather digits of the Machine_Radix.
3 procedure Show_Copy_Sign_Leading_Part_Machine is
4 begin
5 Put_Line
6 ("Float'Copy_Sign (1.0, -10.0): "
7 & Float'Copy_Sign (1.0, -10.0)'Image);
8 Put_Line
9 ("Float'Copy_Sign (-1.0, -10.0): "
10 & Float'Copy_Sign (-1.0, -10.0)'Image);
11 Put_Line
12 ("Float'Copy_Sign (1.0, 10.0): "
13 & Float'Copy_Sign (1.0, 10.0)'Image);
(continues on next page)
Attribute: Machine
Not every real number is directly representable as a floating-point value on a specific ma-
chine. For example, let's take a value such as 1.0 x 1015 (or 1,000,000,000,000,000):
3 procedure Show_Float_Value is
4 package F_IO is new
5 Ada.Text_IO.Float_IO (Float);
6
7 V : Float;
8 begin
9 F_IO.Default_Fore := 3;
10 F_IO.Default_Aft := 1;
11 F_IO.Default_Exp := 0;
12
13 V := 1.0E+15;
14 Put ("1.0E+15 = ");
15 F_IO.Put (Item => V);
16 New_Line;
17
18 end Show_Float_Value;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Floating_Point_Types.Float_Value
MD5: a7f80f7584ebaf39f2d5f9564c9c7d64
Runtime output
1.0E+15 = 999999986991000.0
If we run this example on a typical PC, we see that the expected value
1_000_000_000_000_000.0 was displayed as 999_999_986_991_000.0. This is be-
cause 1.0 x 1015 isn't directly representable on this machine, so it has to be modified to a
value that is actually representable (on the machine).
This automatic modification we've just described is actually hidden, so to say, in the as-
signment. However, we can make it more visible by using the Machine (X) attribute,
which returns a version of X that is representable on the target machine. The Machine (X)
attribute rounds (or truncates) X to either one of the adjacent machine numbers for the
specific floating-point type of X. (Of course, if the real value of X is directly representable
on the target machine, no modification is performed.)
In fact, we could rewrite the V := 1.0E+15 assignment of the code example as V :=
Float'Machine (1.0E+15), as we're never assigning a real value directly to a floating-
pointing variable — instead, we're first converting it to a version of the real value that is
representable on the target machine. In this case, 999999986991000.0 is a representable
version of the real value 1.0 x 1015 . Of course, writing V := 1.0E+15 or V := Float'Machine
(1.0E+15) doesn't make any difference to the actual value that is assigned to V (in the case
of this specific target architecture), as the conversion to a representable value happens
automatically during the assignment to V.
There are, however, instances where using the Machine attribute does make a difference in
the result. For example, let's say we want to calculate the difference between the original
real value in our example (1.0 x 1015 ) and the actual value that is assigned to V. We can do
this by using the Machine attribute in the calculation:
3 procedure Show_Machine_Attribute is
4 package F_IO is new
5 Ada.Text_IO.Float_IO (Float);
6
7 V : Float;
8 begin
9 F_IO.Default_Fore := 3;
10 F_IO.Default_Aft := 1;
11 F_IO.Default_Exp := 0;
12
13 Put_Line
14 ("Original value: 1_000_000_000_000_000.0");
15
16 V := 1.0E+15;
17 Put ("Machine value: ");
18 F_IO.Put (Item => V);
19 New_Line;
20
26 end Show_Machine_Attribute;
MD5: c2db2cca028dc5811068f9b7f1bc209d
Runtime output
Original value: 1_000_000_000_000_000.0
Machine value: 999999986991000.0
Difference: 13008896.0
When we run this example on a typical PC, we see that the difference is roughly 1.3009 x
107 . (Actually, the value that we might see is 1.3008896 x 107 , which is a version of 1.3009
x 107 that is representable on the target machine.)
In this section, we discuss various attributes and operations related to fixed-point types.
Attribute: Machine_Radix
3 procedure Show_Fixed_Machine_Radix is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Fixed_Machine_
↪Radix
MD5: a09d060a58f76550e948a8245ffb5fde
106 http://www.ada-auth.org/standards/22rm/html/RM-3-5-10.html
107 http://www.ada-auth.org/standards/22rm/html/RM-A-5-4.html
Runtime output
T3_D3'Machine_Radix: 2
TQ31'Machine_Radix: 2
In this section, we discuss attributes that return Boolean values indicating whether a feature
is available or not in the target architecture:
• Machine_Rounds is an attribute that indicates what happens when the result of a fixed-
point operation is inexact:
– T'Machine_Rounds = True: inexact result is rounded;
– T'Machine_Rounds = False: inexact result is truncated.
• Machine_Overflows is an attribute that indicates whether a Constraint_Error is
guaranteed to be raised when a fixed-point operation with that type produces an over-
flow or divide-by-zero.
3 procedure Show_Boolean_Attributes is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5
The Small and Delta attributes return numbers that indicate the numeric precision of a
fixed-point type. In many cases, the Small of a type T is equal to the Delta of that type —
i.e. T'Small = T'Delta. Let's discuss each attribute and how they distinguish from each
other.
The Delta attribute returns the value of the delta that was used in the type definition. For
example, if we declare type T3_D3 is delta 10.0 ** (-3) digits D, then the value of
T3_D3'Delta is the 10.0 ** (-3) that we used in the type definition.
The Small attribute returns the "small" of a type, i.e. the smallest value used in the machine
representation of the type. The small must be at least equal to or smaller than the delta —
in other words, it must conform to the T'Small <= T'Delta rule.
The Small and the Delta need not actually be small numbers. They can be arbitrarily large.
For instance, they could be 1.0, or 1000.0. Consider the following example:
4 procedure Show_Fixed_Type_Info is
5 begin
6 Put_Line ("Size : "
7 & Fixed'Size'Image);
8 Put_Line ("Small : "
9 & Fixed'Small'Image);
10 Put_Line ("Delta : "
11 & Fixed'Delta'Image);
12 Put_Line ("First : "
13 & Fixed'First'Image);
14 Put_Line ("Last : "
15 & Fixed'Last'Image);
16 end Show_Fixed_Type_Info;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Large_Small_
↪Attribute
MD5: 89672950b355060d250e0f5d7e2d40cb
Runtime output
Size : 32
Small : 1.58456325028528675E+29
Delta : 1.58456325028528675E+29
First : -340282366920938463463374607431768211456.0
Last : 340282366762482138434845932244680310784.0
In this example, the small of the Fixed type is actually quite large:
1.5845632502852867529 . (Also, the first and the last values are
large: -340,282,366,920,938,463,463,374,607,431,768,211,456.0 and
340,282,366,762,482,138,434,845,932,244,680,310,784.0, or approximately -3.402838
and 3.402838 .)
In this case, if we assign 1 or 1,000 to a variable F of this type, the actual value stored in F
is zero. Feel free to try this out!
When we declare an ordinary fixed-point data type, we must specify the delta. Specifying
the small, however, is optional:
• If the small isn't specified, it is automatically selected by the compiler. In this case,
the actual value of the small is an implementation-defined power of two — always
following the rule that says: T'Small <= T'Delta.
• If we want, however, to specify the small, we can do that by using the Small aspect.
In this case, it doesn't need to be a power of two.
For decimal fixed-point types, we cannot specify the small. In this case, it's automatically
selected by the compiler, and it's always equal to the delta.
Let's see an example:
5 procedure Show_Fixed_Small_Delta is
6 begin
7 Put_Line ("T3_D3'Small: "
8 & T3_D3'Small'Image);
9 Put_Line ("T3_D3'Delta: "
10 & T3_D3'Delta'Image);
11 Put_Line ("T3_D3'Size: "
12 & T3_D3'Size'Image);
13 Put_Line ("--------------------");
14
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Fixed_Small_
↪Delta
MD5: 0e811c7c0b92f05483b0ac7c3489dc3d
Runtime output
T3_D3'Small: 1.00000000000000000E-03
T3_D3'Delta: 1.00000000000000000E-03
T3_D3'Size: 11
--------------------
TD3'Small: 9.76562500000000000E-04
TD3'Delta: 1.00000000000000000E-03
TD3'Size: 11
--------------------
TQ31'Small: 4.65661287307739258E-10
TQ31'Delta: 4.65661287307739258E-10
TQ32'Size: 32
--------------------
TQ15'Small: 4.65661287307739258E-10
TQ15'Delta: 3.05175781250000000E-05
TQ15'Size: 32
As we can see in the output of the code example, the Delta attribute returns the value we
used for delta in the type definition of the T3_D3, TD3, TQ31 and TQ15 types.
The TD3 type is an ordinary fixed-point type with the the same delta as the decimal T3_D3
type. In this case, however, TD3'Small is not the same as the TD3'Delta. On a typical
desktop PC, TD3'Small is 2-10 , while the delta is 10-3 . (Remember that, for ordinary fixed-
point types, if we don't specify the small, it's automatically selected by the compiler as a
power of two smaller than or equal to the delta.)
In the case of the TQ15 type, we're specifying the small by using the Small aspect. In
this case, the underlying size of the TQ15 type is 32 bits, while the precision we get when
operating with this type is 16 bits. Let's see a specific example for this type:
5 procedure Show_Fixed_Small_Delta is
6 V : TQ15;
7 begin
8 Put_Line ("V'Size: " & V'Size'Image);
9
10 V := TQ15'Small;
11 Put_Line ("V: " & V'Image);
12
13 V := TQ15'Delta;
14 Put_Line ("V: " & V'Image);
15 end Show_Fixed_Small_Delta;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Fixed_Small_
↪Delta
MD5: f2a71db911913d6fbf5343671599c0ae
Runtime output
V'Size: 32
V: 0.00000
V: 0.00003
In the first assignment, we assign TQ15'Small (2-31 ) to V. This value is smaller than the
type's delta (2-15 ). Even though V'Size is 32 bits, V'Delta indicates 16-bit precision, and
TQ15'Small requires 32-bit precision to be represented correctly. As a result, V has a value
of zero after this assignment.
In contrast, after the second assignment — where we assign TQ15'Delta (2-15 ) to V — we
see, as expected, that V has the same value as the delta.
The Fore and Aft attributes indicate the number of characters or digits needed for display-
ing a value in decimal representation. To be more precise:
• The Fore attribute refers to the digits before the decimal point and it returns the num-
ber of digits plus one for the sign indicator (which is either - or space), and it's always
at least two.
• The Aft attribute returns the number of decimal digits that is needed to represent the
delta after the decimal point.
Let's see an example:
3 procedure Show_Fixed_Fore_Aft is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Fixed_Fore_Aft
MD5: d031f74d967a96dee1c6a83ff4bd14cf
Runtime output
T3_D3'Fore: 2
T3_D3'Aft: 3
TQ31'Fore: 2
TQ31'Aft: 10
----
Dec: -0.123
Fix: -0.0000000005
As we can see in the output of the Dec and Fix variables at the bottom, the value of
Fore is two for both T3_D3 and TQ31. This value corresponds to the length of the string
"-0" displayed in the output for these variables (the first two characters of "-0.123" and
"-0.0000000005").
The value of Dec'Aft is three, which matches the number of digits after the decimal point
in "-0.123". Similarly, the value of Fix'Aft is 10, which matches the number of digits after
the decimal point in "-0.0000000005".
The attributes presented in this subsection are only available for decimal fixed-point types.
Attribute: Digits
Digits is an attribute that returns the number of significant decimal digits of a decimal
fixed-point subtype. This corresponds to the value that we use for the digits in the defi-
nition of a decimal fixed-point type.
Let's see an example:
3 procedure Show_Decimal_Digits is
4 type T3_D6 is delta 10.0 ** (-3) digits 6;
5 subtype T3_D2 is T3_D6 digits 2;
6 begin
7 Put_Line ("T3_D6'Digits: "
8 & T3_D6'Digits'Image);
9 Put_Line ("T3_D2'Digits: "
10 & T3_D2'Digits'Image);
11 end Show_Decimal_Digits;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Decimal_Digits
MD5: d46e67bd0f8b369918e7ab9ab4413ae7
Runtime output
T3_D6'Digits: 6
T3_D2'Digits: 2
In this example, T3_D6'Digits is six, which matches the value that we used for digits
in the type definition of T3_D6. The same logic applies for subtypes, as we can see in the
value of T3_D2'Digits. Here, the value is two, which was used in the declaration of the
T3_D2 subtype.
Attribute: Scale
According to the Ada Reference Manual, the Scale attribute "indicates the position of the
point relative to the rightmost significant digits of values" of a decimal type. For example:
• If the value of Scale is two, then there are two decimal digits after the decimal point.
• If the value of Scale is negative, that implies that the Delta is a power of 10 greater
than 1, and it would be the number of zero digits that every value would end in.
The Scale corresponds to the N used in the delta 10.0 ** (-N) expression of the type
declaration. For example, if we write delta 10.0 ** (-3) in the declaration of a type T,
then the value of T'Scale is three.
Let's look at this complete example:
3 procedure Show_Decimal_Scale is
4 type TM3_D6 is delta 10.0 ** 3 digits 6;
5 type T3_D6 is delta 10.0 ** (-3) digits 6;
6 type T9_D12 is delta 10.0 ** (-9) digits 12;
7 begin
8 Put_Line ("TM3_D6'Scale: "
9 & TM3_D6'Scale'Image);
10 Put_Line ("T3_D6'Scale: "
11 & T3_D6'Scale'Image);
12 Put_Line ("T9_D12'Scale: "
13 & T9_D12'Scale'Image);
14 end Show_Decimal_Scale;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Fixed_Point_Types.Decimal_Scale
MD5: 56a99848cf31a9c69fe6d91ead73375a
Runtime output
TM3_D6'Scale: -3
T3_D6'Scale: 3
T9_D12'Scale: 9
Attribute: Round
The Round attribute rounds a value of any real type to the nearest value that is a multiple
of the delta of the decimal fixed-point type, rounding away from zero if exactly between
two such multiples.
For example, if we have a type T with three digits, and we use a value with 10 digits after
the decimal point in a call to T'Round, the resulting value will have three digits after the
decimal point.
Note that the X input of an S'Round (X) call is a universal real value, while the returned
value is of S'Base type.
Let's look at this example:
3 procedure Show_Decimal_Round is
4 type T3_D3 is delta 10.0 ** (-3) digits 3;
5 begin
6 Put_Line ("T3_D3'Round (0.2774): "
7 & T3_D3'Round (0.2774)'Image);
8 Put_Line ("T3_D3'Round (0.2777): "
9 & T3_D3'Round (0.2777)'Image);
10 end Show_Decimal_Round;
Runtime output
T3_D3'Round (0.2774): 0.277
T3_D3'Round (0.2777): 0.278
Here, the T3_D3 has a precision of three digits. Therefore, to fit this precision, 0.2774 is
rounded to 0.277, and 0.2777 is rounded to 0.278.
As we've seen before, we can define numeric types in Ada with a high degree of precision.
However, these normal numeric types in Ada are limited to what the underlying hardware
actually supports. For example, any signed integer type — whether defined by the language
or the user — cannot have a range greater than that of System.Min_Int .. System.
Max_Int because those constants reflect the actual hardware's signed integer types. In
certain applications, that precision might not be enough, so we have to rely on arbitrary-
precision arithmetic108 . These so-called "big numbers" are limited conceptually only by
available memory, in contrast to the underlying hardware-defined numeric types.
Ada supports two categories of big numbers: big integers and big reals — both are specified
in child packages of the Ada.Numerics.Big_Numbers package:
Category Package
Big Integers Ada.Numerics.Big_Numbers.Big_Integers
Big Reals Ada.Numerics.Big_Numbers.Big_Real
108 https://en.wikipedia.org/wiki/arbitrary-precision_arithmetic
Overview
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 with Ada.Numerics.Big_Numbers.Big_Reals;
9 use Ada.Numerics.Big_Numbers.Big_Reals;
10
11 procedure Show_Simple_Big_Numbers is
12 BI : Big_Integer;
13 BR : Big_Real;
14 begin
15 BI := 12345678901234567890;
16 BR := 2.0 ** 1234;
17
21 BI := BI + 1;
22 BR := BR + 1.0;
23
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Simple_Big_Numbers
MD5: d25e0c73ef04b6c950f2ab63fc96a353
Runtime output
BI: 12345678901234567890
BR:␣
↪29581122460809862906004469571610359078633968713537299223955620705065735079623892426105383724837
↪000
BI: 12345678901234567891
BR:␣
↪29581122460809862906004469571610359078633968713537299223955620705065735079623892426105383724837
↪000
109 http://www.ada-auth.org/standards/22rm/html/RM-A-5-5.html
110 http://www.ada-auth.org/standards/22rm/html/RM-A-5-6.html
111 http://www.ada-auth.org/standards/22rm/html/RM-A-5-7.html
In this example, we're declaring the big integer BI and the big real BR, and we're increment-
ing them by one.
Naturally, we're not limited to using the + operator (such as in this example). We can use
the same operators on big numbers that we can use with normal numeric types. In fact,
the common unary operators (+, -, abs) and binary operators (+, -, *, /, **, Min and Max)
are available to us. For example:
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Simple_Big_Numbers_Operators is
9 BI : Big_Integer;
10 begin
11 BI := 12345678901234567890;
12
15 BI := -BI + BI / 2;
16 BI := BI - BI * 2;
17
MD5: c4f405e3ea916bc8a3f309acdeb0606a
Runtime output
BI: 12345678901234567890
BI: 6172839450617283945
In this example, we're applying the four basic operators (+, -, *, /) on big integers.
Factorial
A typical example is the factorial112 : a sequence of the factorial of consecutive small num-
bers can quickly lead to big numbers. Let's take this implementation as an example:
9 return Fact;
10 end Factorial;
3 with Factorial;
4
5 procedure Show_Factorial is
6 begin
7 for I in 1 .. 50 loop
8 Put_Line (I'Image & "! = "
9 & Factorial (I)'Image);
10 end loop;
11 end Show_Factorial;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Factorial_Integer
MD5: 9b20469533706ef025a03b506a07b920
Runtime output
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000
Here, we're using Long_Long_Integer for the computation and return type of the Facto-
rial function. (We're using Long_Long_Integer because its range is probably the biggest
possible on the machine, although that is not necessarily so.) The last number we're able
to calculate before getting an exception is 20!, which basically shows the limitation of stan-
dard integers for this kind of algorithm. If we use big integers instead, we can easily display
all numbers up to 50! (and more!):
3 with Ada.Numerics.Big_Numbers.Big_Integers;
4 use Ada.Numerics.Big_Numbers.Big_Integers;
5
9 return Fact;
10 end Factorial;
5 with Factorial;
6
7 procedure Show_Big_Number_Factorial is
8 begin
9 for I in 1 .. 50 loop
10 Put_Line (I'Image & "! = "
11 & Factorial (I)'Image);
12 end loop;
13 end Show_Big_Number_Factorial;
Runtime output
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
(continues on next page)
As we can see in this example, replacing the Long_Long_Integer type by the Big_Integer
type fixes the problem (the runtime exception) that we had in the previous version. (Note
that we're using the To_Big_Integer function to convert from Integer to Big_Integer:
we discuss these conversions next.)
Note that there is a limit to the upper bounds for big integers. However, this limit isn't
dependent on the hardware types — as it's the case for normal numeric types —, but rather
compiler specific. In other words, the compiler can decide how much memory it wants to
use to represent big integers.
Conversions
Most probably, we want to mix big numbers and standard numbers (i.e. integer and real
numbers) in our application. In this section, we talk about the conversion between big
numbers and standard types.
Validity
The package specifications of big numbers include subtypes that ensure that a actual value
of a big number is valid:
These subtypes include a contract for this check. For example, this is the definition of the
Valid_Big_Integer subtype:
Any operation on big numbers is actually performing this validity check (via a call to the
Is_Valid function). For example, this is the addition operator for big integers:
As we can see, both the input values to the operator as well as the return value are expected
to be valid — the Valid_Big_Integer subtype triggers this check, so to say. This approach
ensures that an algorithm operating on big numbers won't be using invalid values.
Conversion functions
These are the most important functions to convert between big number and standard types:
Big Reals
• To_Big_Real (floating- • From_Big_Real
point types or fixed-
point types)
We use the To_Big_Integer and To_Integer functions to convert back and forth between
Big_Integer and Integer types:
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Simple_Big_Integer_Conversion is
9 BI : Big_Integer;
10 I : Integer := 10000;
11 begin
12 BI := To_Big_Integer (I);
13 Put_Line ("BI: " & BI'Image);
14
MD5: 84f55568b26bf6c1c6f0b06391e8ac0f
Runtime output
BI: 10000
I: 10001
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Arbitrary_Big_Integer_Conversion is
9
20 BI : Big_Integer;
21 LLI : Long_Long_Integer := 10000;
22 U_32 : Mod_32_Bit := 2 ** 32 + 1;
23
24 begin
25 BI := To_Big_Integer (LLI);
26 Put_Line ("BI: " & BI'Image);
27
31 BI := To_Big_Integer (U_32);
32 Put_Line ("BI: " & BI'Image);
33
37 end Show_Arbitrary_Big_Integer_Conversion;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Arbitrary_Big_
↪Integer_Conversion
MD5: 21466010594cf09f37776bc8cb61ee9c
Runtime output
BI: 10000
LLI: 10001
BI: 1
U_32: 2
When converting between big real and floating-point types, we have to instantiate the
generic Float_Conversions package:
5 with Ada.Numerics.Big_Numbers.Big_Reals;
6 use Ada.Numerics.Big_Numbers.Big_Reals;
7
8 procedure Show_Big_Real_Floating_Point_Conversion
9 is
10 type D10 is digits 10;
11
20 BR : Big_Real;
21 LF : Long_Float := 2.0;
22 F10 : D10 := 1.999;
23
24 begin
25 BR := To_Big_Real (LF);
26 Put_Line ("BR: " & BR'Image);
27
31 BR := To_Big_Real (F10);
32 Put_Line ("BR: " & BR'Image);
33
37 end Show_Big_Real_Floating_Point_Conversion;
MD5: 531c59a06b46c2074bc5378b5dcddd35
Runtime output
BR: 2.000
LF: 3.00000000000000E+00
BR: 1.999
F10: 2.099000000E+00
When converting between big real and ordinary fixed-point types, we have to instantiate
the generic Fixed_Conversions package:
5 with Ada.Numerics.Big_Numbers.Big_Reals;
6 use Ada.Numerics.Big_Numbers.Big_Reals;
7
17 BR : Big_Real;
18 FQ31 : TQ31 := 0.25;
19
20 begin
21 BR := To_Big_Real (FQ31);
22 Put_Line ("BR: " & BR'Image);
23
27 end Show_Big_Real_Fixed_Point_Conversion;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Real_Fixed_Point_
↪Conversion
MD5: 94a87bfc6ffad70f757cfc8b6ae32530
Runtime output
BR: 0.250
FQ31: 0.5000000000
In this example, we declare the TQ31_Conversions to be able to convert between big re-
als and the custom fixed-point type TQ31 type. Again, we use the To_Big_Real and the
From_Big_Real functions for the conversions.
Note that there's no direct way to convert between decimal fixed-point types and big real
types. (Of course, you could perform this conversion indirectly by using a floating-point or
an ordinary fixed-point type in between.)
We can also convert between big reals and big integers (or standard integers):
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 with Ada.Numerics.Big_Numbers.Big_Reals;
9 use Ada.Numerics.Big_Numbers.Big_Reals;
10
11 procedure Show_Big_Real_Big_Integer_Conversion
12 is
13 I : Integer;
(continues on next page)
17 begin
18 I := 12345;
19 BR := To_Real (I);
20 Put_Line ("BR (from I): " & BR'Image);
21
22 BI := 123456;
23 BR := To_Big_Real (BI);
24 Put_Line ("BR (from BI): " & BR'Image);
25
26 end Show_Big_Real_Big_Integer_Conversion;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Real_Big_Integer_
↪Conversion
MD5: 9a217c0551bc80269596d7217d2be879
Runtime output
Here, we use the To_Real and the To_Big_Real and functions for the conversions.
String conversions
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 with Ada.Numerics.Big_Numbers.Big_Reals;
9 use Ada.Numerics.Big_Numbers.Big_Reals;
10
11 procedure Show_Big_Number_String_Conversion
12 is
13 BI : Big_Integer;
14 BR : Big_Real;
15 begin
16 BI := From_String ("12345678901234567890");
17 BR := From_String ("12345678901234567890.0");
18
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Number_String_
↪Conversion
MD5: 3819df198ec140b457fa56a65d8876f9
Runtime output
BI: 2#1010101101010100101010011000110011101011000111110000101011010010#
BR: 12.345678E+18
In this example, we use the From_String to convert a string to a big number. Note that
the From_String function is actually called when converting a literal — because of the
corresponding aspect for user-defined literals in the definitions of the Big_Integer and the
Big_Real types.
-- Declaration from
-- Ada.Numerics.Big_Numbers.Big_Integers;
function From_Universal_Image
(Arg : String)
return Valid_Big_Integer
renames From_String;
-- Declaration from
-- Ada.Numerics.Big_Numbers.Big_Reals;
function From_Universal_Image
(Arg : String)
return Valid_Big_Real
renames From_String;
Also, we call the To_String function to get a string for the big numbers. Naturally, using
the To_String function instead of the Image attribute — as we did in previous examples —
allows us to customize the format of the string that we display in the user message.
Similar to integer types, big integers have the Big_Natural and Big_Positive subtypes to
indicate natural and positive numbers. However, in contrast to the Natural and Positive
subtypes, the Big_Natural and Big_Positive subtypes are defined via predicates rather
than the simple ranges of normal (ordinary) numeric types:
subtype Natural is
Integer range 0 .. Integer'Last;
subtype Positive is
Integer range 1 .. Integer'Last;
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Big_Positive_Natural is
9 BI, D, N : Big_Integer;
10 begin
11 D := 3;
12 N := 2;
13 BI := Big_Natural (D / Big_Positive (N));
14
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Positive_Natural
MD5: 6debfb86e11c7bfa3dbaf2d81eb24360
Runtime output
BI: 1
By using the Big_Natural and Big_Positive subtypes in the calculation above (in the
assignment to BI), we ensure that we don't perform a division by zero, and that the result
of the calculation is a natural number.
We can use the mod and rem operators with big integers:
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Big_Integer_Rem_Mod is
9 BI : Big_Integer;
10 begin
11 BI := 145 mod (-4);
12 Put_Line ("BI (mod): " & BI'Image);
13
Runtime output
BI (mod): -5
BI (rem): 1
In this example, we use the mod and rem operators in the assignments to BI.
Moreover, there's a Greatest_Common_Divisor function for big integers which, as the name
suggests, calculates the greatest common divisor of two big integer values:
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 procedure Show_Big_Integer_Greatest_Common_Divisor
9 is
10 BI : Big_Integer;
11 begin
12 BI := Greatest_Common_Divisor (145, 25);
13 Put_Line ("BI: " & BI'Image);
(continues on next page)
15 end Show_Big_Integer_Greatest_Common_Divisor;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Integer_Greatest_
↪Common_Divisor
MD5: b2d0098fcca6f949f228276b4d862b56
Runtime output
BI: 5
In this example, we retrieve the greatest common divisor of 145 and 25 (i.e.: 5).
An interesting feature of big reals is that they support quotients. In fact, we can simply
assign 2/3 to a big real variable. (Note that we're able to omit the decimal points, as we
write 2/3 instead of 2.0 / 3.0.) For example:
5 with Ada.Numerics.Big_Numbers.Big_Reals;
6 use Ada.Numerics.Big_Numbers.Big_Reals;
7
8 procedure Show_Big_Real_Quotient_Conversion
9 is
10 BR : Big_Real;
11 begin
12 BR := 2 / 3;
13 -- Same as:
14 -- BR := From_Quotient_String ("2 / 3");
15
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Real_Quotient_
↪Conversion
MD5: 4ef8355332e73a1f7da036b8e1e4b898
Runtime output
BR: 0.666
Q: 2 / 3
(continues on next page)
Range checks
Previously, we've talked about the Big_Natural and Big_Positive subtypes. In addition
to those subtypes, we have the In_Range function for big numbers:
5 with Ada.Numerics.Big_Numbers.Big_Integers;
6 use Ada.Numerics.Big_Numbers.Big_Integers;
7
8 with Ada.Numerics.Big_Numbers.Big_Reals;
9 use Ada.Numerics.Big_Numbers.Big_Reals;
10
11 procedure Show_Big_Numbers_In_Range is
12
13 BI : Big_Integer;
14 BR : Big_Real;
15
22 begin
23 BI := 1023;
24 BR := 1023.9;
25
46 end Show_Big_Numbers_In_Range;
Project: Courses.Advanced_Ada.Data_Types.Numerics.Big_Numbers.Big_Numbers_In_Range
MD5: 9c85e8374db1095142260f45c4c4e7e1
Runtime output
In this example, we call the In_Range function to check whether the big integer number
(BI) and the big real number (BR) are in the range between 0 and 1024.
TWENTYSIX
CONTROL FLOW
26.1 Expressions
According to the Ada Reference Manual, an expression "is a formula that defines the com-
putation or retrieval of a value." Also, when an expression is evaluated, the computed or
retrieved value always has an associated type known at compile-time.
Even though the definition above is very simple, Ada expressions are actually very flexible
— and they can also be very complex. In fact, if you read the corresponding section113
of the Ada Reference Manual, you'll quickly discover that they include elements such as
relations, membership choices, terms and primaries. Some of these are classic elements of
expressions in programming languages, although some of their forms are unique to Ada. In
this section, we present examples of just some of these elements. For a complete overview,
please refer to the Reference Manual.
Expressions usually consist of relations, which in turn consist of simple expressions. (There
are more details to this, but we'll keep it simple for the moment.) Let's see a code example
with a few expressions, which we dissect into the corresponding grammatical elements
(we're going to discuss them later):
Listing 1: show_expression_elements.adb
1 procedure Show_Expression_Elements is
2 type Mode is (Off, A, B, C, D);
3
9 M1, M2 : Mode;
10 Dummy : Boolean;
11 begin
(continues on next page)
113 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
114 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
581
Learning Ada
14 Dummy :=
15 M1 in Active_Mode
16 and then M2 in Off | A;
17 --
18 -- ^^^^^^^^^^^^^^^^^ relation
19 --
20 -- ^^^^^^^^^^^^^^ relation
21 -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22 -- expression
23
24 Dummy :=
25 M1 in Active_Mode;
26 -- ^^ name
27 -- ^^ primary
28 -- ^^ factor
29 -- ^^ term
30 -- ^^ simple expression
31 --
32 -- ^^^^^^^^^^^ membership choice
33 -- ^^^^^^^^^^^ membership choice list
34 --
35 -- ^^^^^^^^^^^^^^^^^ relation
36 -- ^^^^^^^^^^^^^^^^^ expression
37
38 Dummy :=
39 M2 in Off | A;
40 -- ^^ name
41 -- ^^ primary
42 -- ^^ factor
43 -- ^^ term
44 -- ^^ simple expression
45 --
46 -- ^^^ membership choice
47 -- ^ membership choice
48 -- ^^^^^^^ membership choice list
49 --
50 -- ^^^^^^^^^^^^^ relation
51 -- ^^^^^^^^^^^^^ expression
52
53 end Show_Expression_Elements;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Expressions_Definition.
↪Expression_Elements
MD5: a22e6f2d2bc181ce77097a1de204eb62
Build output
In this code example, we see three expressions. As we mentioned earlier, every expression
has a type; here, the type of each expression is Boolean.
The first expression (M1 in Active_Mode and then M2 in Off | A) consists of two
relations: M1 in Active_Mode and M2 in Off | A. Let's discuss some of the details.
The M1 in Active_Mode relation consists of the simple expression M1 and the membership
choice list Active_Mode. (Here, the in keyword is part of the relation definition.) Also, as
we see in the comments of the source code, the simple expression M1 is, at the same time,
a term, a factor, a primary and a name.
Let's briefly talk about this chain of syntactic elements for simple expressions. Very roughly
said, this is how we can break up simple expressions:
• a simple expression consists of terms;
• a term consists of factors;
• a factor consists of primaries;
• a primary can be one of those:
– a numeric literal;
– null;
– a string literal;
– an aggregate (page 435);
– a name;
– an allocator (like new Integer);
– a parenthesized expression (page 585);
– a conditional expression (page 588);
– a quantified expression (page 590);
– a declare expression (page 594).
simple_expression ::=
[unary_adding_operator] term {binary_adding_operator term}
primary ::=
numeric_literal | null | string_literal | aggregate
| name | allocator | (expression)
| (conditional_expression) | (quantified_expression)
| (declare_expression)
Later on in this chapter, we discuss conditional expressions (page 588), quantified expres-
sions (page 590) and declare expressions (page 594) in more details.
In the relation M2 in Off | A from the code example, Off | A is a membership choice
list, and Off and A are membership choices.
expression ::=
relation {and relation}
| relation {and then relation}
(continues on next page)
relation ::=
simple_expression
[relational_operator simple_expression]
| simple_expression [not] in
membership_choice_list
| raise_expression
Again, for more details, please refer to the section on expressions115 of the Ada Reference
Manual.
Numeric expressions
The expressions we've seen so far had the Boolean type. Although much of the grammar
described in the Manual exists exclusively for Boolean operations, we can also write numeric
expressions such as the following one:
Listing 2: show_numeric_expressions.adb
1 procedure Show_Numeric_Expressions is
2 C1 : constant Integer := 5;
3 Dummy : Integer;
4 begin
5 Dummy :=
6 -2 ** 4 + 3 * C1 ** 8;
7 -- ^ numeric literal
8 -- ^ primary
9 -- ^^ name
10 -- ^^ primary
11 -- ^^^^^^^ factor
12 -- ^ multiplying operator
13 -- ^ numeric literal
14 -- ^ primary
15 -- ^ factor
16 -- ^^^^^^^^^^^ term
17 --
18 -- ^ numeric literal
19 -- ^ primary
20 -- ^ numeric literal
21 -- ^ primary
22 -- ^^^^^^ factor
23 -- ^^^^^^ term
24 -- ^ binary adding operator
25 -- ^ unary adding operator
(continues on next page)
115 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
116 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
117 http://www.ada-auth.org/standards/22rm/html/RM-4-5-2.html
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Expressions_Definition.
↪Numeric_Expressions
MD5: a3c902c7aa5b0afe30ae220256c3306a
Other expressions
Expressions aren't limited to the Boolean type or to numeric types. Indeed, expressions
can be of any type, and the definition of primaries we've seen earlier on already hints in
this direction — as it includes elements such as allocators. Because expressions are very
flexible, covering all possible variations and combinations in this section is out of scope.
Again, please refer to the section on expressions119 of the Ada Reference Manual for further
details.
Parenthesized expression
Listing 3: show_parenthesized_expressions.adb
1 procedure Show_Parenthesized_Expressions is
2 C1 : constant Integer := 4;
3 C2 : constant Integer := 5;
4
5 Dummy : Integer;
6 begin
7 Dummy :=
8 (2 + C1) * C2;
9 -- ^^ name
10 -- ^^ primary
(continues on next page)
118 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
119 http://www.ada-auth.org/standards/22rm/html/RM-4-4.html
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Expressions_Definition.
↪Parenthesized_Expressions
MD5: 5871d2b0cd33e4f562b96381e0f0d293
In this example, we first start with the single expression (2 + C1) * C2, which is also a
simple expression consisting of just one term, which consists of two factors: (2 + C1) and
C2. The (2 + C1) factor is also a primary. Now, because of the parentheses, we identify
that the primary (2 + C1) is an expression that is embedded in another expression.
Important
To be fair, the existence of parentheses in a primary could also indicate other kinds of ex-
pressions, such as conditional or quantified expressions. However, differentiating between
them is straightforward, as we'll see later on in this chapter.
We then proceed to parse the (2 + C1) expression, which consists of the terms 2 and C1.
As we've seen in the comments of the code example, each of these terms consists of one
factor, which consists of one primary. In the end, after parsing the primaries, we identify
that 2 is a numeric literal and C1 is a name.
Note that the usage of parentheses might lead to situations where we have expressions in
potentially unsuspected places. For example, consider the following code example:
Listing 4: show_name_in_expression.adb
1 procedure Show_Name_In_Expression is
2 type Mode is (Off, A, B, C, D);
3
4 M1 : Mode;
5 begin
6 M1 := A;
7
8 case M1 is
9 when Off | D =>
(continues on next page)
15 end Show_Name_In_Expression;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Expressions_Definition.Name_
↪In_Expression
MD5: ec8fcbc511e6a372da4f0ad99d2619a5
Here, the case statement expects a selecting expression. In this case, M1 is identified as
a name — after being identified as a relation, a simple expression, a term, a factor and a
primary.
However, if we replace case M1 is by case (M1) is, (M1) is identified as a parenthe-
sized expression, not as a name! This parenthesized expression is first parsed and eval-
uated, which might have implications in case statements, as we'll see in another chapter
(page 612).
Let's look at another example, this time with a subprogram call:
Listing 5: increment_by_one.ads
1 procedure Increment_By_One (I : in out Integer);
Listing 6: increment_by_one.adb
1 procedure Increment_By_One (I : in out Integer) is
2 begin
3 I := I + 1;
4 end Increment_By_One;
Listing 7: show_name_in_expression.adb
1 with Increment_By_One;
2
3 procedure Show_Name_In_Expression is
4 V : Integer := 0;
5 begin
6 Increment_By_One ((V));
7 end Show_Name_In_Expression;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Expressions_Definition.Name_
↪In_Expression
MD5: 4805df49dc702e5cb365252e58742dd2
Build output
The Increment_By_One procedure from this example expects a variable as an actual param-
eter because the parameter mode is in out. However, the (V) in the call to the procedure
is interpreted as an expression, so we end up providing a value — the result of the expres-
sion — as the actual parameter instead of the V variable. Naturally, this is a compilation
error. (Of course, writing Increment_By_One (V) fixes the error.)
Listing 8: expr_func.ads
1 package Expr_Func is
2
6 end Expr_Func;
Let's say we have a system with four states Off, On, Waiting, and Invalid. For this system,
we want to implement a function named Toggled that returns the toggled value of a state
S. If the current value of S is either Off or On, the function toggles from Off to On (or from
On to Off). For other values, the state remains unchanged — i.e. the returned value is the
same as the input value. This is the implementation using a conditional expression:
Listing 9: expr_func.ads
1 package Expr_Func is
2
12 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Conditional_Expressions.
↪Conditional_If_Expressions_1
MD5: 7a99711afecc0b481557f9874dfbf4de
As you can see, if-expressions may contain an elsif branch (and therefore be more com-
plicated).
The code above corresponds to this more verbose version:
7 end Expr_Func;
14 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Conditional_Expressions.
↪Conditional_If_Expressions_2
MD5: 9e6cdf53c9c934f37e5717e1d230615a
If we compare the if-block of this code example to the if-expression of the previous example,
we notice that the if-expression is just a simplified version without the return keyword and
the end if;. In fact, converting an if-block to an if-expression is quite straightforward.
We could also replace the if-expression used in the Toggled function above with a case-
expression. For example:
11 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Conditional_Expressions.
↪Conditional_Case_Expressions_1
MD5: 0dd3a86f0872d1e8c3a81f7a17c44bd5
Note that we use commas in case-expressions to separate the alternatives (the when ex-
pressions). The code above corresponds to this more verbose version:
7 end Expr_Func;
12 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Conditional_Expressions.
↪Conditional_Case_Expressions_2
MD5: db6a0737e3931c83c31f53e4da3d8a2b
If we compare the case block of this code example to the case-expression of the previ-
ous example, we notice that the case-expression is just a simplified version of the case
block without the return keyword and the end case;, and with alternatives separated by
commas instead of semicolons.
Quantified expressions are for expressions using a quantifier — which can be either all or
some — and a predicate. This kind of expressions let us formalize statements such as:
• "all values of array A must be zero" into for all I in A'Range => A (I) = 0, and
• "at least one value of array A must be zero" into for some I in A'Range => A (I)
= 0.
In the quantified expression for all I in A'Range => A (I) = 0, the quantifier is all
and the predicate is A (I) = 0. In the second expression, the quantifier is some. The result
of a quantified expression is always a Boolean value.
For example, we could use the quantified expressions above and implement these two
functions:
• Is_Zero, which checks whether all components of an array A are zero, and
• Has_Zero, which checks whether array A has at least one component of the array A is
zero.
This is the complete code:
120 http://www.ada-auth.org/standards/22rm/html/RM-4-5-7.html
3 type Integer_Arr is
4 array (Positive range <>) of Integer;
5
17 end Int_Arrays;
15 end Int_Arrays;
5 procedure Test_Int_Arrays is
6 A : Integer_Arr := (0, 0, 1);
7 begin
8 Display_Array (A, "A");
9 Put_Line ("Is_Zero: "
10 & Boolean'Image (Is_Zero (A)));
11 Put_Line ("Has_Zero: "
12 & Boolean'Image (Has_Zero (A)));
13
14 A := (0, 0, 0);
15
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Quantified_Expression.
↪Quantified_Expression_1
MD5: 4bbda8a3830272748500f797f23f76fc
Runtime output
A: 0 0 1
Is_Zero: FALSE
Has_Zero: TRUE
A: 0 0 0
Is_Zero: TRUE
Has_Zero: TRUE
As you might have expected, we can rewrite a quantified expression as a loop in the for
I in A'Range loop if ... return ... form. In the code below, we're implementing
Is_Zero and Has_Zero using loops and conditions instead of quantified expressions:
3 type Integer_Arr is
4 array (Positive range <>) of Integer;
5
15 end Int_Arrays;
14 return True;
15 end Is_Zero;
16
26 return False;
27 end Has_Zero;
28
39 end Int_Arrays;
5 procedure Test_Int_Arrays is
6 A : Integer_Arr := (0, 0, 1);
7 begin
8 Display_Array (A, "A");
9 Put_Line ("Is_Zero: "
10 & Boolean'Image (Is_Zero (A)));
11 Put_Line ("Has_Zero: "
12 & Boolean'Image (Has_Zero (A)));
13
14 A := (0, 0, 0);
15
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Quantified_Expression.
↪Quantified_Expression_2
MD5: a957a8fd60e1849248efe1a84eae6afa
Runtime output
A: 0 0 1
Is_Zero: FALSE
Has_Zero: TRUE
A: 0 0 0
Is_Zero: TRUE
Has_Zero: TRUE
So far, we've seen quantified expressions using indices — e.g. for all I in A'Range =>
.... We could avoid indices in quantified expressions by simply using the E of A form. In
this case, we can just write for all E of A => .... Let's adapt the implementation of
Is_Zero and Has_Zero using this form:
3 type Integer_Arr is
4 array (Positive range <>) of Integer;
5
14 end Int_Arrays;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Quantified_Expression.
↪Quantified_Expression_3
MD5: 059d12a6529483ebcc5db23dc6262896
Here, we're checking the components E of the array A and comparing them against zero.
So far, we've seen expressions that make use of existing objects declared outside of the
expression. Sometimes, we might want to declare constant objects inside the expression,
so we can use them locally in the expression. Similarly, we might want to rename an
object and use the renamed object in an expression. In those cases, we can use a declare
expression.
A declare expression allows for declaring or renaming objects within an expression:
3 package P is
4
11 end P;
121 http://www.ada-auth.org/standards/22rm/html/RM-4-5-8.html
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Declare_Expressions.Simple_
↪Declare_Expression
MD5: 5da80e76393645d6eb1cb8cfe88e190a
The declare expression starts with the declare keyword and the usual object declarations,
and it's followed by the begin keyword and the body. In this example, the body of the
declare expression is a conditional expression.
Of course, the code above isn't really useful, so let's look at a more complete example:
3 package Integer_Arrays is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
11 --
12 -- Expression function using
13 -- declare expression:
14 --
15 function Avg (Arr : Integer_Array)
16 return Float is
17 (declare
18 A : Integer_Array renames Arr;
19 S : constant Float := Float (Sum (A));
20 L : constant Float := Float (A'Length);
21 begin
22 S / L);
23
24 end Integer_Arrays;
13 end Integer_Arrays;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Declare_Expressions.Integer_
↪Arrays
MD5: 8e96d49b1676f0aaf95437e271069690
Runtime output
Sum: 6
Avg: 2.00000E+00
In this example, the Avg function is implemented using a declare expression. In this ex-
pression, A renames the Arr array, and S is a constant initialized with the value returned
by the Sum function.
The declarative part of a declare expression is more restricted than the declarative part of
a subprogram or declare block. In fact, we cannot:
• declare variables;
• declare constants of limited types;
• rename an object of limited type that is constructed within the declarative part;
• declare aliased constants;
• declare constants that make use of the Access or Unchecked_Access attributes in the
initialization;
• declare constants of anonymous access type.
Let's see some examples of erroneous declarations:
3 package Integer_Arrays is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
10 type Const_Integer_Access is
11 access constant Integer;
12
19 --
20 -- Expression function using
21 -- declare expression:
22 --
23 function Avg (Arr : Integer_Array)
24 return Float is
25 (declare
26 A : Integer_Array renames Arr;
27
39 A1 : Const_Integer_Access :=
40 S1'Unchecked_Access;
41 -- ERROR: using 'Unchecked_Access
42 -- attribute
43
50 private
51
54 end Integer_Arrays;
17 end Integer_Arrays;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Declare_Expressions.Integer_
↪Arrays_Error
MD5: e1f72f817baea87f66fb34b6aa8d1949
Build output
In this version of the Avg function, we see many errors in the declarative part of the declare
expression. If we convert the declare expression into an actual function implementation,
however, those declarations won't trigger compilation errors. (Feel free to try this out!)
A reduction expression reduces a list of values into a single value. For example, we can
reduce the list [2, 3, 4] to a single value:
• by adding the values of the list: 2 + 3 + 4 = 9, or
• by multiplying the values of the list: 2 * 3 * 4 = 24.
We write a reduction expression by using the Reduce attribute and providing the reducer
and its initial value:
• the reducer is the operator (e.g.: + or *) that we use to combine the values of the list;
• the initial value is the value that we use before all other values of the list.
For example, if we use + as the operator and 0 an the initial value, we get the reduction
expression: 0 + 2 + 3 + 4 = 9. This can be implemented using an array:
3 procedure Show_Reduction_Expression is
4 A : array (1 .. 3) of Integer;
5 I : Integer;
6 begin
7 A := [2, 3, 4];
8 I := A'Reduce ("+", 0);
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Simple_Reduction_Expression
MD5: 1a0164b3c4768125c8dbbe8a0f4955a1
Runtime output
A =
[ 2, 3, 4]
I = 9
Here, we have the array A with a list of values. The A'Reduce ("+", 0) expression reduces
the list of values of A into a single value — in this case, an integer value that is stored in I.
This statement is equivalent to:
I := 0;
for E of A loop
I := I + E;
end loop;
3 procedure Show_Reduction_Expression is
4 A : array (1 .. 3) of Integer;
5 I : Integer;
6 begin
7 A := [2, 3, 4];
8 I := A'Reduce ("*", 1);
9
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Simple_Reduction_Expression
MD5: 415b1ee8b21cca6d2438a34c88e7e2df
Runtime output
A =
[ 2, 3, 4]
I = 24
In this example, we call A'Reduce ("*", 1) to reduce the list. (Note that we use an
initial value of one because it is the identity element123 of a multiplication, so the complete
operation is: 1 * 2 * 3 * 4 = 24.)
Value sequences
In addition to arrays, we can apply reduction expression to value sequences, which consist
of an iterated element association — for example, [for I in 1 .. 3 => I + 1]. We can
simply append the reduction expression to a value sequence:
3 procedure Show_Reduction_Expression is
4 I : Integer;
5 begin
6 I := [for I in 1 .. 3 =>
7 I + 1]'Reduce ("+", 0);
8 Put_Line ("I = "
9 & I'Image);
10
11 I := [for I in 1 .. 3 =>
12 I + 1]'Reduce ("*", 1);
13 Put_Line ("I = "
14 & I'Image);
15 end Show_Reduction_Expression;
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Reduction_Expression_Value_Sequences
MD5: e714f69700e3f0387314ee0e531620c4
Runtime output
I = 9
I = 24
In this example, we create the value sequence [for I in 1 .. 3 => I + 1] and reduce
it using the + and * operators. (Note that the operations in this example have the same
results as in the previous examples using arrays.)
123 https://en.wikipedia.org/wiki/Identity_element
124 http://www.ada-auth.org/standards/22rm/html/RM-4-5-10.html
Custom reducers
In the previous examples, we've used standard operators such as + and * as the reducer. We
can, however, write our own reducers and pass them to the Reduce attribute. For example:
3 procedure Show_Reduction_Expression is
4 type Integer_Array is
5 array (Positive range <>) of Integer;
6
7 A : Integer_Array (1 .. 3);
8 I : Long_Integer;
9
10 procedure Accumulate
11 (Accumulator : in out Long_Integer;
12 Value : Integer) is
13 begin
14 Accumulator := Accumulator
15 + Long_Integer (Value);
16 end Accumulate;
17
18 begin
19 A := [2, 3, 4];
20 I := A'Reduce (Accumulate, 0);
21
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Custom_Reducer_Procedure
MD5: 3190a1ff6a8027268ca96a75cf214714
Runtime output
A =
[ 2, 3, 4]
I = 9
In this example, we implement the Accumulate procedure as our reducer, which is called to
accumulate the individual elements (integer values) of the list. We pass this procedure to
the Reduce attribute in the I := A'Reduce (Accumulate, 0) statement, which is equivalent
to:
I := 0;
for E of A loop
Accumulate (I, E);
end loop;
Note that the accumulator type doesn't need to match the type of the individual compo-
nents. In this example, we're using Integer as the component type, while the accumulator
type is Long_Integer. (For this kind of reducers, using Long_Integer instead of Inte-
ger for the accumulator type makes lots of sense due to the risk of triggering overflows
while the reducer is accumulating values — e.g. when accumulating a long list with larger
numbers.)
In the example above, we've implemented the reducer as a procedure. However, we can
also implement it as a function. In this case, the accumulated value is returned by the
function:
3 procedure Show_Reduction_Expression is
4 type Integer_Array is
5 array (Positive range <>) of Integer;
6
7 A : Integer_Array (1 .. 3);
8 I : Long_Integer;
9
10 function Accumulate
11 (Accumulator : Long_Integer;
12 Value : Integer)
13 return Long_Integer is
14 begin
15 return Accumulator + Long_Integer (Value);
16 end Accumulate;
17
18 begin
19 A := [2, 3, 4];
20 I := A'Reduce (Accumulate, 0);
21
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Custom_Reducer_Function
MD5: ee5d5bb2b151ef7552d752c7e452127d
Runtime output
A =
[ 2, 3, 4]
I = 9
In this example, we converted the Accumulate procedure into a function (while the core
implementation is essentially the same).
Note that the reduction expression remains the same, independently of whether we're us-
ing a procedure or a function as the reducer. Therefore, the statement with the reduc-
tion expression in this example is the same as in the previous example: I := A'Reduce
(Accumulate, 0);. Now that we're using a function, this statement is equivalent to:
I := 0;
for E of A loop
(continues on next page)
The accumulator type isn't restricted to scalars: in fact, we could use record types as well.
For example:
3 procedure Show_Reduction_Expression is
4 type Integer_Array is
5 array (Positive range <>) of Integer;
6
7 A : Integer_Array (1 .. 3);
8
14 function Accumulate
15 (Accumulator : Integer_Accumulator;
16 Value : Integer)
17 return Integer_Accumulator is
18 begin
19 return (Value => Accumulator.Value
20 + Long_Integer (Value),
21 Count => Accumulator.Count + 1);
22 end Accumulate;
23
31 Acc : Integer_Accumulator;
32
33 begin
34 A := [2, 3, 4];
35
In this example, we're using the Integer_Accumulator record type in our reducer — the
Accumulate function. In this case, we're not only accumulating the values, but also counting
the number of elements in the list. (Of course, we could have used A'Length for that as
well.)
Also, we're not limited to numeric types: we can also create a reducer using strings as the
accumulator type. In fact, we can display the initial value and the elements of the list by
using unbounded strings:
3 with Ada.Strings.Unbounded;
4 use Ada.Strings.Unbounded;
5
6 procedure Show_Reduction_Expression is
7 type Integer_Array is
8 array (Positive range <>) of Integer;
9
10 A : Integer_Array (1 .. 3);
11
12 function Unbounded_String_List
13 (Accumulator : Unbounded_String;
14 Value : Integer)
15 return Unbounded_String is
16 begin
17 return Accumulator
18 & ", " & Value'Image;
19 end Unbounded_String_List;
20
21 begin
22 A := [2, 3, 4];
23
Project: Courses.Advanced_Ada.Control_Flow.Expressions.Reduction_Expressions.
↪Reducer_String_Accumulator
MD5: 43c54e93e404a235c8721db7c691a864
Runtime output
A =
[ 2, 3, 4]
L = 0, 2, 3, 4
In this case, the "accumulator" is concatenating the initial value and individual values of
the list into a string.
26.2 Statements
We can classify statements as either simple or compound. Simple statements don't contain
other statements; think of them as "atomic units" that cannot be further divided. Compound
statements, on the other hand, may contain other — simple or compound — statements.
Here are some examples from each category:
Category Examples
Simple statements Null statement, assignment, subprogram call, etc.
Compound statements If statement, case statement, loop statement, block statement
26.2.2 Labels
We can use labels to identify statements in the code. They have the following format:
<<Some_Label>>. We write them right before the statement we want to apply it to. Let's
see an example of labels with simple statements:
3 procedure Show_Statement_Identifier is
4 pragma Warnings (Off, "is not referenced");
5 begin
6 <<Show_Hello>> Put_Line ("Hello World!");
7 <<Show_Test>> Put_Line ("This is a test.");
8
9 <<Show_Separator>>
10 <<Show_Block_Separator>>
11 Put_Line ("====================");
12 end Show_Statement_Identifier;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Simple_Labels
MD5: 820f5963b476af5c04314fd4373d2286
Runtime output
Hello World!
This is a test.
====================
Here, we're labeling each statement. For example, we use the Show_Hello label to identify
the Put_Line ("Hello World!"); statement. Note that we can use multiple labels a single
statement. In this code example, we use the Show_Separator and Show_Block_Separator
labels for the same statement.
125 http://www.ada-auth.org/standards/22rm/html/RM-5-1.html
126 http://www.ada-auth.org/standards/22rm/html/RM-5-1.html
Labels are mainly used in combination with goto statements. (Although pretty much un-
common, we could potentially use labels to indicate important statements in the code.)
Let's see an example where we use a goto label; statement to jump to a specific label:
4 Some_Error : Boolean;
5 begin
6 Some_Error := False;
7
8 if Some_Error then
9 goto Cleanup;
10 end if;
11
12 <<Cleanup>> null;
13 end Show_Cleanup;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Label_Goto
MD5: 0ce06582bbefae818d4da3b7d2d3436b
Here, we transfer the control to the cleanup statement as soon as an error is detected.
Use-case: Continue
Another use-case is that of a Continue label in a loop. Consider a loop where we want to
skip further processing depending on a condition:
20 -- Do more stuff...
21
22 null;
23 end if;
24 end loop;
25 end Show_Continue;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Label_Continue_1
MD5: 115eeaf08d5fb072d707d6325fe9cfd0
22 -- Do more stuff...
23
24 <<Continue>>
25 end loop;
26 end Show_Continue;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Label_Continue_2
MD5: 260b52ead782adf76eee5cf3c4e8332b
Here, we use a Continue label at the end of the loop and jump to it in the case that no
further processing is needed. Note that, in this example, we don't have a statement after
the Continue label because the label itself is at the end of a statement — to be more
specific, at the end of the loop statement. In such cases, there's an implicit null statement.
Historically
Since Ada 2012, we can simply write:
loop
-- Some statements...
<<Continue>>
end loop;
loop
-- Some statements...
<<Continue>> null;
end loop;
We can use labels with compound statements as well. For example, we can label a for
loop:
3 procedure Show_Statement_Identifier is
4 pragma Warnings (Off, "is not referenced");
5
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Loop_Label
MD5: 5ca80b5a379ba0b08ccfaa4c6eab64d5
Runtime output
Found: TRUE
3 procedure Show_Statement_Identifier is
4 Arr : constant array (1 .. 5) of Integer :=
5 (1, 4, 6, 42, 49);
6 Found : Boolean := False;
7 begin
8 Find_42 : for E of Arr loop
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Statements.Labels.Loop_Statement_
↪Identifier
MD5: e52cb5eea9427addf3cabe64dd73bc2d
Runtime output
Found: TRUE
Loop statement and block statement identifiers are generally preferred over labels. Later
in this chapter, we discuss this topic in more detail.
We've introduced bare loops back in the Introduction to Ada course (page 15). In this
section, we'll briefly discuss loop names and exit loop statements.
A bare loop has this form:
loop
exit when Some_Condition;
end loop;
Loop_Name:
loop
exit Loop_Name when Some_Condition;
end loop Loop_Name;
In this case, we have to use the loop's name after end loop. Also, having a name for a loop
allows us to indicate which loop we're exiting from: exit Loop_Name when.
Let's see a complete example:
4 procedure Show_Vector_Cursor_Iteration is
5
11 use Integer_Vectors;
(continues on next page)
19 Show_Elements :
20 loop
21 exit Show_Elements when C = No_Element;
22
28 end Show_Vector_Cursor_Iteration;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Exit_Loop_Statement.Exit_
↪Named_Loop
MD5: b77353f6ed98f8ddb32c73c47d249020
Runtime output
Naming a loop is particularly useful when we have nested loops and we want to exit directly
from the inner loop:
7 Outer_Processing : loop
8
9 Inner_Processing : loop
10 exit Outer_Processing when Cond;
11 end loop Inner_Processing;
12
15 end Show_Inner_Loop_Exit;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Exit_Loop_Statement.Inner_
↪Loop_Exit
MD5: b5c7434f1bf23c2cb8f81e4c13a31386
Here, we indicate that we exit from the Outer_Processing loop in case a condition Cond is
met, even if we're actually within the inner loop.
In the Introduction to Ada course, we talked about if statements (page 11), loop statements
(page 13), and case statements (page 16). This is a very simple code example with these
statements:
19 --
20 -- Loop statement
21 --
22 for I in 1 .. 5 loop
23 Val := Val * 2 - I;
24 end loop;
25
26 --
27 -- Case statement
28 --
29 case Val is
30 when 0 .. 5 =>
31 null;
32 when others =>
33 Val := 5;
34 end case;
35
36 end Show_If_Case_Loop_Statements;
Project: Courses.Advanced_Ada.Control_Flow.Statements.If_Case_Loop_Statements.
↪Example
MD5: 4fdc7f00e5218ed59d9eb050339567f1
In this section, we'll look into a more advanced detail about the case statement.
127 http://www.ada-auth.org/standards/22rm/html/RM-5-7.html
As we know, the case statement has a choice expression (case Choice_Expression is),
which is expected to be a discrete type. Also, this expression can be a function call or a
type conversion, for example — in additional to being a variable or a constant.
As we discussed earlier on (page 585), if we use parentheses, the contents between those
parentheses is parsed as an expression. In the context of case statements, the expression
is first evaluated before being used as a choice expression. Consider the following code
example:
11 function To_Satisfaction_Scale
12 (S : Scale)
13 return Satisfaction_Scale;
14
15 end Scales;
3 function To_Satisfaction_Scale
4 (S : Scale)
5 return Satisfaction_Scale
6 is
7 Satisfaction : Satisfaction_Scale;
8 begin
9 case (S) is
10 when 0 .. 2 =>
11 Satisfaction := Very_Dissatisfied;
12 when 3 .. 4 =>
13 Satisfaction := Dissatisfied;
14 when 5 .. 6 =>
15 Satisfaction := OK;
16 when 7 .. 8 =>
17 Satisfaction := Satisfied;
(continues on next page)
128 http://www.ada-auth.org/standards/22rm/html/RM-5-3.html
129 http://www.ada-auth.org/standards/22rm/html/RM-5-4.html
130 http://www.ada-auth.org/standards/22rm/html/RM-5-5.html
22 return Satisfaction;
23 end To_Satisfaction_Scale;
24
25 end Scales;
5 procedure Show_Case_Statement_Expression is
6 Score : constant Scale := 0;
7 begin
8 Put_Line ("Score: "
9 & Scale'Image (Score)
10 & Satisfaction_Scale'Image (
11 To_Satisfaction_Scale (Score)));
12
13 end Show_Case_Statement_Expression;
MD5: 353ff771291e0c994ec052e818f9720c
Build output
scales.adb:9:07: error: missing case values: -128 .. -1
scales.adb:9:07: error: missing case values: 11 .. 127
gprbuild: *** compilation phase failed
When we try to compile this code example, the compiler complains about missing values in
the To_Satisfaction_Scale function. As we mentioned in the Introduction to Ada course
(page 16), every possible value for the choice expression needs to be covered by a unique
branch of the case statement. In principle, it seems that we're actually covering all possible
values of the Scale type, which ranges from 0 to 10. However, we've written case (S) is
instead of case S is. Because of the parentheses, (S) is evaluated as an expression. In
this case, the expected range of the case statement is not Scale'Range, but the range of
its base type (page 283) Scale'Base'Range.
In other languages
In C, the switch-case statement requires parentheses for the choice expression:
2 #include <stdio.h>
3
8 switch (s)
(continues on next page)
MD5: 64ef6b15f1bdf14ca9273964ec5e1755
Runtime output
Value in the 0 -- 1 range
Value > 1
In Ada, parentheses aren't expected in the choice expression. Therefore, we shouldn't write
case (S) is in a C-like fashion — unless, of course, we really want to evaluate an expression
in the case statement.
We've introduced block statements back in the Introduction to Ada course (page 19). They
have this simple form:
12 end Show_Block_Statement;
MD5: 61134b3899620c6d9ed68974fae33b5e
We can use an identifier when writing a block statement. (This is similar to loop statement
identifiers that we discussed in the previous section.) In this example, we implement a
block called Simple_Block:
5 Simple_Block : declare
6 I : Integer;
7 begin
8 I := 0;
9 end Simple_Block;
10
11 end Show_Block_Statement;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Block_Statements.Block_
↪Statement_Identifier
MD5: b327b7675931d9b994637671c806c7c3
Note that we must write end Simple_Block; when we use the Simple_Block identifier.
Block statement identifiers are useful:
• to indicate the begin and the end of a block — as some blocks might be long or nested
in other blocks;
• to indicate the purpose of the block (i.e. as code documentation).
A common idiom in Ada is to build up a function result in a local object, and then return that
object:
3 type Array_Of_Natural is
4 array (Positive range <>) of Natural;
5
17 begin
18 null;
19 end Show_Return;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Extended_Return_Statements.
↪Simple_Return
MD5: 16e85a8cba869802f912627c40a64c20
Since Ada 2005, a notation called the extended return statement is available: this allows
you to declare the result object and return it as part of one statement. It looks like this:
3 type Array_Of_Natural is
4 array (Positive range <>) of Natural;
5
17 begin
18 null;
19 end Show_Extended_Return;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Extended_Return_Statements.
↪Extended_Return
MD5: d6d6edaf800a0e346ff8ede13cbbe100
The return statement here creates Result, initializes it to 0, and executes the code between
do and end return. When end return is reached, Result is automatically returned as the
function result.
Note: This section was originally written by Robert A. Duff and published as Gem #10:
Limited Types in Ada 2005133 .
3 procedure Show_String_Construct is
4
5 function Make_String
6 (S : String;
7 Prefix : String;
8 Use_Prefix : Boolean) return String
9 is
10 Length : Natural := S'Length;
11 begin
12 if Use_Prefix then
13 Length := Length + Prefix'Length;
14 end if;
15
23 Result
24 (Prefix'Length + 1 .. Length) := S;
25 else
26 Result := S;
27 end if;
28
29 end return;
30 end Make_String;
31
32 S1 : String := "Ada";
33 S2 : String := "Make_With_";
34 begin
35 Put_Line ("No prefix: "
36 & Make_String (S1, S2, False));
37 Put_Line ("With prefix: "
38 & Make_String (S1, S2, True));
39 end Show_String_Construct;
Project: Courses.Advanced_Ada.Control_Flow.Statements.Extended_Return_Statements.
↪Extended_Return_Other_Usages
MD5: a2b26ceed06a0ab66aff6c2b59c02003
Runtime output
No prefix: Ada
With prefix: Make_With_Ada
In this example, we first calculate the length of the string and store it in Length. We then
use this information to initialize the return object of the Make_String function.
26.3 Subprograms
In this section, we discuss some details about parameter modes and associations. First of
all, as we know, parameters can be either formal or actual:
• Formal parameters are the ones we see in a subprogram declaration and implemen-
tation;
• Actual parameters are the ones we see in a subprogram call.
– Note that actual parameters are also called subprogram arguments in other lan-
guages.
We define parameter associations as the connection between an actual parameter in a
subprogram call and its declaration as a formal parameter in a subprogram specification or
body.
We already discussed formal parameter modes in the Introduction to Ada (page 28) course:
As this topic was already discussed in that course — and we used parameter modes exten-
sively in all code examples from that course —, we won't introduce the topic again here.
Instead, we'll look into some of the more advanced details.
In the Introduction to Ada (page 28) course, we saw that parameter modes don't correspond
directly to how parameters are actually passed. In fact, an in out parameter could be
passed by copy. For example:
3 procedure Check_Param_Passing
4 (Formal : System.Address;
5 Actual : System.Address);
134 http://www.ada-auth.org/standards/22rm/html/RM-6-2.html
135 http://www.ada-auth.org/standards/22rm/html/RM-6-4-1.html
4 procedure Check_Param_Passing
5 (Formal : System.Address;
6 Actual : System.Address) is
7 begin
8 Put_Line ("Formal parameter at "
9 & System.Address_Image (Formal));
10 Put_Line ("Actual parameter at "
11 & System.Address_Image (Actual));
12 if System.Address_Image (Formal) =
13 System.Address_Image (Actual)
14 then
15 Put_Line
16 ("Parameter is passed by reference.");
17 else
18 Put_Line
19 ("Parameter is passed by copy.");
20 end if;
21 end Check_Param_Passing;
3 package Machine_X is
4
5 procedure Update_Value
6 (V : in out Integer;
7 AV : System.Address);
8
9 end Machine_X;
5 procedure Update_Value
6 (V : in out Integer;
7 AV : System.Address) is
8 begin
9 V := V + 1;
10 Check_Param_Passing (Formal => V'Address,
11 Actual => AV);
12 end Update_Value;
13
14 end Machine_X;
3 procedure Show_By_Copy_By_Ref_Params is
4 A : Integer := 5;
5 begin
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.By_Copy_By_Ref_Params
MD5: e437d3432703124496f0a217177959eb
Runtime output
By reference By reference
• Tagged types
• Task and protected
types
• Explicitly limited
record types
• Composite types
with at least one
subcomponent of a
by-reference type
• Private types whose
full type is a by-
reference type
• Any descendant of
the types mentioned
above
Note that, for parameters of limited types, only those parameters whose type is explicitly
limited are always passed by reference. We discuss this topic in more details in another
chapter (page 967).
Let's see an example:
3 package Machine_X is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
21 procedure Update_Value
22 (R : in out Rec;
(continues on next page)
25 procedure Update_Value
26 (RA : in out Rec_Array;
27 ARA : System.Address);
28
29 procedure Update_Value
30 (R : in out Tagged_Rec;
31 AR : System.Address);
32
33 end Machine_X;
5 procedure Update_Value
6 (R : in out Rec;
7 AR : System.Address)
8 is
9 begin
10 R.A := R.A + 1;
11 Check_Param_Passing (Formal => R'Address,
12 Actual => AR);
13 end Update_Value;
14
15 procedure Update_Value
16 (RA : in out Rec_Array;
17 ARA : System.Address)
18 is
19 begin
20 RA.A := RA.A + 1;
21 Check_Param_Passing (Formal => RA'Address,
22 Actual => ARA);
23 end Update_Value;
24
25 procedure Update_Value
26 (R : in out Tagged_Rec;
27 AR : System.Address)
28 is
29 begin
30 R.A := R.A + 1;
31 Check_Param_Passing (Formal => R'Address,
32 Actual => AR);
33 end Update_Value;
34
35 end Machine_X;
4 procedure Show_By_Copy_By_Ref_Params is
5 TR : Tagged_Rec := (A => 5);
6 R : Rec := (A => 5);
7 RA : Rec_Array := (A => 5,
8 Arr => (others => 0));
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.By_Copy_By_Ref_Params
MD5: 3ca46380c4df36af9393041181ff2f17
Runtime output
Tagged record
Formal parameter at 00007FFD9D7686B0
Actual parameter at 00007FFD9D7686B0
Parameter is passed by reference.
Untagged record
Formal parameter at 00007FFD9D7684FC
Actual parameter at 00007FFD9D7686AC
Parameter is passed by copy.
Untagged record with array
Formal parameter at 00007FFD9D768510
Actual parameter at 00007FFD9D768510
Parameter is passed by reference.
When we run this example, we see that the object of tagged type (Tagged_Rec) is passed
by reference to the Update_Value procedure. In the case of the objects of untagged record
types, you might see this:
• the parameter of Rec type — which is an untagged record with a single component of
integer type —, the parameter is passed by copy;
• the parameter of Rec_Array type — which is an untagged record with a large array of
100 components —, the parameter is passed by reference.
Because Rec and Rec_Array are neither by-copy nor by-reference types, the decision about
how to pass them to the Update_Value procedure is made by the compiler. (Thus, it is
possible that you see different results when running the code above.)
Bounded errors
When we use parameters of types that are neither by-copy nor by-reference types, we
might encounter the situation where we have the same object bound to different names in
a subprogram. For example, if:
• we use a global object Global_R of a record type Rec
and
• we have a subprogram with an in-out parameter of the same record type Rec
and
• we pass Global_R as the actual parameter for the in-out parameter of this subpro-
gram,
then we have two access paths to this object: one of them using the global variable directly,
and the other one using it indirectly via the in-out parameter. This situation could lead to
undefined behavior or to a program error. Consider the following code example:
3 package Machine_X is
4
11 procedure Update_Value
12 (R : in out Rec;
13 AR : System.Address);
14
15 end Machine_X;
3 with Check_Param_Passing;
4
7 procedure Update_Value
8 (R : in out Rec;
9 AR : System.Address)
10 is
11 procedure Show_Vars is
12 begin
13 Put_Line ("Global_R.A: "
14 & Integer'Image (Global_R.A));
15 Put_Line ("R.A: "
16 & Integer'Image (R.A));
17 end Show_Vars;
18 begin
19 Check_Param_Passing (Formal => R'Address,
20 Actual => AR);
21
31 end Machine_X;
4 procedure Show_By_Copy_By_Ref_Params is
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.By_Copy_By_Ref_Params
MD5: 96be7054b7ff64a304705edf6b15f031
Runtime output
Calling Update_Value...
Formal parameter at 00007FFC5B7D4A9C
Actual parameter at 00000000008003BC
Parameter is passed by copy.
Incrementing Global_R.A...
Global_R.A: 1
R.A: 0
Incrementing R.A...
Global_R.A: 1
R.A: 5
After call to Update_Value...
Global_R.A: 5
In the Update_Value procedure, because Global_R and R have a type that is neither a
by-pass nor a by-reference type, the language does not specify whether the old or the
new value would be read in the calls to Put_Line. In other words, the actual behavior is
undefined. Also, this situation might raise the Program_Error exception.
Important
As a general advice:
• you should be very careful when using global variables and
• you should avoid passing them as parameters in situations such as the one illustrated
in the code example above.
Aliased parameters
5 procedure Update_Value
6 (V : aliased in out Integer;
7 AV : System.Address);
8
9 end Machine_X;
5 procedure Update_Value
6 (V : aliased in out Integer;
7 AV : System.Address)
8 is
9 begin
10 V := V + 1;
11 Check_Param_Passing (Formal => V'Address,
12 Actual => AV);
13 end Update_Value;
14
15 end Machine_X;
3 procedure Show_By_Copy_By_Ref_Params is
4 A : aliased Integer := 5;
5 begin
6 Update_Value (A, A'Address);
7 end Show_By_Copy_By_Ref_Params;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.By_Copy_By_Ref_Params
MD5: c066af3a7081815d0a7598733f9e6aec
Runtime output
3 procedure Show_By_Copy_By_Ref_Params is
4 A : Integer := 5;
5 begin
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.By_Copy_By_Ref_Params
MD5: 9e6586e0b771de68040131cae81799b8
Build output
Again, we discuss more about aliased parameters (page 785) and aliased objects (page 778)
later on in the context of access types.
Parameter Associations
When actual parameters are associated with formal parameters, some rules are checked.
As a typical example, the type of each actual parameter must match the type of the corre-
sponding actual parameter. In this section, we see some details about how this association
is made and some of the potential errors.
As we already know, when calling subprograms, we can use positional or named parameter
association — or a mixture of both. Also, parameters can have default values. Let's see
some examples:
6 end Operations;
9 end Operations;
3 procedure Show_Param_Association is
4 A : Integer := 5;
5 begin
6 -- Positional association
7 Add (A, 2.0);
8
9 -- Positional association
10 -- (using default value)
11 Add (A);
12
13 -- Named association
14 Add (Left => A,
15 Right => 2.0);
16
MD5: 64d3f44ac2bf72317fae22658f6d218e
This code snippet has examples of positional and name parameter association. Also, it has
an example of mixed positional / named parameter association. In most cases, the actual
A parameter is associated with the formal Left parameter, and the actual 2.0 parameter is
associated with the formal Right parameter.
In addition to that, parameters can have default values, so, when we write Add (A), the
A variable is associated with the Left parameter and the default value (1.0) is associated
with the Right parameter.
Also, when we use named parameter association, the parameter order is irrelevant: we
can, for example, write the last parameter as the first one. Therefore, we can write Add
(Right => 2.0, Left => A) instead of Add (Left => A, Right => 2.0).
Ambiguous calls
Ambiguous calls can be detected by the compiler during parameter association. For ex-
ample, when we have both default values in parameters and subprogram overloading, the
compiler might be unable to decide which subprogram we're calling:
8 end Operations;
14 end Operations;
3 procedure Show_Param_Association is
4 A : Integer := 5;
5 begin
6 Add (A);
7 -- ERROR: cannot decide which
8 -- procedure to take
9 end Show_Param_Association;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.Param_Association_1
MD5: 2725517f82d4068b669028eca1815079
Build output
As we see in this example, the Add procedure is overloaded. The first instance has one
parameter, and the second instance has two parameters, where the second parameter has
a default value. When we call Add with just one parameter, the compiler cannot decide
whether we intend to call
• the first instance of Add with one parameter
or
• the second instance of Add using the default value for the second parameter.
In this specific case, there are multiple options to solve the issue, but all of them involve
redesigning the package specification:
• we could just rename one of Add procedures (thereby eliminating the subprogram
overloading);
• we could rename the first parameter of one of the Add procedures and use named
parameter association in the call to the procedure;
– For example, we could rename the parameter to Value and call Add (Value =>
A).
• remove the default value from the second parameter of the second instance of Add.
When we have more than one out or in out parameters in a subprogram, we might run into
the situation where the actual parameter overlaps with another parameter. For example:
6 end Machine_X;
10 end Machine_X;
3 procedure Show_By_Copy_By_Ref_Params is
4 A : Integer := 5;
5 begin
6 Update_Value (A, A);
7 end Show_By_Copy_By_Ref_Params;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Parameter_Modes_
↪Associations.Illegal_Calls
MD5: d18a7056463fee9298dd1fdef0a31daf
Build output
In this case, we're using A for both output parameters in the call to Update_Value. Passing
one variable to more than one output parameter in a given call is forbidden in Ada, so this
triggers a compilation error. Depending on the specific context, you could solve this issue
by using temporary variables for the other output parameters.
26.3.2 Operators
Operators are commonly used for variables of scalar types such as Integer and Float. In
these cases, they replace usual function calls. (To be more precise, operators are function
calls, but written in a different format.) For example, we simply write A := A + B +
C; when we want to add three integer variables. A hypothetical, non-intuitive version of
this operation could be A := Add (Add (A, B), C);. In such cases, operators allow for
expressing function calls in a more intuitive way.
Many primitive operators exist for scalar types. We classify them as follows:
Category Operators
Logical and, or, xor
Relational =, /=, <, <=, >, >=
Unary adding +, -
Binary adding +, -, &
Multiplying *, /, mod, rem
Highest precedence **, abs, not
User-defined operators
For non-scalar types, not all operators are defined. For example, it wouldn't make sense to
expect a compiler to include an addition operator for a record type with multiple compo-
nents. Exceptions to this rule are the equality and inequality operators (= and /=), which
are defined for any type (be it scalar, record types, and array types).
For array types, the concatenation operator (&) is a primitive operator:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 end Integer_Arrays;
4 procedure Show_Array_Concatenation is
(continues on next page)
137 http://www.ada-auth.org/standards/22rm/html/RM-4-5.html
12 for E of R loop
13 Put (E'Image & ' ');
14 end loop;
15 New_Line;
16 end Show_Array_Concatenation;
MD5: 1899e66ec1d0b36b10d8b89fc2dfac0e
Runtime output
1 2 3 4 5 6 7 8 9 10
In this example, we're using the primitive & operator to concatenate the A and B arrays in
the assignment to R. Similarly, we're concatenating individual components (integer values)
to create an aggregate that we assign to A and B.
In contrast to this, the addition operator is not available for arrays:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 end Integer_Arrays;
4 procedure Show_Array_Addition is
5 A, B, R : Integer_Array (1 .. 5);
6 begin
7 A := (1 & 2 & 3 & 4 & 5);
8 B := (6 & 7 & 8 & 9 & 10);
9 R := A + B;
10
11 for E of R loop
12 Put (E'Image & ' ');
13 end loop;
14 New_Line;
15
16 end Show_Array_Addition;
Build output
We can, however, define custom operators for any type. For example, if a specific type
doesn't have a predefined addition operator, we can define our own + operator for it.
Note that we're limited to the operator symbols that are already defined by the Ada lan-
guage (see the previous table for the complete list of operators). In other words, the op-
erator we define must be selected from one of those existing symbols; we cannot use new
symbols for custom operators.
In other languages
Some programming languages — such as Haskell — allow you to define and use custom
operator symbols. For example, in Haskell, you can create a new "broken bar" (¦) operator
for integer values:
Let's define a custom addition operator that adds individual components of the Inte-
ger_Array type:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
12 end Integer_Arrays;
12 return R;
13 end "+";
14
15 end Integer_Arrays;
4 procedure Show_Array_Addition is
5 A, B, R : Integer_Array (1 .. 5);
6 begin
7 A := (1 & 2 & 3 & 4 & 5);
8 B := (6 & 7 & 8 & 9 & 10);
9 R := A + B;
10
11 for E of R loop
12 Put (E'Image & ' ');
13 end loop;
14 New_Line;
15
16 end Show_Array_Addition;
MD5: 6f50fa47270d97d3fb50379b6275777d
Runtime output
7 9 11 13 15
Now, the R := A + B line doesn't trigger a compilation error anymore because the +
operator is defined for the Integer_Array type.
In the implementation of the +, we return an array with the range of the Left array where
each component is the sum of the Left and Right arrays. In the declaration of the +
operator, we're defining the expected behavior in the postcondition. Here, we're saying
that, for each index of the resulting array (for all I in "+"'Result'Range), the value of
each component of the resulting array at that specific index is the sum of the components
from the Left and Right arrays at the same index ("+"'Result (I) = Left (I) + Right
(I)). (for all denotes a quantified expression (page 590).)
Note that, in this implementation, we assume that the range of Right is a subset of the
range of Left. If that is not the case, the Constraint_Error exception will be raised at
runtime in the loop. (You can test this by declaring B as Integer_Array (5 .. 10), for
example.)
We can also define custom operators for record types. For example, we could declare two
+ operators for a record containing the name and address of a person:
13 private
14
23 end Addresses;
34 end Addresses;
4 procedure Show_Address_Addition is
5 John : Person := "John" + "4 Main Street";
6 Jane : Person := "Jane" + "7 High Street";
7 begin
8 Display (John);
9 Display (Jane);
10 Put_Line ("----------------");
11
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Operators.Rec_Operator
MD5: c69ff43ed5a80a0c62bad87eada14301
Runtime output
Name: John
Address: 4 Main Street
Name: Jane
Address: 7 High Street
----------------
Name: Jane
Address: 4 Main Street
In this example, the first + operator takes two strings — with the name and address of a
person — and returns an object of Person type. We use this operator to initialize the John
and Jane variables.
The second + operator in this example brings two people together. Here, the person on the
left side of the + operator moves to the home of the person on the right side. In this specific
case, Jane is moving to John's house.
As a small remark, we usually expect that the + operator is commutative. In other words,
changing the order of the elements in the operation doesn't change the result. However,
in our definition above, this is not the case, as we can confirm by comparing the operation
in both orders:
4 procedure Show_Address_Addition is
5 John : constant Person :=
6 "John" + "4 Main Street";
7 Jane : constant Person :=
8 "Jane" + "7 High Street";
9 begin
10 if Jane + John = John + Jane then
11 Put_Line ("It's commutative!");
12 else
13 Put_Line ("It's not commutative!");
14 end if;
15 end Show_Address_Addition;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Operators.Rec_Operator
MD5: 2af6e1a31100a1d0fa786d42cc93c09b
Runtime output
In this example, we're using the primitive = operator for the Person to assess whether the
result of the addition is commutative.
Usually, we implement Ada functions with a construct like this: begin return X; end;. In
other words, we create a begin ... end; block and we have at least one return statement
in that block. An expression function, in contrast, is a function that is implemented with
a simple expression in parentheses, such as (X);. In this case, we don't use a begin ...
end; block or a return statement.
As an example of an expression, let's say we want to implement a function named Is_Zero
that checks if the value of the integer parameter I is zero. We can implement this function
with the expression I = 0. In the usual approach, we would create the implementation
by writing is begin return I = 0; end Is_Zero;. When using expression functions,
however, we can simplify the implementation by just writing is (I = 0);. This is the
complete code of Is_Zero using an expression function:
7 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.Simple_
↪Expression_Function_1
MD5: 44779999566f764279e1c2f292226f95
An expression function has the same effect as the usual version using a block. In fact, the
code above is similar to this implementation of the Is_Zero function using a block:
6 end Expr_Func;
9 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.Simple_
↪Expression_Function_2
MD5: 4d90b1c63928cbaf9c86a6cc6421bb61
The only difference between these two versions of the Expr_Func packages is that, in the
first version, the package specification contains the implementation of the Is_Zero func-
tion, while, in the second version, the implementation is in the body of the Expr_Func
package.
An expression function can be, at same time, the specification and the implementation
of a function. Therefore, in the first version of the Expr_Func package above, we don't
have a separate implementation of the Is_Zero function because (I = 0) is the actual
implementation of the function. Note that this is only possible for expression functions; you
cannot have a function implemented with a block in a package specification. For example,
the following code is wrong and won't compile:
9 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.Simple_
↪Expression_Function_3
MD5: 919f9c101b3224006e1302130eba8dd2
We can, of course, separate the function declaration from its implementation as an expres-
sion function. For example, we can rewrite the first version of the Expr_Func package and
move the expression function to the body of the package:
6 end Expr_Func;
7 end Expr_Func;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.Simple_
↪Expression_Function_4
MD5: 491a491da92636a35579f870969aaf08
In addition, we can use expression functions in the private part of a package specification.
For example, the following code declares the Is_Valid function in the specification of the
My_Data package, while its implementation is an expression function in the private part of
the package specification:
8 private
9
18 end My_Data;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.
↪Private_Expression_Function_1
MD5: beb57eca67b3954097e0f7ac00ea70c9
Naturally, we could write the function implementation in the package body instead:
8 private
9
14 end My_Data;
7 end My_Data;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Expression_Functions.
↪Private_Expression_Function_2
MD5: 3c6e2a3c53c7c8e1a7b86efccdc3bf8d
26.3.4 Overloading
Note: This section was originally written by Robert A. Duff and published as Gem #50:
Overload Resolution140 .
Ada allows overloading of subprograms, which means that two or more subprogram decla-
rations with the same name can be visible at the same place. Here, "name" can refer to
operator symbols, like "+". Ada also allows overloading of various other notations, such as
literals and aggregates.
In most languages that support overloading, overload resolution is done "bottom up" —
that is, information flows from inner constructs to outer constructs. As usual, computer
folks draw their trees upside-down, with the root at the top. For example, if we have two
procedures Print:
3 package Types is
4 type Sequence is null record;
(continues on next page)
139 http://www.ada-auth.org/standards/22rm/html/RM-6-8.html
140 https://www.adacore.com/gems/gem-50
11 use Types;
12
13 X : Sequence;
14 begin
15
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Overloading
MD5: 020c4f04285c80c1050d8edbaf2dbcae
3 package Types is
4 type Sequence is null record;
5 type Set is null record;
6
20 use Types;
21
22 X : Sequence;
23 begin
24 -- Compiler selects function
25 -- Empty return Sequence
26 Print_Sequence (Empty);
27 end Show_Top_Down_Overloading;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Overloading
MD5: 3b776a3efdee3d7e583ddbf5159c9a1b
The type of the formal parameter S of Print_Sequence determines which Empty is meant
in the call. In C++, for example, the equivalent of the Print (X) example would resolve,
but the Print_Sequence (Empty) would be illegal, because C++ does not use top-down
information.
If we overload things too heavily, we can cause ambiguities:
3 package Types is
4 type Sequence is null record;
5 type Set is null record;
6
20 use Types;
21
22 X : Sequence;
23 begin
24 Print (Empty); -- Illegal!
25 end Show_Overloading_Error;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Overloading
MD5: 5182c517a1afff4568ab2404ac66fda8
Build output
The call is ambiguous, and therefore illegal, because there are two possible meanings. One
way to resolve the ambiguity is to use a qualified expression to say which type we mean:
Print (Sequence'(Empty));
Note that we're now using both bottom-up and top-down overload resolution: Sequence'
determines which Empty is meant (top down) and which Print is meant (bottom up). You
can qualify an expression, even if it is not ambiguous according to Ada rules — you might
want to clarify the type because it might be ambiguous for human readers.
Of course, you could instead resolve the Print (Empty) example by modifying the source
code so the names are unique, as in the earlier examples. That might well be the best
solution, assuming you can modify the relevant sources. Too much overloading can be
confusing. How much is "too much" is in part a matter of taste.
Ada really needs to have top-down overload resolution, in order to resolve literals. In some
languages, you can tell the type of a literal by looking at it, for example appending L (letter
el) means "the type of this literal is long int". That sort of kludge won't work in Ada, because
we have an open-ended set of integer types:
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Literal_
↪Resolution
MD5: f428b6b4c642c44ede6bc21e7522c532
You can't tell by looking at the literal 20 what its type is. The type of formal parameter Count
tells us that 20 is an Apple_Count, as opposed to some other type, such as Standard.
Long_Integer.
Technically, the type of 20 is universal_integer, which is implicitly converted to Ap-
ple_Count — it's really the result type of that implicit conversion that is at issue. But that's
an obscure point — you won't go too far wrong if you think of the integer literal notation as
being overloaded on all integer types.
Developers sometimes wonder why the compiler can't resolve something that seems obvi-
ous. For example:
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Literal_
↪Resolution_Error
MD5: 4789d8eea9b82649ba8e453bb861688a
Build output
This call is ambiguous, and therefore illegal. But why? Clearly the developer must have
meant the Orange_Count one, because 10_000 is out of range for Apple_Count. And all the
relevant expressions happen to be static.
Well, a good rule of thumb in language design (for languages with overloading) is that the
overload resolution rules should not be "too smart". We want this example to be illegal to
avoid confusion on the part of developers reading the code. As usual, a qualified expression
fixes it:
Another example, similar to the literal, is the aggregate. Ada uses a simple rule: the type
of an aggregate is determined top down (i.e., from the context in which the aggregate
appears). Bottom-up information is not used; that is, the compiler does not look inside the
aggregate in order to determine its type.
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Overloading.Record_
↪Resolution_Error
MD5: e3dd1f1d0c403bcf672f4bab881b8ef9
Build output
There are two Grind procedures visible, so the type of the aggregate could be Complex or
String, so it is ambiguous and therefore illegal. The compiler is not required to notice that
there is only one type with components Re and Im, of some real type — in fact, the compiler
is not allowed to notice that, for overloading purposes.
We can qualify as usual:
Only after resolving that the type of the aggregate is Complex can the compiler look inside
and make sure Re and Im make sense.
This not-too-smart rule for aggregates helps prevent confusion on the part of developers
reading the code. It also simplifies the compiler, and makes the overload resolution algo-
rithm reasonably efficient.
We've seen previously (page 631) that we can define custom operators for any type. We've
also seen that subprograms can be overloaded (page 640). Since operators are functions,
we're essentially talking about operator overloading, as we're defining the same operator
(say + or -) for different types.
As another example of operator overloading, in the Ada standard library, operators are de-
fined for the Complex type of the Ada.Numerics.Generic_Complex_Types package. This
package contains not only the definition of the + operator for two objects of Complex type,
but also for combination of Complex and other types. For instance, we can find these dec-
larations:
This example shows that the + operator — as well as other operators — are being overloaded
in the Generic_Complex_Types package.
We can also override operators of derived types. This allows for modifying the behavior of
operators for the corresponding derived types.
To override an operator of a derived type, we simply implement a function for that operator.
This is the same as how we implement custom operators (as we've seen previously).
As an example, when adding two fixed-point values, the result might be out of range, which
causes an exception to be raised. A common strategy to avoid exceptions in this case is
to saturate the resulting value. This strategy is typically employed in signal processing
algorithms, for example.
In this example, we declare and use the 32-bit fixed-point type TQ31:
6 end Fixed_Point;
141 http://www.ada-auth.org/standards/22rm/html/RM-6-6.html
142 http://www.ada-auth.org/standards/22rm/html/RM-G-1-1.html
4 procedure Show_Sat_Op is
5 A, B, C : TQ31;
6 begin
7 A := TQ31'Last;
8 B := TQ31'Last;
9 C := A + B;
10
15 A := TQ31'First;
16 B := TQ31'First;
17 C := A + B;
18
23 end Show_Sat_Op;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Operator_Overriding.Fixed_
↪Point_Exception
MD5: 15d8860773ec7c0e505d0ee94781ae14
Runtime output
Here, we're using the standard + operator, which raises a Constraint_Error exception in
the C := A + B; statement due to an overflow. Let's now override the addition operator
and enforce saturation when the result is out of range:
9 end Fixed_Point;
25 end Fixed_Point;
4 procedure Show_Sat_Op is
5 A, B, C : TQ31;
6 begin
7 A := TQ31'Last;
8 B := TQ31'Last;
9 C := A + B;
10
15 A := TQ31'First;
16 B := TQ31'First;
17 C := A + B;
18
23 end Show_Sat_Op;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Operator_Overriding.Fixed_
↪Point_Operator_Overloading
MD5: 6317bcf9c278c01f86dbdcb761d86240
Runtime output
In the implementation of the overridden + operator of the TQ31 type, we declare another
type (TQ31_2) with a wider range than TQ31. We use variables of the TQ31_2 type to perform
the actual addition, and then we verify whether the result is still in TQ31's range. If it is, we
simply convert the result back to the TQ31 type. Otherwise, we saturate it — using either
the first or last value of the TQ31 type.
When overriding operators, the overridden operator replaces the original one. For example,
in the A + B operation of the Show_Sat_Op procedure above, we're using the overridden
version of the + operator, which performs saturation. Therefore, this operation doesn't raise
an exception (as it was the case with the original + operator).
Usually, when calling a procedure P, we expect that it returns to the caller's thread of con-
trol after performing some action in the body of P. However, there are situations where a
procedure never returns. We can indicate this fact by using the No_Return aspect in the
subprogram declaration.
A typical example is that of a server that is designed to run forever until the process is killed
or the machine where the server runs is switched off. This server can be implemented as
an endless loop. For example:
3 procedure Run_Server
4 with No_Return;
5
6 end Servers;
3 procedure Run_Server is
4 begin
5 pragma Warnings
6 (Off,
7 "implied return after this statement");
8 while True loop
9 -- Processing happens here...
10 null;
11 end loop;
12 end Run_Server;
13
14 end Servers;
3 procedure Show_Endless_Loop is
4 begin
5 Run_Server;
6 end Show_Endless_Loop;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Nonreturning_Procedures.
↪Server_Proc
MD5: 3f859b6e2aca8e31367658632e84126c
In this example, Run_Server doesn't exit from the while True loop, so it never returns to
the Show_Endless_Loop procedure.
The same situation happens when we call a procedure that raises an exception uncondition-
ally. In that case, exception handling is triggered, so that the procedure never returns to
the caller. An example is that of a logging procedure that writes a message before raising
an exception internally:
3 Logged_Failure : exception;
4
8 end Loggers;
11 end Loggers;
4 procedure Show_No_Return_Exception is
5 Check_Passed : constant Boolean := False;
6 begin
7 if not Check_Passed then
8 Log_And_Raise ("Check failed!");
9 Put_Line ("This line will not be reached!");
10 end if;
11 end Show_No_Return_Exception;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Nonreturning_Procedures.Log_
↪Exception
MD5: 10b4933d8c862d14ade54935cbd2b541
In this example, Log_And_Raise writes a message to the user and raises the
Logged_Failure, so it never returns to the Show_No_Return_Exception procedure.
We could implement exception handling in the Show_No_Return_Exception procedure,
so that the Logged_Failure exception could be handled there after it's raised in
Log_And_Raise. However, this wouldn't be considered a normal return to the procedure
because it wouldn't return to the point where it should (i.e. to the point where Put_Line is
about to be called, right after the call to the Log_And_Raise procedure).
If a nonreturning procedure returns nevertheless, this is considered a program error, so that
the Program_Error exception is raised. For example:
3 Logged_Failure : exception;
4
8 end Loggers;
10 end Loggers;
4 procedure Show_No_Return_Exception is
5 Check_Passed : constant Boolean := False;
6 begin
7 if not Check_Passed then
8 Log_And_Raise ("Check failed!");
9 Put_Line ("This line will not be reached!");
10 end if;
11 end Show_No_Return_Exception;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Nonreturning_Procedures.
↪Erroneous_Log_Exception
MD5: e44fd8df0529dda5749e85b9e300a999
Build output
loggers.adb:7:07: warning: implied return after this statement will raise Program_
↪Error [enabled by default]
Runtime output
Check failed!
It's important to highlight that the inlining we're referring above happens automatically, so
the decision about which subprogram is inlined depends entirely on the compiler. However,
in some cases, it's better to reduce the optimization level and perform manual inlining
instead of automatic inlining. We do that by using the Inline aspect.
Let's look at this example:
3 type Float_Array is
4 array (Positive range <>) of Float;
5
10 end Float_Arrays;
14 end Float_Arrays;
143 http://www.ada-auth.org/standards/22rm/html/RM-6-5-1.html
144 https://en.wikipedia.org/wiki/Inline_expansion
5 procedure Compute_Average is
6 Values : constant Float_Array :=
7 (10.0, 11.0, 12.0, 13.0);
8 Average_Value : Float;
9 begin
10 Average_Value := Average (Values);
11 Put_Line ("Average = "
12 & Float'Image (Average_Value));
13 end Compute_Average;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Inline_Subprograms.Inlining_
↪Float_Arrays
MD5: 246bc11e8a969d69873f416f583f450e
Runtime output
Average = 1.15000E+01
When compiling this example, the compiler will most probably inline Average in the Com-
pute_Average procedure. Note, however, that the Inline aspect is just a recommendation
to the compiler. Sometimes, the compiler might not be able to follow this recommendation,
so it won't inline the subprogram.
These are some examples of situations where the compiler might not be able to inline a
subprogram:
• when the code is too large,
• when it's too complicated — for example, when it involves exception handling —, or
• when it contains tasks, etc.
Note that we can use the Inline aspect for generic subprograms as well. When we do this,
we indicate to the compiler that we wish it inlines all instances of that generic subprogram.
Null procedures are procedures that don't have any effect, as their body is empty. We
declare a null procedure by simply writing is null in its declaration. For example:
5 end Null_Procs;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Null_Proc_1
MD5: a8a801e6c71d8177db61e4aa131b8832
As expected, calling a null procedure doesn't have any effect. For example:
3 procedure Show_Null_Proc is
4 begin
5 Do_Nothing ("Hello");
6 end Show_Null_Proc;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Null_Proc_1
MD5: 274eed0b0952b9aa7e422933ece42d86
Null procedures are equivalent to implementing a procedure with a body that only contains
null. Therefore, the Do_Nothing procedure above is equivalent to this:
5 end Null_Procs;
8 end Null_Procs;
145 http://www.ada-auth.org/standards/22rm/html/RM-6-3-2.html
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Null_Proc_1
MD5: d0c9dc9265ebbaa9603681182dee1d92
We can use null procedures as a way to simulate interfaces for non-tagged types — similar
to what actual interfaces do for tagged types. For example, we may start by declaring a
type and null procedures that operate on that type. For example, let's model a very simple
API:
9 end Simple_Storage;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Simple_
↪Storage_Model
MD5: 553e78bc15dcec1302be4b5f484ac21f
Here, the API of the Storage_Model type consists of the Set and Display procedures. Nat-
urally, we can use objects of the Storage_Model type in an application, but this won't have
any effect:
4 procedure Show_Null_Proc is
5 S : Storage_Model;
6 begin
7 Put_Line ("Setting 24...");
8 Set (S, "24");
9 Display (S);
10 end Show_Null_Proc;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Simple_
↪Storage_Model
MD5: 523b3e7239e683f2d879caa9139106ca
Runtime output
Setting 24...
By itself, the Storage_Model type is not very useful. However, we can derive other types
from it and override the null procedures. Let's say we want to implement the Inte-
ger_Storage type to store an integer value:
15 private
16
21 end Simple_Storage;
16 end Simple_Storage;
4 procedure Show_Null_Proc is
5 S : Integer_Storage;
6 begin
7 Put_Line ("Setting 24...");
8 Set (S, "24");
9 Display (S);
10 end Show_Null_Proc;
Project: Courses.Advanced_Ada.Control_Flow.Subprograms.Null_Procedures.Simple_
↪Storage_Model
MD5: 55d491d1ef72fb7be2bf0d2a212f335b
Runtime output
Setting 24...
Value: 24
In this example, we can view Storage_Model as a sort of interface for derived non-tagged
types, while the derived types — such as Integer_Storage — provide the actual imple-
mentation.
The section on null records (page 420) contains an extended example that makes use of
null procedures.
26.4 Exceptions
26.4.1 Asserts
When we want to indicate a condition in the code that must always be valid, we can use
the pragma Assert. As the name implies, when we use this pragma, we're asserting some
truth about the source-code. (We can also use the procedural form, as we'll see later.)
Important
Another method to assert the truth about the source-code is to use pre and post-conditions.
In this example, we're asserting that the value of I is always 10. We could also display a
message if the assertion is false:
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Asserts.Pragma_Assert_2
MD5: b70fa67c92542ade39c388964ce12302
Build output
Runtime output
Similarly, we can use the procedural form of Assert. For example, the code above can
implemented as follows:
3 procedure Show_Procedure_Assert is
4 I : constant Integer := 11;
5
6 begin
7 Assert (I = 10, "I is not 10");
8 end Show_Procedure_Assert;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Asserts.Procedure_Assert
MD5: cbab23645ff89d4adffcaaddaeb6f0e3
Runtime output
Note that a call to Assert is simply translated to a check — and the Assertion_Error
exception from the Ada.Assertions package being raised in the case that the check fails.
For example, the code above roughly corresponds to this:
3 procedure Show_Assertion_Error is
4 I : constant Integer := 11;
5
6 begin
7 if I /= 10 then
8 raise Assertion_Error with "I is not 10";
9 end if;
10
11 end Show_Assertion_Error;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Asserts.Assertion_Error
MD5: 9c846acf998ca7adabd47c3b5a6ce39f
Runtime output
We can activate and deactivate assertions based on assertion policies. We can do that by
using the pragma Assertion_Policy. As an argument to this pragma, we indicate whether
a specific policy must be checked or ignored.
For example, we can deactivate assertion checks by specifying Assert => Ignore. Simi-
larly, we can activate assertion checks by specifying Assert => Check. Let's see a code
example:
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Assertion_Policies.Pragma_
↪Assertion_Policy_1
MD5: 39b8aa4a34b6169c03b54074f4136519
Build output
Here, we're specifying that asserts shall be ignored. Therefore, the call to the pragma
Assert doesn't raise an exception. If we replace Ignore with Check in the call to Asser-
tion_Policy, the assert will raise the Assertion_Error exception.
The following table presents all policies that we can set:
147 http://www.ada-auth.org/standards/22rm/html/RM-11-4-2.html
Policy Descripton
Assert Check assertions
Static_Predicate Check static predicates
Dynamic_Predicate Check dynamic predicates
Pre Check pre-conditions
Pre'Class Check pre-condition of classes of tagged types
Post Check post-conditions
Post'Class Check post-condition of classes of tagged types
Type_Invariant Check type invariants
Type_Invariant'Class Check type invariant of classes of tagged types
You can specify multiple policies in a single call to Assertion_Policy. For example, you
can activate all policies by writing:
148 https://gcc.gnu.org/onlinedocs/gnat_rm/Pragma-Assertion_005fPolicy.html
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Assertion_Policies.Multiple_
↪Assertion_Policies
MD5: 3abbf97160b755b84cc4f7e652ca5551
Note that the Assert procedure raises an exception independently of the assertion policy
(Assertion_Policy (Assert => Ignore)). For example:
4 procedure Show_Assert_Procedure_Policy is
5 pragma Assertion_Policy (Assert => Ignore);
6
7 I : constant Integer := 1;
8 begin
9 Put_Line ("------ Pragma Assert -----");
10 pragma Assert (I = 0);
11
15 Put_Line ("Finished.");
16 end Show_Assert_Procedure_Policy;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Assertion_Policies.Assert_
↪Procedure_Policy
MD5: 7be3bab24d856081afeddabe40afc84f
Build output
Runtime output
Here, the pragma Assert is ignored due to the assertion policy. However, the call to Assert
is not ignored.
This table shows all language-defined checks and the associated exceptions:
Check Exception
Access_Check Constraint_Error
Discriminant_Check Constraint_Error
Division_Check Constraint_Error
Index_Check Constraint_Error
Length_Check Constraint_Error
Overflow_Check Constraint_Error
Range_Check Constraint_Error
Tag_Check Constraint_Error
Accessibility_Check Program_Error
Allocation_Check Program_Error
Elaboration_Check Program_Error
Storage_Check Storage_Error
In addition, we can use All_Checks to refer to all those checks above at once.
Let's discuss each check and see code examples where those checks are performed. Note
that all examples are erroneous, so please avoid reusing them elsewhere.
150 http://www.ada-auth.org/standards/22rm/html/RM-11-4-2.html
Access Check
As you know, an object of an access type might be null. It would be an error to dereference
this object, as it doesn't indicate a valid position in memory. Therefore, the access check
verifies that an access object is not null when dereferencing it. For example:
5 AI : Integer_Access;
6 begin
7 AI.all := 10;
8 end Show_Access_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.Access_
↪Check
MD5: 4db8b63efb23caa7da926d4ec9f204bf
Build output
Runtime output
3 type Integer_Access is
4 access all Integer;
5
6 type Safe_Integer_Access is
7 not null access all Integer;
8
9 AI : Integer_Access;
10 SAI : Safe_Integer_Access := new Integer;
11
12 begin
13 SAI := Safe_Integer_Access (AI);
14 end Show_Access_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.Access_
↪Check_2
MD5: 47895a404e2a111476cd67f43c12d4b5
Build output
Runtime output
Here, the value of AI is null (by default), so we cannot assign it to SAI because its type
excludes null.
Note that, if we remove the := new Integer assignment from the declaration of SAI, the
null exclusion fails in the declaration itself (because the default value of the access type is
null).
Discriminant Check
As we've seen earlier, a variant record is a record with discriminants that allows for changing
its structure. In operations such as an assignment, it's important to ensure that the discrim-
inants of the objects match — i.e. to ensure that the structure of the objects matches. The
discriminant check verifies whether this is the case. For example:
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Discriminant_Check
MD5: 665ab37962f8f9c129acac543b1eb15d
Build output
Runtime output
Here, R's discriminant (Valid) is False, so we cannot assign an object whose Valid dis-
criminant is True.
Also, when accessing a component, the discriminant check ensures that this component
exists for the current discriminant value:
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Discriminant_Check_2
MD5: 440973b0be7c4261ddf3c2211a2c1325
Build output
Runtime output
Here, R's discriminant (Valid) is False, so we cannot access the Counter component, for
it only exists when the Valid discriminant is True.
Division Check
The division check verifies that we're not trying to divide a value by zero when using the /,
rem and mod operators. For example:
3 procedure Show_Division_Check is
4 I : Integer;
5 begin
6 I := Div_Op (10, 0);
7 I := Rem_Op (10, 0);
8 I := Mod_Op (10, 0);
9 end Show_Division_Check;
MD5: 6ec0856be947eea6610cffaa0e875d45
Runtime output
All three calls in the Show_Division_Check procedure — to the Div_Op, Rem_Op and Mod_Op
functions — can raise an exception because we're using 0 as the second argument, which
makes the division check in those functions fail.
Index Check
We use indices to access components of an array. An index check verifies that the index
we're using to access a specific component is within the array's bounds. For example:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
22 begin
23 Arr_1 (10) := Value_Of (Arr_1, 10);
24
25 end Show_Index_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.Index_
↪Check
MD5: fa791718701c4ac805badf368df9064e
Runtime output
Length Check
In array assignments, both arrays must have the same length. To ensure that this is the
case, a length check is performed. For example:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
16 begin
(continues on next page)
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.Length_
↪Check
MD5: a521afd0a46a67d260e8b0bd5f046ce4
Runtime output
Here, the length of Arr_1 is 10, while the length of Arr_2 is 9, so we cannot assign Arr_2
(From parameter) to Arr_1 (To parameter) in the Assign procedure.
Overflow Check
Operations on scalar objects might lead to overflow, which, if not checked, lead to wrong
information being computed and stored. Therefore, an overflow check verifies that the
value of a scalar object is within the base range of its type. For example:
7 A := A + B;
8 end Show_Overflow_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Overflow_Check
MD5: baa46d9085cbd14863aaa7e24dc7b9cc
Build output
Runtime output
In this example, A already has the last possible value of the Integer'Base range, so in-
creasing it by one causes an overflow error.
Range Check
The range check verifies that a scalar value is within a specific range — for instance, the
range of a subtype. Let's see an example:
5 I : Int_1_10;
6
7 begin
8 I := 11;
9 end Show_Range_Check;
MD5: 54b1d67d98d97a58d4265a854fcfa992
Build output
show_range_check.adb:8:09: warning: value not in range of type "Int_1_10" defined␣
↪at line 3 [enabled by default]
Runtime output
In this example, we're trying to assign 11 to the variable I of the Int_1_10 subtype, which
has a range from 1 to 10. Since 11 is outside that range, the range check fails.
Tag Check
The tag check ensures that the tag of a tagged object matches the expected tag in a dis-
patching operation. For example:
7 end P;
4 with P; use P;
5
11 begin
12 Put_Line ("A1'Tag: "
13 & Ada.Tags.Expanded_Name (A1'Tag));
14 Put_Line ("A2'Tag: "
15 & Ada.Tags.Expanded_Name (A2'Tag));
16
17 A2 := A1;
18
19 end Show_Tag_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.Tag_
↪Check
MD5: 5a685be7804200a884649f54c175ee42
Runtime output
A1'Tag: P.T1
A2'Tag: P.T2
Accessibility Check
The accessibility check verifies that the accessibility level of an entity matches the expected
level. We discuss accessibility levels in a later chapter (page 788).
Let's look at an example that mixes access types and anonymous access types. Here, we
use an anonymous access type in the declaration of A1 and a named access type in the
declaration of A2:
6 end P;
3 procedure Show_Accessibility_Check is
4
8 begin
9 A2 := T_Class (A1);
10
11 end Show_Accessibility_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Accessibility_Check
MD5: 7120d908b55ef576db93e9a15db257f2
Build output
Runtime output
The anonymous type (access T'Class), which is used in the declaration of A1, doesn't
have the same accessibility level as the T_Class type. Therefore, the accessibility check
fails during the T_Class (A1) conversion.
We can see the accessibility check failing in this example as well:
3 procedure Show_Accessibility_Check is
4
9 begin
10 P (T_Class (A));
11
12 end Show_Accessibility_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Accessibility_Check
MD5: 97db82410dd3459249d0e7a97118b7ef
Build output
Runtime output
Again, the check fails in the T_Class (A) conversion and raises a Program_Error exception.
Allocation Check
The allocation check ensures, when a task is about to be created, that its master has not
been completed or the finalization has not been started.
This is an example adapted from AI-00280151 :
4 package P is
5 type T1 is new
6 Ada.Finalization.Controlled with null record;
7 procedure Finalize (X : in out T1);
8
9 type T2 is new
10 Ada.Finalization.Controlled with null record;
11 procedure Finalize (X : in out T2);
12
13 X1 : T1;
14
3 package body P is
4
17 end P;
3 procedure Show_Allocation_Check is
(continues on next page)
151 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ais/ai-00280.txt?rev=1.12&raw=N
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Allocation_Check
MD5: 915e8ab21e550c981503c014bcceade1
Runtime output
Finalizing T2...
Here, in the finalization of the X1 object of T1 type, we're trying to create an object of
T2 type. This is forbidden and, therefore, the allocation check raises a Program_Error
exception.
Elaboration Check
The elaboration check verifies that subprograms — or protected entries, or task activations
— have been elaborated before being called.
This is an example adapted from AI-00064152 :
3 procedure Show_Elaboration_Check is
4
7 type Pointer_To_Func is
8 access function return Integer;
9
12 Y : constant Integer := F;
13 Z : constant Pointer_To_Func := X;
14
15 -- Renaming-as-body
16 function F return Integer renames Z.all;
(continues on next page)
152 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ais/ai-00064.txt?rev=1.12&raw=N
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Checks_And_Exceptions.
↪Elaboration_Check
MD5: 80a39df912aae8788296f81ee9d4a79e
Build output
Runtime output
This is a curious example: first, we declare a function F and assign the value returned by this
function to constant Y in its declaration. Then, we declare F as a renamed function, thereby
providing a body to F — this is called renaming-as-body. Consequently, the compiler doesn't
complain that a body is missing for function F. (If you comment out the function renaming,
you'll see that the compiler can then detect the missing body.) Therefore, at runtime, the
elaboration check fails because the body of the first declaration of the F function is actually
missing.
Storage Check
The storage check ensures that the storage pool has enough space when allocating memory.
Let's revisit an example that we discussed earlier (page 355):
8 end Custom_Types;
5 procedure Show_Storage_Check is
6
9 begin
10 Put_Line ("Allocating RAV1...");
(continues on next page)
16 New_Line;
17 end Show_Storage_Check;
MD5: 4e4bd284adb1c1d97f8f7563068c18de
Runtime output
Allocating RAV1...
Allocating RAV2...
On each allocation (new UInt_7), a storage check is performed. Because there isn't enough
reserved storage space before the second allocation, the checks fails and raises a Stor-
age_Error exception.
Note: Parts of this section were originally published as Gem #142 : Exception-ally154
The standard Ada run-time library provides the package Ada.Exceptions. This package
provides a number of services to help analyze exceptions.
Each exception is associated with a (short) message that can be set by the code that raises
the exception, as in the following code:
raise Constraint_Error with "some message";
Historically
Since Ada 2005, we can use the raise Constraint_Error with "some message" syntax.
In Ada 95, you had to call the Raise_Exception procedure:
Ada.Exceptions.Raise_Exception -- Ada 95
(Constraint_Error'Identity, "some message");
153 http://www.ada-auth.org/standards/22rm/html/RM-11-5.html
154 https://www.adacore.com/gems/gem-142-exceptions
Exceptions also embed information set by the run-time itself that can be retrieved by calling
the Exception_Information function. The function Exception_Information also displays
the Exception_Message.
For example:
exception
when E : others =>
Put_Line
(Ada.Exceptions.Exception_Information (E));
You can also retrieve this information individually. Here, you can use:
• the Exception_Name functions — and its derivatives Wide_Exception_Name and
Wide_Wide_Exception_Name — to retrieve the name of an exception.
• the Exception_Message function to retrieve the message associated with an excep-
tion.
Let's see a complete example:
4 procedure Show_Exception_Info is
5
6 Custom_Exception : exception;
7
8 procedure Nested is
9 begin
10 raise Custom_Exception
11 with "We got a problem";
12 end Nested;
13
14 begin
15 Nested;
16
Collecting exceptions
Save_Occurrence
You can save an exception occurrence using the Save_Occurrence procedure. (Note that a
Save_Occurrence function exists as well.)
For example, the following application collects exceptions into a list and displays them after
running the Test_Exceptions procedure:
3 package Exception_Tests is
4
5 Custom_Exception : exception;
6
7 type All_Exception_Occur is
8 array (Positive range <>) of
9 Exception_Occurrence;
10
11 procedure Test_Exceptions
12 (All_Occur : in out All_Exception_Occur;
13 Last_Occur : out Integer);
14
15 end Exception_Tests;
3 procedure Save_To_List
4 (E : Exception_Occurrence;
5 All_Occur : in out All_Exception_Occur;
6 Last_Occur : in out Integer)
7 is
8 L : Integer renames Last_Occur;
9 O : All_Exception_Occur renames All_Occur;
10 begin
11 L := L + 1;
12 if L > O'Last then
13 raise Constraint_Error
14 with "Cannot save occurrence";
15 end if;
16
21 procedure Test_Exceptions
22 (All_Occur : in out All_Exception_Occur;
23 Last_Occur : out Integer)
24 is
25
26 procedure Nested_1 is
27 begin
28 raise Custom_Exception
29 with "We got a problem";
30 exception
31 when E : others =>
32 Save_To_List (E,
33 All_Occur,
34 Last_Occur);
35 end Nested_1;
36
37 procedure Nested_2 is
38 begin
39 raise Constraint_Error
40 with "Constraint is not correct";
41 exception
42 when E : others =>
43 Save_To_List (E,
44 All_Occur,
45 Last_Occur);
46 end Nested_2;
47
48 begin
49 Last_Occur := 0;
50
51 Nested_1;
52 Nested_2;
53 end Test_Exceptions;
54
55 end Exception_Tests;
6 procedure Show_Exception_Info is
7 L : Integer;
8 O : All_Exception_Occur (1 .. 10);
9 begin
10 Test_Exceptions (O, L);
11
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exceptions_Package.Save_
↪Occurrence
MD5: da0cc5db7039e1458dbcf8be49db969d
Runtime output
Similarly, we can use files to read and write exception occurrences. To do that, we can
simply use the Read and Write attributes.
3 with Ada.Streams.Stream_IO;
4 use Ada.Streams.Stream_IO;
5
6 with Ada.Exceptions;
7 use Ada.Exceptions;
8
9 procedure Exception_Occurrence_Stream is
10
11 Custom_Exception : exception;
12
13 S : Stream_Access;
14
15 procedure Nested_1 is
16 begin
17 raise Custom_Exception
18 with "We got a problem";
19 exception
20 when E : others =>
21 Exception_Occurrence'Write (S, E);
22 end Nested_1;
23
24 procedure Nested_2 is
25 begin
26 raise Constraint_Error
27 with "Constraint is not correct";
28 exception
29 when E : others =>
30 Exception_Occurrence'Write (S, E);
31 end Nested_2;
32
33 F : File_Type;
34 File_Name : constant String :=
35 "exceptions_file.bin";
36 begin
37 Create (F, Out_File, File_Name);
38 S := Stream (F);
39
40 Nested_1;
41 Nested_2;
42
45 Read_Exceptions : declare
46 E : Exception_Occurrence;
47 begin
48 Open (F, In_File, File_Name);
49 S := Stream (F);
50
54 Ada.Text_IO.Put_Line
55 (Exception_Information (E));
56 end loop;
57 Close (F);
58 end Read_Exceptions;
59
60 end Exception_Occurrence_Stream;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exceptions_Package.Exception_
↪Occurrence_Stream
MD5: 3d9f2bd9480aa6dcc250b249b9ef4870
Runtime output
In this example, we store the exceptions raised in the application in the excep-
tions_file.bin file. In the exception part of procedures Nested_1 and Nested_2, we
call Exception_Occurrence'Write to store an exception occurence in the file. In
the Read_Exceptions block, we read the exceptions from the the file by calling
Exception_Occurrence'Read.
Here is a typical exception handler that catches all unexpected exceptions in the applica-
tion:
4 procedure Main is
5
6 procedure Nested is
7 begin
8 raise Constraint_Error
9 with "some message";
10 end Nested;
11
12 begin
13 Nested;
14
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exceptions_Package.Exception_
↪Information
MD5: f95068ca90d79b92a7c2031322349153
Runtime output
The output we get when running the application is not very informative. To get more infor-
mation, we need to rerun the program in the debugger. To make the session more inter-
esting though, we should add debug information in the executable, which means using the
-g switch in the gnatmake command.
The session would look like the following (omitting some of the output from the debugger):
(gdb) bt
#0 <__gnat_debug_raise_exception> (e=0x62ec40 <constraint_error>) at s-excdeb.
↪adb:43
And we now know exactly where the exception was raised. But in fact, we could have this
information directly when running the application. For this, we need to bind the applica-
tion with the switch -E, which tells the binder to store exception tracebacks in exception
occurrences. Let's recompile and rerun the application.
The traceback, as is, is not very useful. We now need to use another tool that is bundled
with GNAT, called addr2line. Here is an example of its use:
This time we do have a symbolic backtrace, which shows information similar to what we
got in the debugger.
For users on OSX machines, addr2line does not exist. On these machines, however, an
equivalent solution exists. You need to link your application with an additional switch, and
then use the tool atos, as in:
> rm *.o
> gnatmake -g main.adb -bargs -E -largs -Wl,-no_pie
> ./main
We will now discuss a relatively new switch of the compiler, namely -gnateE. When used,
this switch will generate extra information in exception messages.
Let's amend our test program to:
4 procedure Main is
5
13 begin
14 Nested (3);
15
16 exception
17 when E : others =>
18 Put_Line
19 (Ada.Exceptions.Exception_Information (E));
20 end Main;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exceptions_Package.Exception_
↪Information
MD5: 3590f2bf48f6ed1cf7745d576924cad4
Runtime output
When running the application, we see that the exception information (traceback) is the
same as before, but this time the exception message is set automatically by the compiler.
So we know we got a Constraint_Error because an incorrect index was used at the named
source location (main.adb, line 10). But the significant addition is the second line of the
message, which indicates exactly the cause of the error. Here, we wanted to get the ele-
ment at index 3, in an array whose range of valid indexes is from 1 to 2. (No need for a
debugger in this case.)
The column information on the first line of the exception message is also very useful when
dealing with null pointers. For instance, a line such as:
A := Rec1.Rec2.Rec3.Rec4.all;
where each of the Rec is itself a pointer, might raise Constraint_Error with a message
"access check failed". This indicates for sure that one of the pointers is null, and by using
the column information it is generally easy to find out which one it is.
We can rename exceptions by using the an exception renaming declaration in this form
Renamed_Exception : exception renames Existing_Exception;. For example:
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exception_Renaming.Exception_
↪Renaming
MD5: ff20825162ee9eef6ac8ed329da2a80f
Runtime output
Exception renaming creates a new view of the original exception. If we rename an exception
from package A in package B, that exception will become visible in package B. For example:
3 Int_E : exception;
4
5 end Internal_Exceptions;
3 package Test_Constraints is
4
8 end Test_Constraints;
6 procedure Show_Exception_Renaming_View is
7 begin
8 raise Ext_E;
9 exception
10 when E : others =>
11 Put_Line
12 (Ada.Exceptions.Exception_Information (E));
13 end Show_Exception_Renaming_View;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Exception_Renaming.Exception_
↪Renaming_View
MD5: a44e2698170c6fab79241d0f33ef8c2e
Runtime output
Here, we're renaming the Int_E exception in the Test_Constraints package. The Int_E
exception isn't directly visible in the Show_Exception_Renaming procedure because we're
not withing the Internal_Exceptions package. However, it is indirectly visible in that
procedure via the renaming (Ext_E) in the Test_Constraints package.
156 http://www.ada-auth.org/standards/22rm/html/RM-8-5-2.html
Note: This section was originally written by Robert Dewar and published as Gem #150:
Out and Uninitialized157
Perhaps surprisingly, the Ada standard indicates cases where objects passed to out and in
out parameters might not be updated when a procedure terminates due to an exception.
Let's take an example:
9 if Error then
10 raise Program_Error;
11 end if;
12 end Local;
13
14 B : Integer := 0;
15
16 begin
17 Local (B, Error => True);
18 exception
19 when Program_Error =>
20 Put_Line ("Value for B is"
21 & Integer'Image (B)); -- "0"
22 end Show_Out_Uninitialized;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Out_Uninitialized.Out_
↪Uninitialized_1
MD5: cebcf14e9fd088e38b98a5132d9fd998
Runtime output
Value for B is 0
This program outputs a value of 0 for B, whereas the code indicates that A is assigned before
raising the exception, and so the reader might expect B to also be updated.
The catch, though, is that a compiler must by default pass objects of elementary types
(scalars and access types) by copy and might choose to do so for other types (records, for
example), including when passing out and in out parameters. So what happens is that
while the formal parameter A is properly initialized, the exception is raised before the new
value of A has been copied back into B (the copy will only happen on a normal return).
3 procedure Show_Out_Uninitialized_Warnings is
4
11 B : Integer := 0;
12
13 begin
14 Local (B);
15 exception
16 when others =>
17 Put_Line ("Value for B is"
18 & Integer'Image (B));
19 end Show_Out_Uninitialized_Warnings;
MD5: 5b6960974c729ea37a70fb313d6e5084
Build output
show_out_uninitialized_warnings.adb:7:10: warning: assignment to pass-by-copy␣
↪formal may have no effect [enabled by default]
Runtime output
Value for B is 0
We now get a compilation warning that the pass-by-copy formal may have no effect.
Of course, GNAT is not able to point out all such errors (see first example above), which in
general would require full flow analysis.
The behavior is different when using parameter types that the standard mandates be
passed by reference, such as tagged types for instance. So the following code will work
as expected, updating the actual parameter despite the exception:
3 procedure Show_Out_Initialized_Rec is
4
15 V : Rec;
16
17 begin
18 V.Field := 0;
19 Local (V);
20 exception
21 when others =>
22 Put_Line ("Value of Field is"
23 & V.Field'Img); -- "1"
24 end Show_Out_Initialized_Rec;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Out_Uninitialized.Out_
↪Uninitialized_3
MD5: 370031a404657ea18ffabf3c1d507cd4
Runtime output
Value of Field is 1
9 end Exported_Procedures;
11 end Exported_Procedures;
3 with Exported_Procedures;
4 use Exported_Procedures;
5
6 procedure Show_Out_Reference is
7 B : Integer := 0;
8 begin
9 Local (B, Error => True);
10 exception
11 when Program_Error =>
12 Put_Line ("Value for B is"
13 & Integer'Image (B)); -- "1"
14 end Show_Out_Reference;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Out_Uninitialized.Out_
↪Uninitialized_4
MD5: aed2788be2b3ceeec19b28421c53fc66
Runtime output
Value for B is 1
In the case of direct assignments to global variables, the behavior in the presence of excep-
tions is somewhat different. For predefined exceptions, most notably Constraint_Error,
the optimization permissions allow some flexibility in whether a global variable is or is not
updated when an exception occurs (see Ada RM 11.6158 ). For instance, the following code
makes an incorrect assumption:
A program is not justified in assuming that X = 0 if the addition raises an exception (as-
suming X is a global here). So any such assumptions in a program are incorrect code which
should be fixed.
158 http://www.ada-auth.org/standards/22rm/html/RM-11-6.html
159 http://www.ada-auth.org/standards/22rm/html/RM-11-6.html
pragma Suppress
Note: This section was originally written by Gary Dismukes and published as Gem #63:
The Effect of Pragma Suppress160 .
One of Ada's key strengths has always been its strong typing. The language imposes strin-
gent checking of type and subtype properties to help prevent accidental violations of the
type system that are a common source of program bugs in other less-strict languages such
as C. This is done using a combination of compile-time restrictions (legality rules), that pro-
hibit mixing values of different types, together with run-time checks to catch violations of
various dynamic properties. Examples are checking values against subtype constraints and
preventing dereferences of null access values.
At the same time, Ada does provide certain "loophole" features, such as
Unchecked_Conversion, that allow selective bypassing of the normal safety features,
which is sometimes necessary when interfacing with hardware or code written in other
languages.
Ada also permits explicit suppression of the run-time checks that are there to ensure that
various properties of objects are not violated. This suppression can be done using pragma
Suppress, as well as by using a compile-time switch on most implementations — in the
case of GNAT, with the -gnatp switch.
In addition to allowing all checks to be suppressed, pragma Suppress supports suppression
of specific forms of check, such as Index_Check for array indexing, Range_Check for scalar
bounds checking, and Access_Check for dereferencing of access values. (See section 11.5
of the Ada Reference Manual for further details.)
Here's a simple example of suppressing index checks within a specific subprogram:
procedure Main is
procedure Sort_Array (A : in out Some_Array) is
pragma Suppress (Index_Check);
-- ^^^^^^^^^^^^^^^^^^^^^
-- eliminate check overhead
begin
...
end Sort_Array;
end Main;
There are various reasons why a compiler might choose not to remove a check. On some
hardware, certain checks may be essentially free, such as null pointer checks or arithmetic
overflow, and it might be impractical or add extra cost to suppress the check. Another
example where it wouldn't make sense to remove checks is for an operation implemented
by a call to a run-time routine, where the check might be only a small part of a more
expensive operation done out of line.
Furthermore, in many cases GNAT can determine at compile time that a given run-time
check is guaranteed to be violated. In such situations, it gives a warning that an exception
will be raised, and generates code specifically to raise the exception. Here's an example:
..
if A > B then
X := X + 1;
..
end if;
For the assignment incrementing X, the compiler will normally generate machine code
equivalent to:
Temp := X + 1;
if Temp > 10 then
raise Constraint_Error;
end if;
X := Temp;
If range checks are suppressed, then the compiler can just generate the increment and
assignment. However, if the compiler is able to somehow prove that X = 10 at this point,
it will issue a warning, and replace the entire assignment with simply:
raise Constraint_Error;
pragma Unsuppress
We can use pragma Unsuppress to reverse the effect of a pragma Suppress. While pragma
Suppress gives permission to the compiler to remove a specific check, pragma Unsuppress
revokes that permission.
Let's see an example:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
10 function Unchecked_Value_Of
11 (A : Integer_Array;
12 I : Integer)
13 return Integer
14 is
15 type Half_Integer_Array is new
16 Integer_Array (A'First ..
17 A'First + A'Length / 2);
18
28 function Value_Of
29 (A : Integer_Array;
30 I : Integer)
31 return Integer
32 is
33 type Half_Integer_Array is new
34 Integer_Array (A'First ..
35 A'First + A'Length / 2);
36
45 begin
46 Arr_1 (10) := Unchecked_Value_Of (Arr_1, 10);
47 Arr_1 (10) := Value_Of (Arr_1, 10);
48
49 end Show_Index_Check;
Project: Courses.Advanced_Ada.Control_Flow.Exceptions.Pragma_Unsuppress.Pragma_
↪Unsuppress
MD5: 0585b78fd57913d3172c7ab1ea6f4864
Runtime output
In this example, we first use a pragma Suppress (Index_Check), so the compiler is allowed
to remove the index check from the Unchecked_Value_Of function. (Therefore, depending
on the compiler, the call to the Unchecked_Value_Of function may complete without raising
an exception.) Of course, in this specific example, suppressing the index check masks a
severe issue.
In contrast, an index check is performed in the Value_Of function because of the pragma
Unsuppress. As a result, the index checks fails in the call to this function, which raises a
Constraint_Error exception.
161 http://www.ada-auth.org/standards/22rm/html/RM-11-5.html
TWENTYSEVEN
MODULAR PROGRAMMING
27.1 Packages
We've seen in the Introduction to Ada course that we can rename packages (page 46).
Grouping packages
A use-case that we haven't mentioned in that course is that we can apply package renaming
to group individual packages into a common hierarchy. For example:
Listing 1: driver_m1.ads
1 package Driver_M1 is
2
3 end Driver_M1;
Listing 2: driver_m2.ads
1 package Driver_M2 is
2
3 end Driver_M2;
Listing 3: drivers.ads
1 package Drivers
2 with Pure is
3
4 end Drivers;
Listing 4: drivers-m1.ads
1 with Driver_M1;
2
162 http://www.ada-auth.org/standards/22rm/html/RM-10-1-1.html
693
Learning Ada
Listing 5: drivers-m2.ads
1 with Driver_M2;
2
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Package_Renaming.Package_
↪Renaming_1
MD5: 8d6a6bec32f7ec4397de1faf9f0b44d9
Here, we're renaming the Driver_M1 and Driver_M2 packages as child packages of the
Drivers package, which is a pure package.
Important
Note that a package that is renamed as a child package cannot refer to information from its
(non-renamed) parent. In other words, Driver_M1 (renamed as Drivers.M1) cannot refer
to information from the Drivers package. For example:
Listing 6: driver_m1.ads
1 package Driver_M1 is
2
5 end Driver_M1;
Listing 7: drivers.ads
1 package Drivers is
2
3 Counter : Integer := 0;
4
5 end Drivers;
Listing 8: drivers-m1.ads
1 with Driver_M1;
2
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Package_Renaming.Package_
↪Renaming_1_Refer_To_Parent
MD5: d174746d8151d9a2cd048ad44e853850
Build output
Note that we cannot create a child package using a parent package name that was intro-
duced by a renaming. For example, let's say we want to create a child package Ext for
the Drivers.M1 package we've seen earlier. We cannot just declare a Drivers.M1.Ext
package like this:
package Drivers.M1.Ext is
end Drivers.M1.Ext;
because the parent unit cannot be a renaming. The solution is to actually extend the original
(non-renamed) package:
Listing 9: driver_m1-ext.ads
1 package Driver_M1.Ext is
2
3 end Driver_M1.Ext;
4 with Drivers.M1.Ext;
5
6 procedure Dummy is
7 begin
8 null;
9 end Dummy;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Package_Renaming.Package_
↪Renaming_1
MD5: e338d668dbd98b1a3917a8d3d948a439
This works fine because any child package of a package P is also a child package of a
renamed version of P. (Therefore, because Ext is a child package of Driver_M1, it is also a
child package of the renamed Drivers.M1 package.)
We can also use renaming to ensure backwards-compatibility when changing the package
hierarchy. For example, we could adapt the previous source-code by:
• converting Driver_M1 and Driver_M2 to child packages of Drivers, and
• using package renaming to mimic the original names (Driver_M1 and Driver_M2).
This is the adapted code:
4 end Drivers;
4 package Drivers.M1 is
5
6 end Drivers.M1;
4 package Drivers.M2 is
5
6 end Drivers.M2;
4 with Drivers.M1;
5
4 with Drivers.M2;
5
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Package_Renaming.Package_
↪Renaming_2
MD5: 27f8066b5f5954514fea51b6e9b9de81
Now, M1 and M2 are actual child packages of Drivers, but their original names are still
available. By doing so, we ensure that existing software that makes use of the original
packages doesn't break.
In this section, we discuss the concept of private packages. However, before we proceed
with the discussion, let's recapitulate some important ideas that we've seen earlier.
In the Introduction to Ada course (page 113), we've seen that encapsulation plays an im-
portant role in modular programming. By using the private part of a package specification,
we can disclose some information, but, at the same time, prevent that this information
gets accessed where it shouldn't be used directly. Similarly, we've seen that we can use
the private part of a package to distinguish between the partial and full view (page 307) of
a data type.
The main application of private packages is to create private child packages, whose purpose
We declare private packages by using the private keyword. For example, let's say we have
a package named Data_Processing:
3 -- ...
4
5 end Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_Decl
MD5: 502811212890785d90c6f891d7f8e557
We simply write private package to declare a private child package named Calculations:
3 -- ...
4
5 end Data_Processing.Calculations;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_Decl
MD5: 20df8b2ac4c9aa93f03a12afd9b7ef30
7 private
8
11 end Data_Processing;
5 end Data_Processing.Calculations;
11 end Data_Processing;
9 end Data_Processing.Calculations;
3 procedure Test_Data_Processing is
4 D : Data;
5 begin
6 Process (D);
7 end Test_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package
MD5: 3edd5f73938e809994347b5876014d0d
In this example, we refer to the private child package Calculations in the body of the
Data_Processing package — by simply writing with Data_Processing.Calculations.
After that, we can call the Calculate procedure normally in the Process procedure.
7 private
8
11 end Data_Processing;
5 end Data_Processing.Calculations;
4 private
5 package Data_Processing.Advanced_Calculations is
6
10 end Data_Processing.Advanced_Calculations;
11 end Data_Processing;
9 end Data_Processing.Calculations;
3 procedure Test_Data_Processing is
4 D : Data;
5 begin
6 Process (D);
7 end Test_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_2
MD5: 32fc76ae13f1eecdd854a029793034d8
Note that, in the body of the Data_Processing package, we're now referring to the new
Advanced_Calculations package instead of the Calculations package.
Referring to a private child package in the specification of another private child package is
OK, but we cannot do the same in the specification of a non-private package. For example,
let's change the specification of the Advanced_Calculations and make it non-private:
4 package Data_Processing.Advanced_Calculations is
5
9 end Data_Processing.Advanced_Calculations;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_2
MD5: 27fd3bdb063a11ed7797cc44fa1e8349
Build output
Now, the compilation doesn't work anymore. However, we could still refer to Calculations
packages in the body of the Advanced_Calculations package:
5 end Data_Processing.Advanced_Calculations;
13 end Data_Processing.Advanced_Calculations;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_2
MD5: 3f37c129a6994c6b71a25ad17dcb440e
This works fine as expected: we can refer to private child packages in the body of another
package — as long as both packages belong to the same package tree.
While we can use a with-clause of a private child package in the body of the
Data_Processing package, we cannot do the same outside the package tree. For example,
we cannot refer to it in the Test_Data_Processing procedure:
3 with Data_Processing.Calculations;
4 use Data_Processing.Calculations;
5
6 procedure Test_Data_Processing is
7 D : Data;
8 begin
9 Calculate (D);
10 end Test_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package
MD5: c844327995b28d60c9a79b138a0f21d2
Build output
5 end Data_Processing.Calculations.Child;
8 end Data_Processing.Calculations.Child;
3 with Data_Processing.Calculations.Child;
4 use Data_Processing.Calculations.Child;
5
6 procedure Test_Data_Processing is
7 D : Data;
8 begin
9 Calculate (D);
10 end Test_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package
MD5: 2eaf23ddbab72578246ac07424008d9d
Build output
Important
We've discussed package renaming in a previous section (page 693). We can rename a
package as a private package, too. For example:
3 end Driver_M1;
4 end Drivers;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_Renaming
MD5: c03584dc26abb108c9c04074234b9637
4 procedure Test_Driver is
5 begin
6 null;
7 end Test_Driver;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_Packages.Private_
↪Package_Renaming
MD5: 55415978604ccea4eeaeb02df13cd2f4
Build output
As expected, although we can have the Driver_M1 package in a with clause of the
Test_Driver procedure, we cannot do the same in the case of the Drivers.M1 package
because it is private.
A private with clause allows us to refer to a package in the private part of another package.
For example, if we want to refer to package P in the private part of Data, we can write
private with P:
5 end P;
3 package Data is
4
5 type T2 is private;
6
7 private
8
9 -- Information from P is
10 -- visible here
11 type T2 is new P.T;
12
13 end Data;
3 procedure Main is
4 A : T2;
5 begin
6 null;
7 end Main;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_With_Clauses.Simple_
↪Private_With_Clause
MD5: d0705add0dd7861c83822b0d35dacba4
As you can see in the example, as the information from P is available in the private part of
Data, we can derive a new type T2 based on T from P. However, we cannot do the same in
the visible part of Data:
163 http://www.ada-auth.org/standards/22rm/html/RM-10-1-1.html
3 package Data is
4
10 end Data;
MD5: b454e875f73432f5632a20ab40ae7da6
Build output
data.ads:8:19: error: "P" is not visible
data.ads:8:19: error: non-visible declaration at p.ads:1
gprbuild: *** compilation phase failed
Also, the information from P is available in the package body. For example, let's declare a
Process procedure in the P package and use it in the body of the Data package:
7 end P;
3 package Data is
4
5 type T2 is private;
6
9 private
10
11 -- Information from P is
12 -- visible here
13 type T2 is new P.T;
14
15 end Data;
8 end Data;
3 procedure Main is
4 A : T2;
5 begin
6 null;
7 end Main;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_With_Clauses.Simple_
↪Private_With_Clause
MD5: cecc09f95bd43dd7fd34a9e289bd2674
In the body of the Data, we can access information from the P package — as we do in the
P.Process (P.T (A)) statement of the Process procedure.
There's one case where using a private with clause is the only way to refer to a package:
when we want to refer to a private child package in another child package. For example,
here we have a package P and its two child packages: Private_Child and Public_Child:
3 end P;
5 end P.Private_Child;
3 package P.Public_Child is
4
5 type T2 is private;
6
7 private
8
11 end P.Public_Child;
3 procedure Test_Parent_Child is
4 A : T2;
5 begin
6 null;
7 end Test_Parent_Child;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_With_Clauses.Private_
↪With_Clause
MD5: a6028416a957184be55a54f96a319e61
3 package P.Public_Child is
4
5 type T2 is private;
6
7 private
8
11 end P.Public_Child;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Private_With_Clauses.Private_
↪With_Clause
MD5: 2f32f29ecb4ae13bb4487c94d3bf18d9
Build output
This gives an error because the information from the P.Private_Child, being a private
child package, cannot be accessed in the public part of another child package. In summary,
unless both packages are private packages, it's only possible to access the information from
a private package in the private part of a non-private child package.
164 http://www.ada-auth.org/standards/22rm/html/RM-10-1-2.html
Sometimes, we might face the situation where two packages depend on information from
each other. Let's consider a package A that depends on a package B, and vice-versa:
3 package A is
4
5 type T1 is record
6 Value : T2;
7 end record;
8
9 end A;
3 package B is
4
5 type T2 is record
6 Value : T1;
7 end record;
8
9 end B;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Limited_Visibility.Circular_
↪Dependency
MD5: ae64f33706f1c58603aff2c33b02c910
Build output
Here, we have two mutually dependent types (page 418) T1 and T2, which are declared
in two packages A and B that refer to each other. These with clauses constitute a circular
dependency, so the compiler cannot compile either of those packages.
One way to solve this problem is by transforming this circular dependency into a partial
dependency. We do this by limiting the visibility — using a limited with clause. To use a
limited with clause for a package P, we simply write limited with P.
If a package A has limited visibility to a package B, then all types from package B are visible
as if they had been declared as incomplete types (page 305). For the specific case of
the previous source-code example, this would be the limited visibility to package B from
package A's perspective:
package B is
-- Incomplete type
type T2;
end B;
3 package A is
4
5 type T1 is record
6 Ref : access B.T2;
7 end record;
8
9 end A;
3 package B is
4
5 type T2 is record
6 Value : T1;
7 end record;
8
9 end B;
MD5: 48591850665085a6fbb184f51b658a1b
3 package A is
4
5 type T1 is record
6 Ref : access B.T2;
7 end record;
8
9 end A;
3 package B is
(continues on next page)
5 type T2 is record
6 Ref : access A.T1;
7 end record;
8
9 end B;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Limited_Visibility.Limited_
↪Visibility_2
MD5: 3884086e89400245346acfbbf0691906
We can limit the visibility and use private with clauses (page 704) at the same time. For a
package P, we do this by simply writing limited private with P.
Let's reuse the previous source-code example and convert types T1 and T2 to private types:
3 package A is
4
5 type T1 is private;
6
7 private
8
12 type T1 is record
13 Ref : access B.T2;
14 end record;
15
16 end A;
3 package B is
4
5 type T2 is private;
6
7 private
8
9 use A;
(continues on next page)
165 http://www.ada-auth.org/standards/22rm/html/RM-10-1-2.html
14 type T2 is record
15 Value : T1;
16 end record;
17
18 end B;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Limited_Visibility.Limited_
↪Private_Visibility
MD5: b3ac546e2f55fb91229e834ca7a9783d
In this updated version of the source-code example, we have not only limited visibility to
package B, but also, each package is just visible in the private part of the other package.
It's important to mention that the limited visibility we've been discussing so far is restricted
to type declarations — which are seen as incomplete types. In fact, when we use a limited
with clause, all other declarations have no visibility at all! For example, let's say we have
a package Info that declares a constant Zero_Const and a function Zero_Func:
5 Zero_Const : constant := 0;
6
7 end Info;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Limited_Visibility.Limited_
↪Private_Visibility_Other_Elements
MD5: e9b01b4d59db5982532634f9162518ce
Also, let's say we want to use the information (from package Info) in package A. If we have
limited visibility to package Info, however, this information won't be visible. For example:
3 package A is
4
5 type T1 is private;
6
7 private
8
9 type T1 is record
10 V : Integer := Info.Zero_Const;
11 W : Integer := Info.Zero_Func;
12 end record;
(continues on next page)
14 end A;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Limited_Visibility.Limited_
↪Private_Visibility_Other_Elements
MD5: 61ecb5dc2617eecac62a05d7d2c6c0df
Build output
27.1.5 Visibility
In the previous sections, we already discussed visibility from various angles. However, it
can be interesting to recapitulate this information with the help of diagrams that illustrate
the different parts of a package and its relation with other units.
Automatic visibility
First, let's consider we have a package A, its children (A.G and A.H), and the grandchild
A.G.T. As we've seen before, information of a parent package is automatically visible in its
children. The following diagrams illustrates this:
Because of this automatic visibility, many with clauses would be redundant in child pack-
ages. For example, we don't have to write with A; package A.G is, since the specification
of package A is already visible in its child packages.
If we focus on package A.G (highlighted in the figure above), we see that it only has auto-
matic visibility to its parent A, but not its child A.G.T. Also, it doesn't have visibility to its
sibling A.H.
In the rest of this section, we discuss all the situations where using with clauses is necessary
to access the information of a package. Let's consider this example where we refer to a
package B in the specification of a package A (using with B):
As we already know, the information from the public part of package B is visible in the public
part of package A. In addition to that, it's also visible in the private part and in the body of
package A. This is indicated by the dotted green arrows in the figure above.
Now, let's see the case where we refer to package B in the private part of package A (using
private with B):
Here, the information is visible in the private part of package A, as well as in its body. Finally,
let's see the case where we refer to package B in the body of package A:
Circular dependency
Let's return to package A and its descendants. As we've seen in previous sections, we
cannot refer to a child package in the specification of its parent package because that
would constitute circular dependency. (For example, we cannot write with A.G; package
A is.) This situation — which causes a compilation error — is indicated by the red arrows
in the figure below:
Note that referring to the child package A.G in the body of its parent is perfectly fine.
Private packages
The previous examples of this section only showed public packages. As we've seen before,
we cannot refer to private packages outside of a package hierarchy, as we can see in the
following example where we try to refer to package A and its descendants in the Test
procedure:
As indicated by the red arrows, we cannot refer to the private child packages of A in the
Test procedure, only the public child packages. Within the package hierarchy itself, we
cannot refer to the private package A.G in public sibling packages. For example:
Here, we cannot refer to the private package A.G in the public package A.H — as indi-
cated by the red arrow. However, we can refer to the private package A.G in other private
packages, such as A.I — as indicated by the green arrows.
Back in the Introduction to Ada course (page 37), we saw that use clauses provide direct
visibility — in the scope where they're used — to the content of a package's visible part.
For example, consider this simple procedure:
3 procedure Display_Message is
4 begin
5 Ada.Text_IO.Put_Line ("Hello World!");
6 end Display_Message;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Type_Clause.No_Use_Clause
MD5: 4c6ff19809c13ebd2fdfda482914e5f8
Runtime output
Hello World!
By adding use Ada.Text_IO to this code, we make the visible part of the Ada.Text_IO
package directly visible in the scope of the Display_Message procedure, so we can now
just write Put_Line instead of Ada.Text_IO.Put_Line:
3 procedure Display_Message is
4 begin
5 Put_Line ("Hello World!");
6 end Display_Message;
Runtime output
Hello World!
In this section, we discuss another example of use clauses. In addition, we introduce two
specific forms of use clauses: use type and use all type.
Let's now consider a simple package called Points, which contains the declaration of the
Point type and two primitive: an Init function and an addition operator.
10 private
11
22 end Points;
3 procedure Show_Point is
4 P : Point;
5 begin
6 P := Init;
7 P := P + 1;
8 end Show_Point;
Here, we have a use clause, so we have direct visibility to the content of Points's visible
part.
In certain situations, however, we might want to avoid the use clause. If that's the case,
we can rewrite the previous implementation by removing the use clause and specifying the
Points package in the prefixed form:
3 procedure Show_Point is
4 P : Points.Point;
5 begin
6 P := Points.Init;
7 P := Points."+" (P, 1);
8 end Show_Point;
Although this code is correct, it might be difficult to read, as we have to specify the package
whenever we're referring to a type or a subprogram from that package. Even worse: we
now have to write operators in the prefixed form — such as Points."+" (P, 1).
use type
3 procedure Show_Point is
4 use type Points.Point;
5
6 P : Points.Point;
7 begin
8 P := Points.Init;
9 P := P + 1;
10 end Show_Point;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Type_Clause.Use_Type_Clause
MD5: a9527276c27a67be8b5a59efcf6e5cfd
Note that use type just gives us direct visibility to the operators of a certain type, but not
other primitives. For this reason, we still have to write Points.Init in the code example.
If we want to have direct visibility to all primitives of a certain type (and not just its opera-
tors), we need to write a use clause in the form use all type. This allows us to simplify
the previous example even further:
3 procedure Show_Point is
4 use all type Points.Point;
5
6 P : Points.Point;
7 begin
8 P := Init;
9 P := P + 1;
10 end Show_Point;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Type_Clause.Use_Type_Clause
MD5: 4a8f6edd4e1811c4e8acb24393690282
Now, we've removed the prefix from all operations on the P variable.
Visibility issues may arise when we have multiple use clauses. For instance, we might
have types with the same name declared in multiple packages. This constitutes a naming
conflict; in this case, the types become hidden — so they're not directly visible anymore,
even if we have a use clause.
Code example
Let's start with a code example. First, we declare and implement a generic procedure that
shows the value of a Complex object:
3 generic
4 with package Complex_Types is new
5 Ada.Numerics.Generic_Complex_Types (<>);
6 procedure Show_Any_Complex
7 (Msg : String;
8 Val : Complex_Types.Complex);
4 procedure Show_Any_Complex
5 (Msg : String;
(continues on next page)
167 http://www.ada-auth.org/standards/22rm/html/RM-8-4.html
12 use Ada.Text_IO;
13 begin
14 Put (Msg & " ");
15 Put (Val);
16 New_Line;
17 end Show_Any_Complex;
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Clause_Naming_Conflicts.
↪Use_Type_Clause_Complex_Types
MD5: 2527291906d3a600eecd6d36e4359c1a
3 with Ada.Numerics.Generic_Complex_Types;
4
5 with Show_Any_Complex;
6
7 procedure Show_Use is
8 package Complex_Float_Types is new
9 Ada.Numerics.Generic_Complex_Types
10 (Real => Float);
11 use Complex_Float_Types;
12
16 C, D, X : Complex;
17 begin
18 C := Compose_From_Polar (3.0, Pi / 2.0);
19 D := Compose_From_Polar (5.0, Pi / 2.0);
20 X := C + D;
21
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Clause_Naming_Conflicts.
↪Use_Type_Clause_Complex_Types
MD5: cc2a612c9884539f33154680854a4c82
Runtime output
C: (-1.31134E-07, 3.00000E+00)
D: (-2.18557E-07, 5.00000E+00)
X: (-3.49691E-07, 8.00000E+00)
In this example, we declare variables of the Complex type, initialize them and use them in
operations. Note that we have direct visibility to the package instance because we've added
a simple use clause after the package instantiation — see use Complex_Float_Types in
the example.
Naming conflict
3 with Ada.Numerics.Generic_Complex_Types;
4
5 with Show_Any_Complex;
6
7 procedure Show_Use is
8 package Complex_Float_Types is new
9 Ada.Numerics.Generic_Complex_Types
10 (Real => Float);
11 use Complex_Float_Types;
12
21 C, D, X : Complex;
22 -- ^ ERROR: type is hidden!
23 begin
24 C := Compose_From_Polar (3.0, Pi / 2.0);
25 D := Compose_From_Polar (5.0, Pi / 2.0);
26 X := C + D;
27
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Clause_Naming_Conflicts.
↪Use_Type_Clause_Complex_Types
MD5: 30b562e2f81ae62912ec4e067150d5cd
Build output
This example doesn't compile because we have direct visibility to both Com-
As we know, a simple fix for this compilation error is to add the package prefix in the variable
declaration:
3 with Ada.Numerics.Generic_Complex_Types;
4
5 with Show_Any_Complex;
6
7 procedure Show_Use is
8 package Complex_Float_Types is new
9 Ada.Numerics.Generic_Complex_Types
10 (Real => Float);
11 use Complex_Float_Types;
12
21 C, D, X : Complex_Float_Types.Complex;
22 -- ^ SOLVED: package is now specified.
23 begin
24 C := Compose_From_Polar (3.0, Pi / 2.0);
25 D := Compose_From_Polar (5.0, Pi / 2.0);
26 X := C + D;
27
MD5: 0b3285364ea0188a678db2fc406741b8
Runtime output
C: (-1.31134E-07, 3.00000E+00)
D: (-2.18557E-07, 5.00000E+00)
X: (-3.49691E-07, 8.00000E+00)
Another possibility is to write a use clause in the form use all type:
5 with Show_Any_Complex;
6
7 procedure Show_Use is
8 package Complex_Float_Types is new
9 Ada.Numerics.Generic_Complex_Types
10 (Real => Float);
11 use all type Complex_Float_Types.Complex;
12
21 C, D, X : Complex_Float_Types.Complex;
22 begin
23 C := Compose_From_Polar (3.0, Pi / 2.0);
24 D := Compose_From_Polar (5.0, Pi / 2.0);
25 X := C + D;
26
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Clause_Naming_Conflicts.
↪Use_Type_Clause_Complex_Types
MD5: 90333ff41e25afb1399f7f94f7e2b566
Runtime output
C: (-1.31134E-07, 3.00000E+00)
D: (-2.18557E-07, 5.00000E+00)
X: (-3.49691E-07, 8.00000E+00)
For the sake of completeness, let's declare and use variables of both Complex types:
3 with Ada.Numerics.Generic_Complex_Types;
4
5 with Show_Any_Complex;
6
7 procedure Show_Use is
8 package Complex_Float_Types is new
9 Ada.Numerics.Generic_Complex_Types
10 (Real => Float);
11 use all type Complex_Float_Types.Complex;
12
24 C, D, X : Complex_Float_Types.Complex;
25 E, F, Y : Complex_Long_Float_Types.Complex;
26 begin
27 C := Compose_From_Polar (3.0, Pi / 2.0);
28 D := Compose_From_Polar (5.0, Pi / 2.0);
29 X := C + D;
30
Project: Courses.Advanced_Ada.Modular_Prog.Packages.Use_Clause_Naming_Conflicts.
↪Use_Type_Clause_Complex_Types
MD5: 48f31250116f107d3143703debb3107d
Runtime output
C: (-1.31134E-07, 3.00000E+00)
D: (-2.18557E-07, 5.00000E+00)
X: (-3.49691E-07, 8.00000E+00)
E: ( 1.83697019872103E-16, 3.00000000000000E+00)
F: ( 3.06161699786838E-16, 5.00000000000000E+00)
Y: ( 4.89858719658941E-16, 8.00000000000000E+00)
We've seen previously (page 696) that we can declare private packages. Because packages
and subprograms can both be library units, we can declare private subprograms as well.
We do this by using the private keyword. For example:
Project: Courses.Advanced_Ada.Modular_Prog.Subprograms_Modularity.Private_
↪Subprograms.Private_Test_Procedure
MD5: 2ea1770a5fd5dee40f015b9d33d2f309
Such a subprogram as the one above isn't really useful. For example, we cannot write a
with clause that refers to the Test procedure, as it's not visible anywhere:
3 procedure Show_Test is
4 begin
5 Test;
6 end Show_Test;
Project: Courses.Advanced_Ada.Modular_Prog.Subprograms_Modularity.Private_
↪Subprograms.Private_Test_Procedure
MD5: 0702378a034f65a69a4c5b5258f7b32e
Build output
As expected, since Test is private, we get a compilation error because this procedure can-
not be referenced in the Show_Test procedure.
168 http://www.ada-auth.org/standards/22rm/html/RM-10-1-1.html
169 http://www.ada-auth.org/standards/22rm/html/RM-10-1-2.html
7 private
8
13 end Data_Processing;
10 end Data_Processing;
3 procedure Test_Data_Processing is
4 D : Data;
5 begin
6 Process (D);
7 end Test_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Subprograms_Modularity.Private_
↪Subprograms.Private_Package_Procedure
MD5: 0f6af1b02f37e011abac5b2a6dfc482d
We can also use private subprograms to test private packages. As we know, in most cases,
we cannot access private packages in external clients — such as external subprograms.
However, by declaring a subprogram private, we're allowed to access private packages.
This can be very useful to create applications that we can use to test private packages.
(Note that these applications must be library-level parameterless subprograms, because
only those can be main programs.)
Let's see an example:
7 private
8
13 end Private_Data_Processing;
8 end Private_Data_Processing;
4 procedure Test_Private_Data_Processing is
5 D : Data;
6 begin
7 Process (D);
8 end Test_Private_Data_Processing;
MD5: 3527e54f99eb2cb52317c987b499caaf
We could also implement the Test subprogram that we use to test a private package P as
a child subprogram of that package. In other words, we could write a procedure P.Test
and use it as our main application. The advantage here is that this allows us to access the
private part of the parent package P in the test procedure.
Let's rewrite the Test_Private_Data_Processing procedure from the previous example
as the child procedure Private_Data_Processing.Test:
7 private
8
13 end Private_Data_Processing;
8 end Private_Data_Processing;
Project: Courses.Advanced_Ada.Modular_Prog.Subprograms_Modularity.Private_
↪Subprograms.Private_Package_Child_Subprogram
MD5: 0726f5890a5b3847244d1ae08989e158
In this code example, we now implement the Test procedure as a child of the Pri-
vate_Data_Processing package. In this procedure, we're able to initialize the private com-
ponent F of the D object. As we know, this initialization of a private component wouldn't be
possible if Test wasn't a child procedure. (For instance, writing such an initialization in the
Test_Private_Data_Processing procedure from the previous code example would trigger
a compilation error.)
TWENTYEIGHT
RESOURCE MANAGEMENT
We discussed access types back in the Introduction to Ada course (page 95). In this chapter,
we discuss further details about access types and techniques when using them. Before we
dig into details, however, we're going to make sure we understand the terminology.
In this section, we discuss some of the terminology associated with access types. Usu-
ally, the terms used in Ada when discussing references and dynamic memory allocation
are different than the ones you might encounter in other languages, so it's necessary you
understand what each term means.
The first term we encounter is (obviously) access type, which is a type that provides us
access to an object or a subprogram. We declare access types by using the access keyword:
Listing 1: show_access_type_declaration.ads
1 package Show_Access_Type_Declaration is
2
3 --
4 -- Declaring access types:
5 --
6
7 -- Access-to-object type
8 type Integer_Access is access Integer;
9
10 -- Access-to-subprogram type
11 type Init_Integer_Access is access
12 function return Integer;
13
14 end Show_Access_Type_Declaration;
MD5: 64e4e0847a73a9ed23e29e09798934de
Here, we're declaring two access types: the access-to-object type Integer_Access and
the access-to-subprogram type Init_Integer_Access. (We discuss access-to-subprogram
types later on (page 820)).
735
Learning Ada
In the declaration of an access type, we always specify — after the access keyword — the
kind of thing we want to designate. In the case of an access-to-object type declaration,
we declare a subtype we want to access, which is known as the designated subtype of
an access type. In the case of an access-to-subprogram type declaration, the subprogram
prototype is known as the designated profile.
In our previous code example, Integer is the designated subtype of the Integer_Access
type, and function return Integer is the designated profile of the Init_Integer_Access
type.
Important
In contrast to other programming languages, an access type is not a pointer, and it doesn't
just indicate an address in memory. We discuss more about addresses (page 849) later on.
Listing 2: show_simple_allocation.adb
1 procedure Show_Simple_Allocation is
2
3 -- Access-to-object type
4 type Integer_Access is access Integer;
5
6 -- Access object
7 I1 : Integer_Access;
8
9 begin
10 I1 := new Integer;
11 -- ^^^^^^^^^^^ allocating an object,
12 -- which becomes the designated
13 -- object for I1
14
15 end Show_Simple_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Terminology.Simple_
↪Allocation
MD5: 32ca8cf523e19b25dabb55da6df1f18d
In this example, I1 is an access object and the object allocated via new Integer is its
designated object.
An access object and a designated (allocated) object, both store values. The value of an
access object is the access value and the value of a designated object is the designated
value. For example:
Listing 3: show_values.adb
1 procedure Show_Values is
2
3 -- Access-to-object type
4 type Integer_Access is access Integer;
5
8 begin
9 I1 := new Integer;
10 I3 := new Integer;
11
18 end Show_Values;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Terminology.Values
MD5: a152ee813b8ed9fad985cf4e2c25d847
In this example, the assignment I2 := I1 copies the access value of I1 to I2. The assign-
ment I3.all := I1.all copies I1's designated value to I3's designated object. (As we
already know, .all is used to dereference an access object. We discuss this topic again
later in this chapter (page 766).)
Ada makes the distinction between pool-specific and general access types, as we'll discuss
in this section. Before doing so, however, let's talk about memory allocation.
In general terms, memory can be allocated dynamically on the heap or statically on the
stack. (Strictly speaking, both are dynamic allocations, in that they occur at run-time with
amounts not previously specified.) For example:
Listing 4: show_simple_allocation.adb
1 procedure Show_Simple_Allocation is
2
9 begin
10 -- Allocating an Integer object on the heap
11 A1 := new Integer;
12
13 declare
14 -- Allocating an Integer object on the
15 -- stack
16 I : Integer;
17 begin
18 null;
19 end;
20
21 end Show_Simple_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Types_
↪Allocation.Simple_Allocation
MD5: 4144feb99e6e0b1a0749fce0b20370a1
Build output
When we allocate an object on the heap via new, the allocation happens in a memory pool
that is associated with the access type. In our code example, there's a memory pool associ-
ated with the Integer_Access type, and each new Integer allocates a new integer object
in that pool. Therefore, access types of this kind are called pool-specific access types. (We
discuss more about these types (page 740) later.)
It is also possible to access objects that were allocated on the stack. To do that, however,
we cannot use pool-specific access types because — as the name suggests — they're only
allowed to access objects that were allocated in the specific pool associated with the type.
Instead, we have to use general access types in this case:
Listing 5: show_general_access_type.adb
1 procedure Show_General_Access_Type is
2
13 begin
14 -- Getting access to an Integer object that
15 -- was allocated on the stack
16 A1 := I'Access;
17
18 end Show_General_Access_Type;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Types_
↪Allocation.General_Access_Types
MD5: f166291ad1975396131775d0aff6ad9d
In this example, we declare the general access type Integer_Access and the access object
A1. To initialize A1, we write I'Access to get access to an integer object I that was allocated
on the stack. (For the moment, don't worry much about these details: we'll talk about
general access types again when we introduce the topic of aliased objects (page 778) later
on.)
Listing 6: show_simple_allocation.adb
1 procedure Show_Simple_Allocation is
2
9 begin
10 --
11 -- Allocating an Integer object on the heap
12 -- and initializing an access object of
13 -- the general access type Integer_Access.
14 --
15 A1 := new Integer;
16
17 end Show_Simple_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Types_
↪Allocation.General_Access_Types_Heap
MD5: 3fa5efeac2f66794f066ab29f26bf7ca
Here, we're using a general access type Integer_Access, but allocating an integer object
on the heap.
Important
In many code examples, we have used the Integer type as the designated subtype of the
access types — by writing access Integer. Although we have used this specific scalar
type, we aren't really limited to those types. In fact, we can use any type as the designated
subtype, including user-defined types, composite types, task types and protected types.
171 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
We've already discussed many aspects about pool-specific access types. In this section, we
recapitulate some of those aspects, and discuss some new details that haven't seen yet.
As we know, we cannot directly assign an object Distance_Miles of type Miles to an
object Distance_Meters of type Meters, even if both share a common Float type an-
cestor. The assignment is only possible if we perform a type conversion from Miles
to Meters, or vice-versa — e.g.: Distance_Meters := Meters (Distance_Miles) *
Miles_To_Meters_Factor.
Similarly, in the case of pool-specific access types, a direct assignment between objects of
different access types isn't possible. However, even if both access types have the same
designated subtype (let's say, they are both declared using is access Integer), it's still
not possible to perform a type conversion between those access types. The only situation
when an access type conversion is allowed is when both types have a common ancestor.
Let's see an example:
Listing 7: show_simple_allocation.adb
1 pragma Ada_2022;
2
5 procedure Show_Simple_Allocation is
6
17 begin
18 A1 := new Integer;
19 Put_Line ("A1 : " & A1'Image);
20 Put_Line ("Pool: " & A1'Storage_Pool'Image);
21
22 A2 := new Integer;
23 Put_Line ("A2: " & A2'Image);
24 Put_Line ("Pool: " & A2'Storage_Pool'Image);
25
38 end Show_Simple_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Types_
↪Allocation.Pool_Specific_Access_Types
MD5: 8984cb9cb9083f09b9b4096da812f805
Runtime output
A1 : (access 10902a0)
Pool: SYSTEM.POOL_GLOBAL.UNBOUNDED_NO_RECLAIM_POOL'{SYSTEM.STORAGE_POOLS.TROOT_
↪STORAGE_POOLC object}
Important
Remember that:
• As mentioned in the Introduction to Ada course (page 97):
– an access type can be unconstrained, but the actual object allocation must be
constrained;
– we can use a qualified expression (page 332) to allocate an object.
• We can use the Storage_Size attribute to limit the size of the memory pool associ-
ated with an access type, as discussed previously in the section about storage size
(page 353).
• When running out of memory while allocating via new, we get a Storage_Error ex-
ception because of the storage check (page 673).
For example:
Listing 8: show_array_allocation.adb
1 pragma Ada_2022;
2
5 procedure Show_Array_Allocation is
6
21 procedure Show_Info
22 (IAA : Integer_Array_Access) is
23 begin
24 Put_Line ("Allocated: " & IAA'Image);
25 Put_Line ("Length: "
26 & IAA.all'Length'Image);
27 Put_Line ("Values: "
28 & IAA.all'Image);
29 end Show_Info;
30
31 begin
32 -- Allocating an integer array with
33 -- constrained range on the heap:
34 A1 := new Integer_Array (1 .. 3);
35 A1.all := [others => 42];
36 Show_Info (A1);
37
49 exception
50 when Storage_Error =>
51 Put_Line ("Out of memory!");
52
53 end Show_Array_Allocation;
Multiple allocation
Up to now, we have seen examples of allocating a single object on the heap. It's possible
to allocate multiple objects at once as well — i.e. syntactic sugar is available to simplify
the code that performs this allocation. For example:
Listing 9: show_access_array_allocation.adb
1 pragma Ada_2022;
2
5 procedure Show_Access_Array_Allocation is
6
9 type Integer_Access_Array is
10 array (Positive range <>) of Integer_Access;
11
15 begin
16 --
17 -- Allocating 10 access objects and
18 -- initializing the corresponding designated
19 -- object with zero:
20 --
21 Arr := (others => new Integer'(0));
22
23 -- Same as:
24 for I in Arr'Range loop
25 Arr (I) := new Integer'(0);
26 end loop;
27
35 end Show_Access_Array_Allocation;
MD5: c32af182dc35879d76df989a689ee35c
Runtime output
Arr:
[(access 13053e0), (access 1305400), (access 1305420), (access 1305440),
(access 1305460), (access 1305480), (access 13054a0), (access 13054c0),
(access 13054e0), (access 1305500)]
Arr (designated values):
0 0 0 0 0 0 0 0 0 0
In this example, we have the access type Integer_Access and an array type of this access
type (Integer_Access_Array). We also declare an array Arr of Integer_Access_Array
type. This means that each component of Arr is an access object. We allocate all ten com-
ponents of the Arr array by simply writing Arr := (others => new Integer). This array
aggregate (page 450) is syntactic sugar for a loop over Arr that allocates each component.
(Note that, by writing Arr := (others => new Integer'(0)), we're also initializing the
designated objects with zero.)
Let's see another code example, this time with task types:
10 type Worker_Array is
11 array (Positive range <>) of Worker_Access;
(continues on next page)
13 end Workers;
14 accept Stop;
15
20 end Workers;
5 procedure Show_Workers is
6 Worker_Arr : Worker_Array (1 .. 20);
7 begin
8 --
9 -- Allocating 20 workers at once:
10 --
11 Worker_Arr := (others => new Worker);
12
24 end Show_Workers;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Types_
↪Allocation.Workers
MD5: d29e3d56585f8d9a63b805c680e5dc54
Runtime output
Started Worker # 1
Started Worker # 4
Started Worker # 2
Started Worker # 3
Started Worker # 5
Started Worker # 6
Started Worker # 7
Started Worker # 8
Started Worker # 9
Started Worker # 10
Started Worker # 11
Started Worker # 12
Started Worker # 13
Started Worker # 14
Started Worker # 15
Started Worker # 16
Started Worker # 17
Started Worker # 18
Started Worker # 19
Started Worker # 20
Some processing...
Stopped Worker # 1
Stopped Worker # 18
Stopped Worker # 17
Stopped Worker # 10
Stopped Worker # 15
Stopped Worker # 4
Stopped Worker # 16
Stopped Worker # 14
Stopped Worker # 6
Stopped Worker # 5
Stopped Worker # 2
Stopped Worker # 13
Stopped Worker # 12
Stopped Worker # 3
Stopped Worker # 9
Stopped Worker # 7
Stopped Worker # 8
Stopped Worker # 19
Stopped Worker # 11
Stopped Worker # 20
In this example, we declare the task type Worker, the access type Worker_Access and
an array of access to tasks Worker_Array. Using this approach, a task is only created
when we allocate an individual component of an array of Worker_Array type. Thus, when
we declare the Worker_Arr array in this example, we're only preparing a container of 20
workers, but we don't have any actual tasks yet. We bring the 20 tasks into existence by
writing Worker_Arr := (others => new Worker).
We can use access types when declaring discriminants. Let's see an example:
10 I : Integer := IA.all;
11 -- ^^^^^^^^^
12 -- Setting I's default to use the
13 -- designated value of IA:
14 end record;
15
18 end Custom_Recs;
13 end Custom_Recs;
3 procedure Show_Discriminants_As_Access_Values is
4
5 IA : constant Integer_Access :=
6 new Integer'(10);
7 R : Rec (IA);
8
9 begin
10 Show (R);
11
12 IA.all := 20;
13 R.I := 30;
14 Show (R);
15
22 end Show_Discriminants_As_Access_Values;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Discriminants_As_
↪Access_Values.Discriminant_Access_Values
MD5: c7850acefd8e5227f4be654faed13055
Runtime output
R.IA = 10
R.I = 10
R.IA = 20
R.I = 30
In the Custom_Recs package from this example, we declare the access type Inte-
ger_Access. We then use this type to declare the discriminant (IA) of the Rec type. In the
Show_Discriminants_As_Access_Values procedure, we see that (as expected) we cannot
change the discriminant of an object of Rec type: an assignment such as R.IA := new
Integer would trigger a compilation error.
Note that we can use a default for the discriminant:
15 end Custom_Recs;
3 procedure Show_Discriminants_As_Access_Values is
4
5 R1 : Rec;
6 -- ^^^
7 -- no discriminant: use default
8
13 begin
14 Show (R1);
15 Show (R2);
16 end Show_Discriminants_As_Access_Values;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Discriminants_As_
↪Access_Values.Discriminant_Access_Values
MD5: 968cb88ed7e9e6958ab66fb6f5a7ce2d
Runtime output
R.IA = 0
R.I = 0
(continues on next page)
Here, we've changed the declaration of the Rec type to allocate an integer object if the
type's discriminant isn't provided — we can see this in the declaration of the R1 object
in the Show_Discriminants_As_Access_Values procedure. Also, in this procedure, we're
allocating an unnamed integer object in the declaration of R2.
Notice that we were using a scalar type as the designated subtype of the Integer_Access
type. We could have used an unconstrained type as well. In fact, this is often used for the
sake of having the effect of an unconstrained discriminant type.
Let's see an example:
15 end Persons;
13 end Persons;
172 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
173 http://www.ada-auth.org/standards/22rm/html/RM-3-7-1.html
3 procedure Show_Person is
4 P : Person (new String'("John"));
5 begin
6 P.Age := 30;
7 Show (P);
8 end Show_Person;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Discriminants_As_
↪Access_Values.Persons
MD5: 9b1109d076b6f06632c8685a41616210
Runtime output
Name = John
Age = 30
In this example, the discriminant of the Person type has an unconstrained designated type.
In the Show_Person procedure, we declare the P object and specify the constraints of the
allocated string object — in this case, a four-character string initialized with the name "John".
9 end Persons;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Discriminants_As_
↪Access_Values.Persons_Error
MD5: 4144852aaf95da62bc4781b1e8dc2717
Build output
As expected, compilation fails for this code because the discriminant of the Person type is
indefinite.
However, the advantage of discriminants as access values isn't restricted to being able to
use unconstrained types such as arrays: we could really use any type as the designated
subtype! In fact, we can generalized this to:
3 package Custom_Recs is
4
5 type Incomp;
6 -- Incomplete type declaration!
7
27 private
28 -- Finally, we have the full view of the
29 -- Incomp type:
30 type Incomp (B : Boolean := True) is
31 null record;
32
33 end Custom_Recs;
3 procedure Show_Rec is
4 R : Rec (new Incomp);
5 begin
6 Show (R);
7 end Show_Rec;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Discriminants_As_
↪Access_Values.Generic_Access
MD5: c65510e8c6a7625cbd08aa9e68f05115
In the Gen_Custom_Recs package, we're using type T (<>) — which can be any type —
for the designated subtype of the access type T_Access, which is the type of T_Rec's dis-
criminant. In the Custom_Recs package, we use the incomplete type Incomp to instantiate
the generic package. Only after the instantiation, we declare the complete type.
Later on, we'll discuss discriminants again when we look into anonymous access discrimi-
nants (page 868), which provide some advantages in terms of accessibility rules (page 788).
3 procedure Show_Person is
4 S1 : String_Access := new String'("John");
5 S2 : String_Access := new String'("Mark");
6 P : Person := (Name => S1,
7 Age => 30);
8 begin
9 P := (Name => S1, Age => 31);
10 -- ^^ OK: we didn't change the
11 -- discriminant.
12 Show (P);
13
MD5: 96f4742365eb6a07c377a5dec28b5767
Runtime output
Name = John
Age = 31
Name = John
Age = 32
Name = Mark
Age = 32
The first and the second assignments to P are OK because we didn't change the discrim-
inant. However, the last assignment raises the Constraint_Error exception at runtime
because we're changing the discriminant.
In addition to using discriminants as access values (page 745), we can use access types
for subprogram formal parameters. For example, the N parameter of the Show procedure
below has an access type:
7 end Names;
MD5: 82ce94987dce9026aed54a0deb3cc548
7 end Names;
10 end Names;
3 procedure Show_Names is
4 N : Name := new String'("John");
5 begin
6 Show (N);
7 end Show_Names;
MD5: 526baf1996b4a2970c3fa2e3485dcbad
Runtime output
Name: John
Note that in this example, the Show procedure is basically just displaying the string. Since
the procedure isn't doing anything that justifies the need for an access type, we could have
implemented it with a simpler type:
7 end Names;
10 end Names;
3 procedure Show_Names is
(continues on next page)
MD5: 097ec1ff781fda9deed1de23cae39ae5
Runtime output
Name: John
It's important to highlight the difference between passing an access value to a subprogram
and passing an object by reference. In both versions of this code example, the compiler will
make use of a reference for the actual parameter of the N parameter of the Show procedure.
However, the difference between these two cases is that:
• N : Name is a reference to an object (because it's an access value) that is passed by
value, and
• N : String is an object passed by reference.
Since the Name type gives us access to an object in the Show procedure, we could actually
change this object inside the procedure. To illustrate this, let's change the Show procedure
to lower each character of the string before displaying it (and rename the procedure to
Lower_And_Show):
7 end Names;
3 with Ada.Characters.Handling;
4 use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Changed_Names is
4 N : Name := new String'("John");
5 begin
6 Lower_And_Show (N);
7 end Show_Changed_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Changed_Names
MD5: 063a507284f5e7ffa669db2c8fdd3d6f
Runtime output
Name: john
Notice that, again, we could have implemented the Lower_And_Show procedure without
using an access type:
7 end Names;
3 with Ada.Characters.Handling;
4 use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Changed_Names is
4 N : Name := new String'("John");
5 begin
6 Lower_And_Show (N.all);
7 end Show_Changed_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Changed_Names_String
MD5: 783ea8c45ed8ad3e0007524c11b6bcc4
Runtime output
Name: john
Instead of changing the object in the Lower_And_Show procedure, we could replace the
access value by another one — for example, by allocating a new string inside the procedure.
In this case, we have to pass the access value by reference using the in out parameter
mode:
7 end Names;
3 with Ada.Characters.Handling;
4 use Ada.Characters.Handling;
5
14 end Names;
3 procedure Show_Changed_Names is
4 N : Name := new String'("John");
5 begin
6 Lower_And_Show (N);
7 end Show_Changed_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Replaced_Names
MD5: a4abfe6fdb1e5029e8eea17641cd960b
Runtime output
Name: john
Now, instead of changing the object referenced by N, we're actually replacing it with a new
object that we allocate inside the Lower_And_Show procedure.
As expected, contrary to the previous examples, we cannot implement this code by relying
on parameter modes to replace the object. In fact, we have to use access types for this
kind of operations.
Note that this implementation creates a memory leak. In a proper implementation, we
should make sure to deallocate the object (page 800), as explained later on.
In previous code examples from this section, we've seen that passing a parameter by ref-
erence using the in or in out parameter modes is an alternative to using access values as
parameters. Let's focus on the subprogram declarations of those code examples and their
parameter modes:
When we analyze the information from this table, we see that in the case of using strings
with different parameter modes, we have a clear indication whether the subprogram might
change the object or not. For example, we know that a call to Show (N : String) won't
change the string object that we're passing as the actual parameter.
In the case of passing an access value, we cannot know whether the designated object is
going to be altered by a call to the subprogram. In fact, in both Show and Lower_And_Show
procedures, the parameter is the same: N : Name — in other words, the parameter mode
is in in both cases. Here, there's no clear indication about the effects of a subprogram call
on the designated object.
The simplest way to ensure that the object isn't changed in the subprogram is by using
access-to-constant types (page 780), which we discuss later on. In this case, we're basically
saying that the object we're accessing in Show is constant, so we cannot possibly change
it:
9 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 N : Name := new String'("John");
5 begin
6 Show (Constant_Name (N));
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Names_Constant
MD5: 77526e0a159bf1bcbef08a21be250f3c
Runtime output
Name: John
In this case, the Constant_Name type ensures that the N parameter won't be changed in
the Show procedure. Note that we need to convert from Name to Constant_Name to be able
to call the Show procedure (in the Show_Names procedure). Although using in String is still
a simpler solution, this approach works fine.
(Feel free to uncomment the call to To_Lower in the Show procedure and the corresponding
with- and use-clauses to see that the compilation fails when trying to change the constant
object.)
We could also mitigate the problem by using contracts. For example:
11 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 N : Name := new String'("John");
5 begin
6 Show (N);
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Names_Postcondition
MD5: 2a70993232baca9d58d36e537a6fd32b
Runtime output
Name: John
Although a bit more verbose than a simple in String, the information in the specification
of Show at least gives us an indication that the object won't be affected by the call to this
subprogram. Note that this code actually compiles if we try to modify N.all in the Show
procedure, but the post-condition fails at runtime when we do that.
(By uncommentating and building the code again, you'll see an exception being raised at
runtime when trying to change the object.)
In the postcondition above, we're using 'Old to refer to the original object before the sub-
program call. Unfortunately, we cannot use this attribute when dealing with limited private
types (page 928) — or limited types in general. For example, let's change the declaration
of Name and have it as a limited private type instead:
13 private
14
24 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 N : Name := Init ("John");
5 begin
6 Show (N);
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Names_Limited_Private
MD5: 39691394d7a934869dc569eb72d1bf3a
Build output
In this case, we have no means to indicate that a call to Show won't change the internal
state of the actual parameter.
9 function To_Constant_Name
10 (N : Name)
11 return Constant_Name;
12
15 private
16
17 type Name is
18 access String;
19
20 type Constant_Name is
21 access constant String;
22
26 function To_Constant_Name
27 (N : Name)
28 return Constant_Name is
29 (Constant_Name (N));
30
31 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 N : Name := Init ("John");
5 begin
6 Show (To_Constant_Name (N));
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Parameters_As_
↪Access_Values.Names_Constant_Limited_Private
MD5: 30da588b57e6b4dfbf9934f77d348473
Runtime output
Name: John
In this version of the source code, the Show procedure doesn't have any side-effects, as we
cannot modify N inside the procedure.
Having the information about the effects of a subprogram call to an object is very important:
we can use this information to set expectations — and avoid unexpected changes to an
object. Also, this information can be used to prove that a program works as expected.
Therefore, whenever possible, we should avoid access values as parameters. Instead, we
can rely on appropriate parameter modes and pass an object by reference.
There are cases, however, where the design of our application doesn't permit replacing
the access type with simple parameter modes. Whenever we have an abstract data type
encapsulated as a limited private type — such as in the last code example —, we might
have no means to avoid access values as parameters. In this case, using the access type
is of course justifiable. We'll see such a case in the next section (page 762).
28.1.5 Self-reference
3 type Integer_List;
4
12 end Linked_List_Example;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Self_Reference.
↪Linked_List_Example
MD5: b2d3a048473d498bbe691bc6e38ca1e9
Here, we're using the incomplete type Integer_List in the declaration of the Next type,
which we then use in the complete declaration of the Integer_List type.
Self-references are useful, for example, to create unbounded containers — such as the
linked lists mentioned in the example above. Let's extend this code example and partially
implement a generic package for linked lists:
7 procedure Append_Front
8 (L : in out List;
9 E : T);
10
11 procedure Append_Rear
12 (L : in out List;
13 E : T);
14
17 private
18
32 end Linked_Lists;
7 procedure Append_Front
8 (L : in out List;
9 E : T)
10 is
11 New_First : constant List := new
12 Component'(Value => E,
(continues on next page)
18 procedure Append_Rear
19 (L : in out List;
20 E : T)
21 is
22 New_Last : constant List := new
23 Component'(Value => E,
24 Next => null);
25 begin
26 if L = null then
27 L := New_Last;
28 else
29 declare
30 Last : List := L;
31 begin
32 while Last.Next /= null loop
33 Last := Last.Next;
34 end loop;
35 Last.Next := New_Last;
36 end;
37 end if;
38 end Append_Rear;
39
57 end Linked_Lists;
3 procedure Test_Linked_List is
4 package Integer_Lists is new
5 Linked_Lists (T => Integer);
6 use Integer_Lists;
7
8 L : List;
9 begin
10 Append_Front (L, 3);
11 Append_Rear (L, 4);
12 Append_Rear (L, 5);
13 Append_Front (L, 2);
(continues on next page)
18 Show (L);
19 end Test_Linked_List;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Self_Reference.
↪Linked_List_Example
MD5: 8af1ff7bbda44362802ba4f93b9c5741
Runtime output
[ 1 2 3 4 5 6 7 ]
In this example, we declare an incomplete type Component in the private part of the generic
Linked_Lists package. We use this incomplete type to declare the access type List, which
is then used as a self-reference in the Next component of the Component type.
Note that we're using the List type as a parameter (page 752) for the Append_Front,
Append_Rear and Show procedures.
In the section on mutually dependent types (page 418), we've seen a code example where
each type depends on the other one. We could rewrite that code example using access
types:
3 type T2;
4 type T2_Access is access T2;
5
6 type T1 is record
7 B : T2_Access;
8 end record;
9
12 type T2 is record
13 A : T1_Access;
14 end record;
15
16 end Mutually_Dependent;
174 http://www.ada-auth.org/standards/22rm/html/RM-3-10-1.html
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Mutually_Dependent_
↪Access_Types.Example
MD5: b21ffc4cdfe3db939dfc841cf8434344
In this example, T1 and T2 are mutually dependent types via the access types T1_Access
and T2_Access — we're using those access types in the declaration of the B and A compo-
nents.
28.1.7 Dereferencing
In the Introduction to Ada course (page 98), we discussed the .all syntax to dereference
access values:
3 procedure Show_Dereferencing is
4
11 begin
12 A1 := new Integer;
13
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Dereferencing.
↪Simple_Dereferencing
MD5: 65655768c17a02991ffeda9a853b6ffb
Runtime output
A1: 22
3 procedure Show_Dereferencing is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
8 type Integer_Array_Access is
9 access Integer_Array;
(continues on next page)
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Dereferencing.Array_
↪Dereferencing
MD5: 0e533dfd8ec1a74af17c99633c292e95
Runtime output
Arr (: 1): 1
Arr (: 2): 2
Arr (: 3): 3
Arr (: 4): 5
Arr (: 5): 8
Arr (: 6): 13
In this example, we dereference the access value by writing Arr.all. We then assign an
array aggregate to it — this becomes Arr.all := (..., ...);. Similarly, in the loop, we
write Arr.all (I) to access the I component of the array.
Implicit Dereferencing
Implicit dereferencing allows us to omit the .all suffix without getting a compilation error.
In this case, the compiler knows that the dereferenced object is implied, not the access
value.
Ada supports implicit dereferencing in these use cases:
• when accessing components of a record or an array — including array slices.
• when accessing subprograms that have at least one parameter (we discuss this topic
later in this chapter);
• when accessing some attributes — such as some array and task attributes.
175 http://www.ada-auth.org/standards/22rm/html/RM-4-1.html
Arrays
Let's start by looking into an example of implicit dereferencing of arrays. We can take the
previous code example and replace Arr.all (I) by Arr (I):
3 procedure Show_Dereferencing is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
8 type Integer_Array_Access is
9 access Integer_Array;
10
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Dereferencing.Array_
↪Implicit_Dereferencing
MD5: ade602a9e6976018e0c00f930a2399f1
Runtime output
Arr (: 1): 1
Arr (: 2): 2
Arr (: 3): 3
Arr (: 4): 5
Arr (: 5): 8
Arr (: 6): 13
Both forms — Arr.all (I) and Arr (I) — are equivalent. Note, however, that there's
no implicit dereferencing when we want to access the whole array. (Therefore, we cannot
write Arr := (1, 2, 3, 5, 8, 13);.) However, as slices are implicitly dereferenced, we
can write Arr (1 .. 6) := (1, 2, 3, 5, 8, 13); instead of Arr.all (1 .. 6) := (1,
2, 3, 5, 8, 13);. Alternatively, we can assign to the array components individually and
use implicit dereferencing for each component:
Arr (1) := 1;
Arr (2) := 2;
Arr (3) := 3;
Arr (4) := 5;
Arr (5) := 8;
Arr (6) := 13;
Implicit dereferencing isn't available for the whole array because we have to distinguish
between assigning to access objects and assigning to actual arrays. For example:
3 procedure Show_Array_Assignments is
4
5 type Integer_Array is
6 array (Positive range <>) of Integer;
7
8 type Integer_Array_Access is
9 access Integer_Array;
10
11 procedure Show_Array
12 (Name : String;
13 Arr : Integer_Array_Access) is
14 begin
15 Put (Name);
16 for E of Arr.all loop
17 Put (Integer'Image (E));
18 end loop;
19 New_Line;
20 end Show_Array;
21
30 -- Array assignment
31 Arr_2.all := Arr_1.all;
32
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Dereferencing.Array_
↪Assignments
MD5: 9b1f99af081000c28a6bf9b033127ea3
Runtime output
Arr_2 1 2 3 5 8 13
Arr_2 377 610 987 1597 2584 4181
Records
3 procedure Show_Dereferencing is
4
MD5: 9af72502d04f128785f77dcc829d5d48
Runtime output
R.I: 1
R.F: 5.00000E+00
Again, we can replace R.all.I by R.I, as record components are implicitly dereferenced.
Also, we could use implicit dereference when assigning to record components individually:
R.I := 1;
R.F := 5.0;
Attributes
3 procedure Show_Dereferencing is
4
5 type Integer_Array is
(continues on next page)
8 type Integer_Array_Access is
9 access Integer_Array;
10
21 Put_Line
22 ("Arr'Component_Size: "
23 & Integer'Image (Arr'Component_Size));
24 Put_Line
25 ("Arr.all'Component_Size: "
26 & Integer'Image (Arr.all'Component_Size));
27
28 Put_Line
29 ("Arr'Size: "
30 & Integer'Image (Arr'Size));
31 Put_Line
32 ("Arr.all'Size: "
33 & Integer'Image (Arr.all'Size));
34 end Show_Dereferencing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Dereferencing.Array_
↪Implicit_Dereferencing
MD5: 5730e18c8d2ed5e26a4d7d325a46a7e9
Runtime output
Arr'First: 1
Arr'Last: 6
Arr'Component_Size: 32
Arr.all'Component_Size: 32
Arr'Size: 128
Arr.all'Size: 192
Here, we can write Arr'First and Arr'Last instead of Arr.all'First and Arr.
all'Last, respectively, because Arr is implicitly dereferenced. The same applies to
Arr'Component_Size. Note that we can write both Arr'Size and Arr.all'Size, but they
have different meanings:
• Arr'Size is the size of the access object; while
• Arr.all'Size indicates the size of the actual array Arr.
In other words, the Size attribute is not implicitly dereferenced. In fact, any attribute that
could potentially be ambiguous is not implicitly dereferenced. Therefore, in those cases,
we must explicitly indicate (by using .all or not) how we want to use the attribute.
Summary
The following table summarizes all instances where implicit dereferencing is supported:
Ragged arrays — also known as jagged arrays — are non-uniform, multidimensional arrays.
They can be useful to implement tables with varying number of coefficients, as we discuss
as an example in this section.
176 http://www.ada-auth.org/standards/22rm/html/RM-4-1.html
177 http://www.ada-auth.org/standards/22rm/html/RM-4-1-1.html
178 http://www.ada-auth.org/standards/22rm/html/RM-4-1-2.html
179 http://www.ada-auth.org/standards/22rm/html/RM-4-1-3.html
180 http://www.ada-auth.org/standards/22rm/html/RM-4-1-4.html
Consider an algorithm that processes data based on coefficients that depends on a selected
quality level:
(Note that this is just a bogus table with no real purpose, as we're not trying to implement
any actual algorithm.)
We can implement this table as a two-dimensional array (Calc_Table), where each quality
level has an associated array:
3 type Quality_Level is
4 (Simplified, Better, Best);
5
6 private
7
23 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Ragged_Arrays.
↪Uniform_Table
MD5: befa8d2b684ee20495f2dd6907dc44d4
Note that, in this implementation, we have a separate table Last that indicates the actual
number of coefficients of each quality level.
Alternatively, we could use a record (Table_Coefficient) that stores the number of coef-
ficients and the actual coefficients:
3 type Quality_Level is
4 (Simplified, Better, Best);
(continues on next page)
6 type Data is
7 array (Positive range <>) of Float;
8
9 private
10
25 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Ragged_Arrays.
↪Uniform_Table
MD5: 4c8602f6ecede0ac1231838c0a0a54b7
In this case, we have a unidimensional array where each component (of Ta-
ble_Coefficient type) contains an array (Coef) with the coefficients.
This is an example of a Process procedure that references the Calc_Table:
6 end Data_Processing.Operations;
16 end Data_Processing.Operations;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Ragged_Arrays.
↪Uniform_Table
MD5: 2b0d2cee265509e64e507cfa6289bdcc
Note that, to loop over the coefficients, we're using for J in 1 .. Calc_Table (Q).
Last loop instead of for J in Calc_Table (Q)'Range loop. As we're trying to make
a non-uniform array fit in a uniform array, we cannot simply loop over all elements using
the Range attribute, but must be careful to use the correct number of elements in the loop
instead.
Also, note that Calc_Table has 15 coefficients in total. Out of those coefficients, 6 coef-
ficients (or 40 percent of the table) aren't being used. Naturally, this is wasted memory
space. We can improve this by using ragged arrays.
Ragged arrays are declared by using an access type to an array. By doing that, each ar-
ray can be declared with a different size, thereby creating a non-uniform multidimensional
array.
For example, we can declare a constant array Table as a ragged array:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 private
7
8 type Integer_Array_Access is
9 access constant Integer_Array;
10
24 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Ragged_Arrays.
↪Simple_Ragged_Array
MD5: 28e044a43bf45585a0268c60d63c629e
Here, each component of Table is an access to another array. As each array is allocated
via new, those arrays may have different sizes.
We can rewrite the example from the previous subsection using a ragged array for the
Calc_Table:
3 type Quality_Level is
4 (Simplified, Better, Best);
5
6 type Data is
7 array (Positive range <>) of Float;
8
9 private
10
23 end Data_Processing;
MD5: 0781b27cba27dbd1e74da54e425a1f4b
Now, we aren't wasting memory space because each data component has the right size that
is required for each quality level. Also, we don't need to store the number of coefficients, as
this information is automatically available from the array initialization — via the allocation
of the Data array for the Coefficients type.
Note that the Coefficients type is defined as access constant. We discuss access-to-
constant types (page 780) in more details later on.
This is the adapted Process procedure:
6 end Data_Processing.Operations;
16 end Data_Processing.Operations;
Now, we can simply loop over the coefficients by writing for J in Calc_Table (Q)'Range
loop, as each element of Calc_Table automatically has the correct range.
28.1.9 Aliasing
The term aliasing181 refers to objects in memory that we can access using more than a
single reference. In Ada, if we allocate an object via new, we have a potentially aliased
object. We can then have multiple references to this object:
3 procedure Show_Aliasing is
4 type Integer_Access is access Integer;
5
6 A1, A2 : Integer_Access;
7 begin
8 A1 := new Integer;
9 A2 := A1;
10
11 A1.all := 22;
12 Put_Line ("A1: " & Integer'Image (A1.all));
13 Put_Line ("A2: " & Integer'Image (A2.all));
14
15 A2.all := 24;
16 Put_Line ("A1: " & Integer'Image (A1.all));
17 Put_Line ("A2: " & Integer'Image (A2.all));
18 end Show_Aliasing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Aliasing_
↪Via_Access
MD5: 2fde6073cec9823a1a9d93aec82384e1
Runtime output
A1: 22
A2: 22
A1: 24
A2: 24
In this example, we access the object allocated via new by using either A1 or A2, as both
refer to the same aliased object. In other words, A1 or A2 allow us to access the same object
in memory.
Important
181 https://en.wikipedia.org/wiki/Aliasing_(computing)
Note that aliasing is unrelated to renaming. For example, we could use renaming to write
a program that looks similar to the one above:
3 procedure Show_Renaming is
4 A1 : Integer;
5 A2 : Integer renames A1;
6 begin
7 A1 := 22;
8 Put_Line ("A1: " & Integer'Image (A1));
9 Put_Line ("A2: " & Integer'Image (A2));
10
11 A2 := 24;
12 Put_Line ("A1: " & Integer'Image (A1));
13 Put_Line ("A2: " & Integer'Image (A2));
14 end Show_Renaming;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Renaming
MD5: 99a47d02000b91f7464dffe994fd8ee6
Runtime output
A1: 22
A2: 22
A1: 24
A2: 24
Here, A1 or A2 are two different names for the same object. However, the object itself isn't
aliased.
Aliased objects
As we discussed previously (page 737), we use new to create aliased objects on the heap.
We can also use general access types to access objects that were created on the stack.
By default, objects created on the stack aren't aliased. Therefore, we have to indicate
that an object is aliased by using the aliased keyword in the object's declaration: Obj :
aliased Integer;.
Let's see an example:
3 procedure Show_Aliased_Obj is
4 type Integer_Access is access all Integer;
5
11 A1.all := 22;
12 Put_Line ("A1: " & Integer'Image (A1.all));
13 end Show_Aliased_Obj;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Access_
↪Aliased_Obj
MD5: 98c8e47d7c2b5df8075918b239a8d476
Runtime output
A1: 22
Here, we declare I_Var as an aliased integer variable and get a reference to it, which we
assign to A1. Naturally, we could also have two accesses A1 and A2:
3 procedure Show_Aliased_Obj is
4 type Integer_Access is access all Integer;
5
12 A1.all := 22;
13 Put_Line ("A1: " & Integer'Image (A1.all));
14 Put_Line ("A2: " & Integer'Image (A2.all));
15
16 A2.all := 24;
17 Put_Line ("A1: " & Integer'Image (A1.all));
18 Put_Line ("A2: " & Integer'Image (A2.all));
19
20 end Show_Aliased_Obj;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Access_
↪Aliased_Obj
MD5: ac331285456462f05abe7e1fd5e3ca2b
Runtime output
A1: 22
A2: 22
A1: 24
A2: 24
Let's now discuss how to declare general access types. In addition to the standard (pool-
specific) access type declarations, Ada provides two access modifiers:
Type Declaration
Access-to-variable type T_Acc is access all T
Access-to-constant type T_Acc is access constant T
3 type Integer_Access is
4 access Integer;
5
6 type Integer_Access_All is
7 access all Integer;
8
9 type Integer_Access_Const is
10 access constant Integer;
11
12 end Integer_Access_Types;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Show_
↪Access_Modifiers
MD5: 98ccaa703194ae88222ccc5a4400e967
As we've seen previously, we can use a type such as Integer_Access to allocate objects
dynamically. However, we cannot use this type to refer to declared objects, for example. In
this case, we have to use an access-to-variable type such as Integer_Access_All. Also, if
we want to access constants — or access objects that we want to treat as constants —, we
use a type such as Integer_Access_Const.
183 http://www.ada-auth.org/standards/22rm/html/RM-3-3-1.html
184 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
Access attribute
To get access to a variable or a constant, we make use of the Access attribute. For example,
I_Var'Access gives us access to the I_Var object.
Let's look at an example of how to use the integer access types from the previous code
snippet:
3 type Integer_Access is
4 access Integer;
5
6 type Integer_Access_All is
7 access all Integer;
8
9 type Integer_Access_Const is
10 access constant Integer;
11
12 procedure Show;
13
14 end Integer_Access_Types;
17 procedure Show is
18 begin
19 Put_Line ("Dyn_Ptr: "
20 & Integer'Image (Dyn_Ptr.all));
21 Put_Line ("I_Var_Ptr: "
22 & Integer'Image (I_Var_Ptr.all));
23 Put_Line ("I_Var_C_Ptr: "
24 & Integer'Image
25 (I_Var_C_Ptr.all));
26 Put_Line ("Fact_Ptr: "
27 & Integer'Image (Fact_Ptr.all));
28 end Show;
29
30 end Integer_Access_Types;
3 procedure Show_Access_Modifiers is
4 begin
5 Integer_Access_Types.Show;
6 end Show_Access_Modifiers;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Show_
↪Access_Modifiers
MD5: c9036f060859207ea14354b26dc8b981
Runtime output
Dyn_Ptr: 30
I_Var_Ptr: 0
I_Var_C_Ptr: 0
Fact_Ptr: 42
In this example, Dyn_Ptr refers to a dynamically allocated object, I_Var_Ptr refers to the
I_Var variable, and Fact_Ptr refers to the Fact constant. We get access to the variable
and the constant objects by using the Access attribute.
Also, we declare I_Var_C_Ptr as an access-to-constant, but we get access to the I_Var
variable. This simply means the object I_Var_C_Ptr refers to is treated as a constant.
Therefore, we can write I_Var := 22;, but we cannot write I_Var_C_Ptr.all := 22;.
Non-aliased objects
As mentioned earlier, by default, declared objects — which are allocated on the stack —
aren't aliased. Therefore, we cannot get a reference to those objects. For example:
3 procedure Show_Access_Error is
4 type Integer_Access is access all Integer;
5 I_Var : Integer;
6 A1 : Integer_Access;
7 begin
8 A1 := I_Var'Access;
9
10 A1.all := 22;
11 Put_Line ("A1: " & Integer'Image (A1.all));
12 end Show_Access_Error;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Access_Non_
↪Aliased_Obj
MD5: 2a9904062eea96ae6dc209493d6f20d4
185 http://www.ada-auth.org/standards/22rm/html/RM-3-10-2.html
Build output
In this example, the compiler complains that we cannot get a reference to I_Var because
I_Var is not aliased.
We can use aliased objects to declare ragged arrays (page 772). For example, we can
rewrite a previous program using aliased constant objects:
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 private
7
8 type Integer_Array_Access is
9 access constant Integer_Array;
10
25 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Ragged_
↪Array_Aliased_Objs
MD5: 7e284560c447c02628e34bac982d4ad5
Here, instead of allocating the constant arrays dynamically via new, we declare three aliased
arrays (Tab_1, Tab_2 and Tab_3) and get a reference to them in the declaration of Table.
It's interesting to mention that access objects can be aliased themselves. Consider this
example where we declare the Integer_Access_Access type to refer to an access object:
3 procedure Show_Aliased_Access_Obj is
4
5 type Integer_Access is
6 access all Integer;
7 type Integer_Access_Access is
8 access all Integer_Access;
9
17 B.all.all := 22;
18 Put_Line ("A: " & Integer'Image (A.all));
19 Put_Line ("B: " & Integer'Image (B.all.all));
20 end Show_Aliased_Access_Obj;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Aliased_
↪Access
MD5: 77e9be5e29cfb99aef9409728202ba9d
Runtime output
A: 22
B: 22
After the assignments in this example, B refers to A, which in turn refers to I_Var. Note
that this code only compiles because we declare A as an aliased (access) object.
Aliased components
Components of an array or a record can be aliased. This allows us to get access to those
components:
3 procedure Show_Aliased_Components is
4
24 A := R.I_Var_2'Access;
25 Put_Line ("A: " & Integer'Image (A.all));
26
27 A := Arr (2)'Access;
28 Put_Line ("A: " & Integer'Image (A.all));
29 end Show_Aliased_Components;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Aliased_
↪Components
MD5: 5dfaa248caf8e37a4a3a1e1a24973777
Runtime output
A: 24
A: 42
In this example, we get access to the I_Var_2 component of record R. (Note that trying to
access the I_Var_1 component would gives us a compilation error, as this component is
not aliased.) Similarly, we get access to the second component of array Arr.
Declaring components with the aliased keyword allows us to specify that those are ac-
cessible via other paths besides the component name. Therefore, the compiler won't store
them in registers. This can be essential when doing low-level programming — for example,
when accessing memory-mapped registers. In this case, we want to ensure that the com-
piler uses the memory address we're specifying (instead of assigning registers for those
components).
Aliased parameters
5 end Data_Processing;
9 type Integer_Constant_Access is
10 access constant Integer;
11
12 A : constant Integer_Constant_Access
13 := I'Access;
14 begin
15 Put_Line ("Value : I "
16 & Integer'Image (A.all));
17 end Show;
18
29 begin
30 Local_Set_One (I'Access);
31 end Set_One;
32
48 end Data_Processing;
3 procedure Show_Aliased_Param is
4 I : aliased Integer := 22;
5 begin
6 Proc (I);
7 end Show_Aliased_Param;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Aliasing.Aliased_
↪Rec_Component
MD5: 076238603036aa51cafcc013f38bc8f3
Runtime output
Value : I 22
Value : I 23
Here, Proc has an aliased in out parameter. In Proc's body, we declare the Inte-
ger_Access type as an access all type. We use the same approach in body of the
Set_One procedure, which has an aliased out parameter. Finally, the Show procedure
has an aliased in parameter. Therefore, we declare the Integer_Constant_Access as
an access constant type.
Note that parameter aliasing has an influence on how arguments are passed to a subpro-
gram when the parameter is of scalar type. When a scalar parameter is declared as aliased,
the corresponding argument is passed by reference. For example, if we had declared pro-
cedure Show (I : Integer), the argument for I would be passed by value. However,
since we're declaring it as aliased Integer, it is passed by reference.
187 http://www.ada-auth.org/standards/22rm/html/RM-6-1.html
188 http://www.ada-auth.org/standards/22rm/html/RM-6-2.html
189 http://www.ada-auth.org/standards/22rm/html/RM-6-4-1.html
This section provides an introduction to accessibility levels and accessibility rules. This
topic can be very complicated, and by no means do we intend to cover all the details here.
(In fact, discussing all the details about accessibility levels and rules could be a long chapter
on its own. If you're interested in them, please refer to the Ada Reference Manual.) In any
case, the goal of this section is to present the intention behind the accessibility rules and
build intuition on how to best use access types in your code.
Lifetime of objects
First, let's talk a bit about lifetime of objects191 . We assume you understand the concept,
so this section is very short.
In very simple terms, the lifetime of an object indicates when an object still has relevant
information. For example, if a variable V gets out of scope, we say that its lifetime has
ended. From this moment on, V no longer exists.
For example:
3 procedure Show_Lifetime is
4 I_Var_1 : Integer := 22;
5 begin
6
7 Inner_Block : declare
8 I_Var_2 : Integer := 42;
9 begin
10 Put_Line ("I_Var_1: "
11 & Integer'Image (I_Var_1));
12 Put_Line ("I_Var_2: "
13 & Integer'Image (I_Var_2));
14
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Lifetime
MD5: ebe36f12c832ecfe71399b89801808d4
Build output
In this example, we declare I_Var_1 in the Show_Lifetime procedure, and I_Var_2 in its
Inner_Block.
This example doesn't compile because we're trying to use I_Var_2 after its lifetime has
ended. However, if such a code could compile and run, the last call to Put_Line would
potentially display garbage to the user. (In fact, the actual behavior would be undefined.)
Accessibility Levels
In basic terms, accessibility levels are a mechanism to assess the lifetime of objects (as
we've just discussed). The starting point is the library level: this is the base level, and no
level can be deeper than that. We start "moving" to deeper levels when we use a library in
a subprogram or call other subprograms for example.
Suppose we have a procedure Proc that makes use of a package Pkg, and there's a block
in the Proc procedure:
package Pkg is
-- Library level
end Pkg;
procedure Proc is
begin
declare
-- Two levels deeper than
-- library level
begin
null;
end;
end Proc;
The levels themselves are not visible to the programmer. For example, there's no Ac-
cess_Level attribute that returns an integer value indicating the level. Also, you cannot
write a user message that displays the level at a certain point. In this sense, accessibility
levels are assessed relatively to each other: we can only say that a specific operation is at
the same or at a deeper level than another one.
Accessibility Rules
The accessibility rules determine whether a specific use of access types or objects is legal
(or not). Actually, accessibility rules exist to prevent dangling references (page 795), which
we discuss later. Also, they are based on the accessibility levels (page 789) we discussed
earlier.
Code example
As mentioned earlier, the accessibility level at a specific point isn't visible to the program-
mer. However, to illustrate which level we have at each point in the following code example,
we use a prefix (L0, L1, and L2) to indicate whether we're at the library level (L0) or at a
deeper level.
Let's now look at the complete code example:
3 type L0_Integer_Access is
4 access all Integer;
5
6 L0_IA : L0_Integer_Access;
7
10 end Library_Level;
3 procedure Show_Library_Level is
4 type L1_Integer_Access is
5 access all Integer;
6
7 L0_IA_2 : L0_Integer_Access;
8 L1_IA : L1_Integer_Access;
9
12 procedure Test is
13 type L2_Integer_Access is
14 access all Integer;
15
16 L2_IA : L2_Integer_Access;
17
25 L2_IA := L2_Var'Access;
26 -- ^^^^^^
27 -- LEGAL: L2 object to
28 -- L2 access object
29 end Test;
30
31 begin
32 L0_IA := new Integer'(22);
33 -- ^^^^^^^^^^^
34 -- LEGAL: L0 object to
35 -- L0 access object
36
42 L0_IA := L1_Var'Access;
43 -- ^^^^^^
44 -- ILLEGAL: L1 object to
45 -- L0 access object
46
47 L0_IA_2 := L1_Var'Access;
48 -- ^^^^^^
49 -- ILLEGAL: L1 object to
50 -- L0 access object
51
52 L1_IA := L0_Var'Access;
53 -- ^^^^^^
54 -- LEGAL: L0 object to
55 -- L1 access object
56
57 L1_IA := L1_Var'Access;
58 -- ^^^^^^
59 -- LEGAL: L1 object to
60 -- L1 access object
61
62 L0_IA := L1_IA;
63 -- ^^^^^
64 -- ILLEGAL: type mismatch
65
72 Test;
73 end Show_Library_Level;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Accessibility_Library_Level
MD5: b3bed7eb2a8dfc78a2e7a7d2ce99f736
Build output
The same rules apply to the conversion between access types. In the code example, the
L0_Integer_Access (L1_IA) conversion is illegal because the resulting object is less deep.
That being said, conversions on the same level are fine:
5 type L1_B_Integer_Access is
6 access all Integer;
7
8 L1_IA : L1_Integer_Access;
9 L1_B_IA : L1_B_Integer_Access;
10
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Same_Level_Conversion
MD5: 7276a06e9f5b634d4f5a10a892071d87
Note that the accessibility rules also apply to access values as subprogram parameters.
For example, compilation fails for this example:
5 type Constant_Name is
6 access constant String;
7
10 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 S : aliased String := "John";
5 begin
6 Show (S'Access);
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Accessibility_Checks_Parameters
MD5: 6b8bf2799caa32f55d216ac0b58fcd39
Build output
In this case, the S'Access cannot be used as the actual parameter for the N parameter of
the Show procedure because it's in a deeper level. If we allocate the string via new, however,
the code compiles as expected:
3 procedure Show_Names is
4 S : Name := new String'("John");
5 begin
6 Show (Constant_Name (S));
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Accessibility_Checks_Parameters
MD5: 30237c83426db758804b802e1953d5d9
Runtime output
Name: John
This version of the code works because both object and access object have the same level.
Dangling References
An access value that points to a non-existent object is called a dangling reference. Later on,
we'll discuss how dangling references may occur using unchecked deallocation (page 803).
Dangling references are created when we have an access value pointing to an object whose
lifetime has ended, so it becomes a non-existent object. This could occur, for example,
when an access value still points to an object X that has gone out of scope.
As mentioned in the previous section, the accessibility rules of the Ada language ensure
that such situations never happen! In fact, whenever possible, the compiler applies those
rules to detect potential dangling references at compile time. When this detection isn't
possible at compile time, the compiler introduces an accessibility check (page 669). If this
check fails at runtime, it raises a Program_Error exception — thereby preventing that a
dangling reference gets used.
Let's see an example of how dangling references could occur:
3 procedure Show_Dangling_Reference is
4
5 type Integer_Access is
6 access all Integer;
7
10 A1 : Integer_Access;
11 begin
12 A1 := I_Var_1'Access;
13 Put_Line ("A1.all: "
14 & Integer'Image (A1.all));
15
18 Inner_Block : declare
(continues on next page)
24 --
25 -- A2 only exists in Inner_Block
26 --
27 A2 : Integer_Access;
28 begin
29 A2 := I_Var_1'Access;
30 Put_Line ("A2.all: "
31 & Integer'Image (A2.all));
32
33 A1 := I_Var_2'Access;
34 -- PROBLEM: A1 and Integer_Access type
35 -- have longer lifetime than
36 -- I_Var_2
37
41 A2 := I_Var_2'Access;
42 -- PROBLEM: A2 has the same lifetime as
43 -- I_Var_2, but Integer_Access
44 -- type has a longer lifetime.
45
54 end Show_Dangling_Reference;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Dangling_Reference_Rules
MD5: 98e597f3f6a12075c474612bb42f4cb7
Build output
Here, we declare the access objects A1 and A2 of Integer_Access type, and the I_Var_1
and I_Var_2 objects. Moreover, A1 and I_Var_1 are declared in the scope of the
Show_Dangling_Reference procedure, while A2 and I_Var_2 are declared in the In-
ner_Block.
When we try to compile this code, we get two compilation errors due to violation of acces-
sibility rules. Let's now discuss these accessibility rules in terms of lifetime, and see which
problems they are preventing in each case.
1. In the A1 := I_Var_2'Access assignment, the main problem is that A1 has a longer
lifetime than I_Var_2. After the Inner_Block finishes — when I_Var_2 gets out of
scope and its lifetime has ended —, A1 would still be pointing to an object that does
not longer exist.
2. In the A2 := I_Var_2'Access assignment, however, both A2 and I_Var_2 have the
same lifetime. In that sense, the assignment may actually look pretty much OK.
• However, as mentioned in the previous section, Ada also cares about the lifetime
of access types. In fact, since the Integer_Access type is declared outside of the
Inner_Block, it has a longer lifetime than A2 and I_Var_2.
• To be more precise, the accessibility rules detect that A2 is an access object of a
type that has a longer lifetime than I_Var_2.
At first glance, this last accessibility rule may seem too strict, as both A2 and I_Var_2 have
the same lifetime — so nothing bad could occur when dereferencing A2. However, consider
the following change to the code:
A2 := I_Var_2'Access;
A1 := A2;
-- PROBLEM: A1 will still be referring
-- to I_Var_2 after the
-- Inner_Block, i.e. when the
-- lifetime of I_Var_2 has
-- ended!
Here, we're introducing the A1 := A2 assignment. The problem with this is that I_Var_2's
lifetime ends when the Inner_Block finishes, but A1 would continue to refer to an I_Var_2
object that doesn't exist anymore — thereby creating a dangling reference.
Even though we're actually not assigning A2 to A1 in the original code, we could have done
it. The accessibility rules ensure that such an error is never introduced into the program.
Inner_Block : declare
type Integer_Local_Access is
access all Integer;
A2 : Integer_Local_Access;
begin
A2 := I_Var_2'Access;
-- This assignment is fine because
-- the Integer_Local_Access type has
-- the same lifetime as I_Var_2.
end Inner_Block;
With this change, A2 becomes an access object of a type that has the same lifetime as
I_Var_2, so that the assignment doesn't violate the rules anymore.
(Note that in the Inner_Block, we could have simply named the local access type In-
teger_Access instead of Integer_Local_Access, thereby masking the Integer_Access
We discuss the effects of dereferencing dangling references later in this chapter (page 805).
In this section, we discuss the Unchecked_Access attribute, which we can use to circumvent
accessibility issues for objects in specific cases. (Note that this attribute only exists for
objects, not for subprograms.)
We've seen previously (page 788) that the accessibility levels verify the lifetime of access
types. Let's see a simplified version of a code example from that section:
5 end Integers;
5 procedure Show_Access_Issue is
6 I_Var : aliased Integer := 42;
7
8 A : Integer_Access;
9 begin
10 A := I_Var'Access;
11 -- PROBLEM: A has the same lifetime as I_Var,
12 -- but Integer_Access type has a
13 -- longer lifetime.
14
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_Access.
↪Dangling_Reference_Rules
MD5: 646acabf3f388b52809349463d20d314
Build output
Here, the compiler complains about the A := I_Var'Access assignment because the In-
teger_Access type has a longer lifetime than A. However, we know that this assignment
to A — and further uses of A in the code — won't cause dangling references to be created.
Therefore, we can assume that assigning the access to I_Var to A is safe.
When we're sure that an access assignment cannot possibly generate dangling references,
we can the use Unchecked_Access attribute. For instance, we can use this attribute to
circumvent the compilation error in the previous code example, since we know that the
assignment is actually safe:
5 end Integers;
5 procedure Show_Access_Issue is
6 I_Var : aliased Integer := 42;
7
8 A : Integer_Access;
9 begin
10 A := I_Var'Unchecked_Access;
11 -- OK: assignment is now accepted.
12
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_Access.
↪Dangling_Reference_Rules
MD5: a71b9076d9e2983ffb9811183afdf6c1
Runtime output
A.all: 42
When we use the Unchecked_Access attribute, most rules still apply. The only difference
to the standard Access attribute is that unchecked access applies the rules as if the object
we're getting access to was being declared at library level. (For the code example we've
just seen, the check would be performed as if I_Var was declared in the Integers package
instead of being declared in the procedure.)
It is strongly recommended to avoid unchecked access in general. You should only use
it when you can safely assume that the access object will be discarded before the object
we had access to gets out of scope. Therefore, if this situation isn't clear enough, it's
best to avoid unchecked access. (Later in this chapter, we'll see some of the nasty issues
that arrive from creating dangling references.) Instead, you should work on improving the
software design of your application by considering alternatives such as using containers or
encapsulating access types in well-designed abstract data types.
192 http://www.ada-auth.org/standards/22rm/html/RM-13-10.html
So far, we've seen multiple examples of using new to allocate objects. In this section, we
discuss how to manually deallocate objects.
Our starting point to manually deallocate an object is the generic Ada.
Unchecked_Deallocation procedure. We first instantiate this procedure for an access
type whose objects we want to be able to deallocate. For example, let's instantiate it for
the Integer_Access type:
3 package Integer_Types is
4
7 --
8 -- Instantiation of Ada.Unchecked_Deallocation
9 -- for the Integer_Access type:
10 --
11 procedure Free is
12 new Ada.Unchecked_Deallocation
13 (Object => Integer,
14 Name => Integer_Access);
15 end Integer_Types;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Simple_Unchecked_Deallocation
MD5: 328b244cf406853e87494c381c9c4c9e
Here, we declare the Free procedure, which we can then use to deallocate objects that
were allocated for the Integer_Access type.
Ada.Unchecked_Deallocation is a generic procedure that we can instantiate for access
types. When declaring an instance of Ada.Unchecked_Deallocation, we have to specify
arguments for:
• the formal Object parameter, which indicates the type of actual objects that we want
to deallocate; and
• the formal Name parameter, which indicates the access type.
In a type declaration such as type Integer_Access is access Integer, Integer denotes
the Object, while Integer_Access denotes the Name.
Because each instance of Ada.Unchecked_Deallocation is bound to a specific access type,
we cannot use it for another access type, even if the type we use for the Object parameter
is the same:
3 package Integer_Types is
4
7 procedure Free is
8 new Ada.Unchecked_Deallocation
(continues on next page)
14 procedure Free is
15 new Ada.Unchecked_Deallocation
16 (Object => Integer,
17 Name => Another_Integer_Access);
18 end Integer_Types;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Simple_Unchecked_Deallocation
MD5: b9bc58ff60632287237e2e322fcbc63e
Here, we're declaring two Free procedures: one for the Integer_Access type, another for
the Another_Integer_Access. We cannot use the Free procedure for the Integer_Access
type when deallocating objects associated with the Another_Integer_Access type, even
though both types are declared as access Integer.
Note that we can use any name when instantiating the Ada.Unchecked_Deallocation pro-
cedure. However, naming it Free is very common.
Now, let's see a complete example that includes object allocation and deallocation:
3 package Integer_Types is
4
7 procedure Free is
8 new Ada.Unchecked_Deallocation
9 (Object => Integer,
10 Name => Integer_Access);
11
14 end Integer_Types;
14 end Integer_Types;
4 procedure Show_Unchecked_Deallocation is
5
6 I : Integer_Access;
7
8 begin
9 Put ("We haven't called new yet... ");
10 Show_Is_Null (I);
11
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Unchecked_Deallocation
MD5: a9f2df04e2fe0d5ee8c17249b4ae315a
Runtime output
3 procedure Show_Unchecked_Deallocation is
4
5 I : Integer_Access;
6
7 begin
8 I := new Integer;
9
10 Free (I);
11 Free (I);
12 Free (I);
13 end Show_Unchecked_Deallocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Unchecked_Deallocation
MD5: ce7f4f912f12d723ca673ca36a478765
The multiple calls to Free for the same access object don't cause any issues. Because the
access value is null after the first call to Free (I), we're actually just passing null as an
argument in the second and third calls to Free. However, any attempt to deallocate an
access value of null is ignored in the Free procedure, so the second and third calls to Free
don't have any effect.
We've discussed dangling references (page 795) before. In this section, we discuss how
unchecked deallocation can create dangling references and the issues of having them in
an application.
Let's reuse the last example and introduce I_2, which will point to the same object as I:
3 procedure Show_Unchecked_Deallocation is
4
5 I, I_2 : Integer_Access;
6
7 begin
8 I := new Integer;
9
10 I_2 := I;
11
15 --
16 -- Use I and I_2...
17 --
18 -- ... then deallocate memory...
19 --
20
21 Free (I);
22
29 Free (I);
30 Free (I);
(continues on next page)
193 http://www.ada-auth.org/standards/22rm/html/RM-4-8.html
194 http://www.ada-auth.org/standards/22rm/html/RM-13-11-2.html
35 Free (I_2);
36 end Show_Unchecked_Deallocation;
MD5: ee5c20209a113a6c1bc7895b8ebdb174
Runtime output
free(): double free detected in tcache 2
As we've seen before, we can have multiple calls to Free (I). However, the call to Free
(I_2) is bad because I_2 is not null. In fact, it is a dangling reference — i.e. I_2 points to
an object that doesn't exist anymore. Also, the first call to Free (I) will reclaim the storage
that was allocated for the object that I originally referred to. The call to Free (I_2) will
then try to reclaim the previously-reclaimed object, but it'll fail in an undefined manner.
Because of these potential errors, you should be very careful when using unchecked deal-
location: it is the programmer's responsibility to avoid creating dangling references!
For the example we've just seen, we could avoid creating a dangling reference by explicitly
assigning null to I_2 to indicate that it doesn't point to any specific object:
3 procedure Show_Unchecked_Deallocation is
4
5 I, I_2 : Integer_Access;
6
7 begin
8 I := new Integer;
9
10 I_2 := I;
11
15 --
16 -- Use I and I_2...
17 --
18 -- ... then deallocate memory...
19 --
20
21 I_2 := null;
22
27 Free (I);
28 Free (I_2);
29 end Show_Unchecked_Deallocation;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Unchecked_Deallocation
MD5: 3381ba594cbbc0f1547e3f819bae0f97
Now, calling Free (I_2) doesn't cause any issues because it doesn't point to any object.
Note, however, that this code example is just meant to illustrate the issues of dangling
pointers and how we could circumvent them. We're not suggesting to use this approach
when designing an implementation. In fact, it's not practical for the programmer to make
every possible dangling reference become null if the calls to Free are strewn throughout
the code.
The suggested design is to not use Free in the client code, but instead hide its use within
bigger abstractions. In that way, all the occurrences of the calls to Free are in one package,
and the programmer of that package can then prevent dangling references. We'll discuss
these design strategies (page 812) later on.
Of course, you shouldn't try to dereference a dangling reference because your program
becomes erroneous, as we discuss in this section. Let's see an example:
4 procedure Show_Unchecked_Deallocation is
5
8 begin
9 I_1 := new Integer'(42);
10 I_2 := I_1;
11
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Unchecked_Deallocation
MD5: 8536190aa5bbafa715ad8153aaeb4889
Runtime output
I_1.all = 42
I_2.all = 42
Freeing I_1
I_2.all = 7443
In this example, we allocate an object for I_1 and make I_2 point to the same object. Then,
we call Free (I), which has the following consequences:
• The call to Free (I_1) will try to reclaim the storage for the original object (I_1.all),
so it may be reused for other allocations.
• I_1 = null after the call to Free (I_1).
• I_2 becomes a dangling reference by the call to Free (I_1).
– In other words, I_2 is still non-null, and what it points to is now undefined.
In principle, we could check for null before trying to dereference the access value. (Re-
member that when deallocating an object via a call to Free, the corresponding access value
is set to null.) In fact, this strategy works fine for I_1, but it doesn't work for I_2 because
the access value is not null. As a consequence, the application tries to dereference I_2.
Dereferencing a dangling reference is erroneous: the behavior is undefined in this case.
For the example we've just seen,
• I_2.all might make the application crash;
• I_2.all might give us a different value than before;
• I_2.all might even give us the same value as before (42) if the original object is still
available.
Because the effect is unpredictable, it might be really difficult to debug the application and
identify the cause.
Having dangling pointers in an application should be avoided at all costs! Again, it is the
programmer's responsibility to be very careful when using unchecked deallocation: avoid
creating dangling references!
3 procedure Show_Unchecked_Deallocation_Errors is
4
8 procedure Free is
9 new Ada.Unchecked_Deallocation
10 (Object => Integer,
11 Name => Integer_Access_Zero);
12
13 type Constant_Integer_Access is
14 access constant Integer;
15
23 I : Integer_Access_Zero;
24
25 begin
26 -- ERROR: Cannot allocate objects from
27 -- empty storage pool
28 I := new Integer;
29
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Unchecked_
↪Deallocation.Unchecked_Deallocation_Error
MD5: 5032d13b2eb6b7ca1979282ddd6df98a
Build output
Note: This section was originally written by Robert A. Duff and published as Gem #23:
Null Considered Harmful197 and Gem #24198 .
Ada, like many languages, defines a special null value for access types. All values of an
access type designate some object of the designated type, except for null, which does
not designate any object. The null value can be used as a special flag. For example, a
singly-linked list can be null-terminated. A Lookup function can return null to mean "not
found", presuming the result is of an access type:
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Null_And_Not_Null_
↪Access.Null_Return
MD5: 6c4eed750d42685198ec9495805e3e23
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Null_And_Not_Null_
↪Access.Not_Found_Exception
MD5: 6ef47b32d4923838ffc28f43e5db323c
Neither design is better in all situations; it depends in part on whether we consider the "not
found" situation to be exceptional.
Clearly, the client calling Lookup needs to know whether it can return null, and if so, what
that means. In general, it's a good idea to document whether things can be null or not,
especially for formal parameters and function results. Prior to Ada 2005, we would do that
with comments. Since Ada 2005, we can use the not null syntax:
197 https://www.adacore.com/gems/ada-gem-23
198 https://www.adacore.com/gems/ada-gem-24
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Null_And_Not_Null_
↪Access.Not_Null_Return
MD5: 4c0bb95da3b5a7c555a763c4951f7e21
13 Not_Found_2 : exception;
14 function Lookup_2 (T : Table)
15 return not null Ref_Element;
16 -- Raises Not_Found_2 if not found.
17
22 private
23 type Element is limited
24 record
25 Component : Integer;
26 end record;
27 type Table is limited null record;
28 end Example;
20 return An_Element'Access;
21 -- suppress error: 'missing "return"
22 -- statement in function body'
23 end Lookup_2;
24
37 procedure R is
38 begin
39 Q (An_Element'Access);
40 end R;
41
44 end Example;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Null_And_Not_Null_
↪Access.Complete_Null_Return
MD5: 01895c7d5f843fd215dcc21d807d4187
In general, it's better to use the language proper for documentation, when possible, rather
than comments, because compile-time and/or run-time checks can help ensure that the
"documentation" is actually true. With comments, there's a greater danger that the com-
ment will become false during maintenance, and false documentation is obviously a men-
ace.
In many, perhaps most cases, null is just a tripping hazard. It's a good idea to put in not
null when possible. In fact, a good argument can be made that not null should be the
default, with extra syntax required when null is wanted. This is the way Standard ML199
works, for example — you don't get any special null-like value unless you ask for it. Of
course, because Ada 2005 needs to be compatible with previous versions of the language,
not null cannot be the default for Ada.
One word of caution: access objects are default-initialized to null, so if you have a not
null object (or component) you had better initialize it explicitly, or you will get Con-
straint_Error. not null is more often useful on parameters and function results, for
this reason.
199 https://en.wikipedia.org/wiki/Standard_ML
Another advantage of not null over comments is for efficiency. Consider procedures P
and Q in this example:
7 end Example.Processing;
15 end Example.Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Null_And_Not_Null_
↪Access.Complete_Null_Return
MD5: dc34b1a27737d57c041be6260dd577fd
Without not null, the generated code for P will do a check that X /= null, which may be
costly on some systems. P is called in a loop, so this check will likely occur many times.
With not null, the check is pushed to the call site. Pushing checks to the call site is usually
beneficial because
1. the check might be hoisted out of a loop by the optimizer, or
2. the check might be eliminated altogether, as in the example above, where the compiler
knows that An_Element'Access cannot be null.
This is analogous to the situation with other run-time checks, such as array bounds checks:
6 procedure Process_Array
7 (X : in out My_Array;
8 Index : My_Index);
9
10 end Show_Process_Array;
3 procedure Process_Array
4 (X : in out My_Array;
5 Index : My_Index) is
6 begin
7 X (Index) := X (Index) + 1;
8 end Process_Array;
9
10 end Show_Process_Array;
MD5: 32424432f5b2e3013292680f92a04320
If X (Index) occurs inside Process_Array, there is no need to check that Index is in range,
because the check is pushed to the caller.
Previously, we learned about dangling references (page 795) and discussed the effects
of dereferencing them (page 805). Also, we've seen the relationship between unchecked
deallocation and dangling references (page 803). Ensuring that all calls to Free for a spe-
cific access type will never cause dangling references can become an arduous task — if not
impossible — if those calls are located in different parts of the source code.
Although we used access types directly in the main application in many of the previous
code examples from this chapter, this approach was in fact selected just for illustration
purposes — i.e. to make the code look simpler. In general, however, we should avoid this
approach. Instead, our recommendation is to encapsulate the access types in some form
of abstraction. In this section, we discuss design strategies for access types that take this
recommendation into account.
The simplest form of abstraction is of course an abstract data type. For example, we could
declare a limited private type, which allows us to hide the access type and to avoid copies of
references that could potentially become dangling references. (We discuss limited private
types later in another chapter (page 928).)
Let's see an example:
22 private
23
26 end Access_Type_Abstraction;
45 end Access_Type_Abstraction;
3 with Access_Type_Abstraction;
4 use Access_Type_Abstraction;
5
6 procedure Main is
7 Obj_1 : Info := To_Info ("hello");
8 Obj_2 : Info := Copy (Obj_1);
9 begin
10 Put_Line ("TO_INFO / COPY");
11 Put_Line ("Obj_1 : "
12 & To_String (Obj_1));
13 Put_Line ("Obj_2 : "
14 & To_String (Obj_2));
15 Put_Line ("----------");
16
17 Reset (Obj_1);
18 Append (Obj_2, " world");
19
30 Put_Line ("COPY");
31 Put_Line ("Obj_1 : "
32 & To_String (Obj_1));
33 Put_Line ("Obj_2 : "
34 & To_String (Obj_2));
35 Put_Line ("----------");
36
37 Destroy (Obj_1);
38 Destroy (Obj_2);
39
40 Put_Line ("DESTROY");
41 Put_Line ("Obj_1 : "
42 & To_String (Obj_1));
43 Put_Line ("Obj_2 : "
44 & To_String (Obj_2));
45 Put_Line ("----------");
46
49 Put_Line ("APPEND");
50 Put_Line ("Obj_1 : "
51 & To_String (Obj_1));
52 Put_Line ("----------");
53
54 Put_Line ("APPEND");
55 Append (Obj_1, " there");
(continues on next page)
59 Destroy (Obj_1);
60 Destroy (Obj_2);
61 end Main;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Design_Strategies.
↪Access_Type_Abstraction
MD5: d652d26314b616d3e1b955c0ce5bbbd7
Runtime output
TO_INFO / COPY
Obj_1 : hello
Obj_2 : hello
----------
RESET / APPEND
Obj_1 :
Obj_2 : hello world
----------
COPY
Obj_1 : hello world
Obj_2 : hello world
----------
DESTROY
Obj_1 :
Obj_2 :
----------
APPEND
Obj_1 : hey
----------
APPEND
Obj_1 : hey there
In this example, we hide an access type in the Info type — a limited private type. We
allocate an object of this type in the To_Info function and deallocate it in the Destroy
procedure. Also, we make sure that the reference isn't copied in the Copy function — we
only copy the designated value in this function. This strategy eliminates the possibility of
dangling references, as each reference is encapsulated in an object of Info type.
In the previous code example, the Destroy procedure had to be called to deallocate the
hidden access object. We could make sure that this deallocation happens automatically
by using a controlled (or limited controlled) type. (We discuss controlled types in another
chapter.)
Let's adapt the previous example and declare Info as a limited controlled type:
3 package Access_Type_Abstraction is
4
22 private
23
35 end Access_Type_Abstraction;
5 --
6 -- STRING_ACCESS SUBPROGRAMS
7 --
8
18 procedure Free is
19 new Ada.Unchecked_Deallocation
20 (Object => String,
21 Name => String_Access);
22
23 --
24 -- PRIVATE SUBPROGRAMS
25 --
26
47 --
48 -- PUBLIC SUBPROGRAMS
49 --
50
85 end Access_Type_Abstraction;
3 with Access_Type_Abstraction;
4 use Access_Type_Abstraction;
5
6 procedure Main is
7 Obj_1 : Info := To_Info ("hello");
8 Obj_2 : Info := Copy (Obj_1);
9 begin
10 --
11 -- TO_INFO / COPY
12 --
13 Put_Line ("TO_INFO / COPY");
14
21 --
22 -- RESET: Obj_1
23 -- APPEND: Obj_2
24 --
25 Put_Line ("RESET / APPEND");
26
27 Reset (Obj_1);
28 Append (Obj_2, " world");
29
36 --
37 -- COPY: Obj_2 => Obj_1
38 --
39 Put_Line ("COPY");
40
50 --
51 -- RESET: Obj_1, Obj_2
52 --
53 Put_Line ("RESET");
54
55 Reset (Obj_1);
56 Reset (Obj_2);
57
64 --
65 -- COPY: Obj_2 => Obj_1
66 --
67 Put_Line ("COPY");
68
78 --
79 -- APPEND: Obj_1 with "hey"
80 --
81 Put_Line ("APPEND");
82
89 --
90 -- APPEND: Obj_1 with "there"
91 --
92 Put_Line ("APPEND");
93
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Design_Strategies.
↪Access_Type_Limited_Controlled_Abstraction
MD5: e98659ad1b87be56fb173fa407ab7e82
Runtime output
TO_INFO / COPY
Obj_1 : hello
Obj_2 : hello
----------
RESET / APPEND
Obj_1 :
Obj_2 : hello world
----------
COPY
Obj_1 : hello world
Obj_2 : hello world
----------
RESET
(continues on next page)
Of course, because we're using the Limited_Controlled type from the Ada.
Finalization package, we had to adapt the prototype of the subprograms from the Ac-
cess_Type_Abstraction. In this version of the code, we only have the allocation taking
place in the To_Info procedure, but we don't have a Destroy procedure for deallocation:
this call was moved to the Finalize procedure.
Since objects of the Info type — such as Obj_1 in the Show_Access_Type_Abstraction pro-
cedure — are now controlled, the Finalize procedure is automatically called when they go
out of scope. In this procedure, which we override for the Info type, we perform the deal-
location of the internal access object Str_A. (You may uncomment the calls to Put_Line in
the body of the Initialize and Finalize subprograms to confirm that these subprograms
are called in the background.)
So far in this chapter, we focused mainly on access-to-objects. However, we can use access
types to subprograms. This is the topic of this section.
In a typical subprogram call, we indicate the subprogram we want to call statically. For
example, let's say we've implemented a procedure Proc that calls a procedure P:
3 procedure Proc is
4 I : Integer := 0;
5 begin
6 P (I);
7 end Proc;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Subprogram_Call
MD5: 0e9547e53d0d02d39920f4d1d6787af6
The call to P is statically dispatched: every time Proc runs and calls P, that call is always to
the same procedure. In other words, we can determine at compilation time which procedure
is called.
In contrast, an access to a subprogram allows us to dynamically indicate which subprogram
we want to call. For example, if we change Proc in the code above to receive the access to
a subprogram P as a parameter, the actual procedure that would be called when running
Proc would be determined at run time, and it might be different for every call to Proc. In
this case, we wouldn't be able to determine at compilation time which procedure would be
called in every case. (In some cases, however, it could still be possible to determine which
procedure is called by analyzing the argument that is passed to Proc.)
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 end Access_To_Subprogram_Types;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 5f834c1b2044ba5ea7d4835c3ebdedb1
In the designated profile of the access type declarations, we list all the parameters that we
expect in the subprogram.
We can use those types to declare access to subprograms — as subprogram parameters,
for example:
4 package Access_To_Subprogram_Params is
5
8 end Access_To_Subprogram_Params;
10 end Access_To_Subprogram_Params;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 17c1a07f48d9fb0efef37aa4c5ec8a51
In the implementation of the Proc procedure of the code example, we call the P procedure
by simply passing I as a parameter. In this case, P is automatically dereferenced. We may,
however, explicitly dereference P by writing P.all (I).
Before we use this package, let's implement a simple procedure that we'll use later on:
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 8553ad7329bf1ed727147b47b7355a70
Now, we can get access to a subprogram by using the Access attribute and pass it as an
actual parameter:
4 with Add_Ten;
5
6 procedure Show_Access_To_Subprograms is
7 begin
8 Proc (Add_Ten'Access);
9 -- ^ Getting access to Add_Ten
10 -- procedure and passing it
11 -- to Proc
12 end Show_Access_To_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 599e9d1306da48e3c532692b34c02a1d
Here, we get access to the Add_Ten procedure and pass it to the Proc procedure.
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 697959b806f6f2bfba248ec15c47883b
In addition to Add_Ten, we've implemented the Add_Twenty procedure, which we use in our
extended test application:
4 with Access_To_Subprogram_Params;
5 use Access_To_Subprogram_Params;
6
7 with Add_Ten;
8 with Add_Twenty;
9
10 procedure Show_Access_To_Subprograms is
11 P : Access_To_Procedure;
12 Some_Int : Integer := 0;
13 begin
14 P := Add_Ten'Access;
15 -- ^ Getting access to Add_Ten
16 -- procedure and assigning it
(continues on next page)
200 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
19 Proc (P);
20 -- ^ Passing access-to-subprogram as an
21 -- actual parameter
22
23 P (Some_Int);
24 -- ^ Using access-to-subprogram object in a
25 -- subprogram call
26
27 P := Add_Twenty'Access;
28 -- ^ Getting access to Add_Twenty
29 -- procedure and assigning it
30 -- to P
31
32 Proc (P);
33 P (Some_Int);
34 end Show_Access_To_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 7b4ea19187806e88ba65847876cafb4f
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 type Access_To_Procedure_Array is
10 array (Positive range <>) of
11 Access_To_Procedure;
12
13 type Access_To_Function_Array is
14 array (Positive range <>) of
15 Access_To_Function;
16
25 end Access_To_Subprogram_Types;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 32203838b97af66ef6ca3f6b1ce646a5
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 with Add_Ten;
7 with Add_Twenty;
8
9 procedure Show_Access_To_Subprograms is
10 PA : constant
11 Access_To_Procedure_Array (1 .. 2) :=
12 (Add_Ten'Access,
13 Add_Twenty'Access);
14
15 Some_Int : Integer := 0;
16 begin
17 Put_Line ("Some_Int: " & Some_Int'Image);
18
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: f1d10056b4b3424bd30d954f34caa255
Runtime output
Some_Int: 0
Some_Int: 10
Some_Int: 30
Here, we declare the PA array and use the access to the Add_Ten and Add_Twenty proce-
dures as its components. We can call any of these procedures by simply specifying the
index of the component, e.g. PA (2). Once we specify the procedure we want to use, we
simply pass the parameters, e.g.: PA (2) (Some_Int).
Now, let's use the Rec_Access_To_Procedure record type in a test application:
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 with Add_Ten;
7 with Add_Twenty;
8
9 procedure Show_Access_To_Subprograms is
10 RA : Rec_Access_To_Procedure;
11 Some_Int : Integer := 0;
12 begin
13 Put_Line ("Some_Int: " & Some_Int'Image);
14
MD5: 4b23b5f6a8c252a1a014a2b54fa32c1a
Runtime output
Some_Int: 0
Some_Int: 10
Some_Int: 30
Here, we declare two record aggregates where we specify the AP component, e.g.: (AP =>
Add_Ten'Access), which indicates the access-to-subprogram we want to use. We can call
the subprogram by simply accessing the AP component, i.e.: RA.AP.
As you might expect, we can use access-to-subprogram types when declaring discrimi-
nants. In fact, when we were talking about discriminants as access values (page 745)
earlier on, we used access-to-object types in our code examples, but we could have used
access-to-subprogram types as well. For example:
19 private
20
26 end Custom_Processing;
25 end Custom_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 02fc0c51722c321c4ec6115de68d1c06
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: cd43fa39dac9a1c9182f69d32eab1d26
5 with Add_Ten;
6 with Mult_Two;
7
8 procedure Show_Access_To_Subprogram_Discriminants
9 is
10
21 begin
22 Init (R_Add_Ten, 1);
23 Init (R_Mult_Two, 2);
24
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 544c224f8bc8e6ba2db4914c2a3dcff4
Runtime output
---- R_Add_Ten ----
R.I = 1
Calling Process procedure...
R.I = 11
---- R_Mult_Two ----
R.I = 2
Calling Process procedure...
R.I = 4
In this procedure, we declare the R_Add_Ten and R_Mult_Two of Rec type and specify the
access to Add_Ten and Mult_Two, respectively, as the IP discriminant. The procedure we
specified here is then called inside a call to the Process procedure.
We can use access-to-subprograms types when declaring formal parameters. For example,
let's revisit the Custom_Processing package from the previous section and convert it into
a generic package.
4 --
5 -- Declaring formal access-to-subprogram
6 -- type:
7 --
8 type T_Processing is
9 access procedure (Element : in out T);
10
11 --
12 -- Declaring formal access-to-subprogram
13 -- parameter:
14 --
15 Proc : T_Processing;
16
30 private
31
36 end Gen_Custom_Processing;
22 end Gen_Custom_Processing;
MD5: 6f06e066bafa5f02abb3ee1b33ea0831
In this version of the procedure, instead of declaring Proc as a discriminant of the Rec
record, we're declaring it as a formal parameter of the Gen_Custom_Processing package.
Also, we're declaring an access-to-subprogram type (T_Processing) as a formal parameter.
(Note that, in contrast to these two parameters that we've just mentioned, Image_T is not
a formal access-to-subprogram parameter: it's actually just a formal subprogram.)
We then instantiate the Gen_Custom_Processing package in our test application:
3 with Add_Ten;
4
7 procedure
8 Show_Access_To_Subprogram_As_Formal_Parameter
9 is
10 type Integer_Processing is
11 access procedure (I : in out Integer);
12
25 R_Add_Ten : Rec;
26
27 begin
28 Init (R_Add_Ten, 1);
29
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_To_Subprogram_Types
MD5: 6ae27ebd59e5307551e9a38f3b94c70c
Runtime output
Selecting subprograms
3 type Data_Container is
4 array (Positive range <>) of Float;
5
6 type Log_Procedure is
7 access procedure (D : Data_Container);
8
9 procedure Process
10 (D : in out Data_Container;
11 Log_Proc : Log_Procedure := null);
(continues on next page)
13 end Data_Processing;
3 procedure Process
4 (D : in out Data_Container;
5 Log_Proc : Log_Procedure := null) is
6 begin
7 -- missing processing part...
8
14 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Log_Procedure
MD5: 59399e0809deb476f608faab7e4398bd
In the implementation of Process, we check whether Log_Proc is null or not. (If it's not
null, we call the procedure. Otherwise, we just skip the call.)
Now, let's implement two logging procedures that match the expected form of the
Log_Procedure type:
4 procedure Log_Element_Per_Line
5 (D : Data_Container) is
6 begin
7 Put_Line ("Elements: ");
8 for V of D loop
9 Put_Line (V'Image);
10 end loop;
11 Put_Line ("------");
12 end Log_Element_Per_Line;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Log_Procedure
MD5: 468789f7331ffcd16f754f7116b076d7
Finally, we implement a test application that selects each of the logging procedures that
we've just implemented:
4 with Log_Element_Per_Line;
5 with Log_Csv;
6
7 procedure Show_Access_To_Subprograms is
8 D : Data_Container (1 .. 5) := (others => 1.0);
9 begin
10 Put_Line ("==== Log_Element_Per_Line ====");
11 Process (D, Log_Element_Per_Line'Access);
12
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Log_Procedure
MD5: 134aa682cea1999efa0ea97052f315c8
Runtime output
Here, we use the Access attribute to get access to the Log_Element_Per_Line and Log_Csv
procedures. Also, in the third call, we don't pass any access as an argument, which is then
null by default.
Null exclusion
We can use null exclusion when declaring an access to subprograms. By doing so, we
ensure that a subprogram must be specified — either as a parameter or when initializing
an access object. Otherwise, an exception is raised. Let's adapt the previous example and
introduce the Init_Function type:
3 type Data_Container is
4 array (Positive range <>) of Float;
5
6 type Init_Function is
7 not null access function return Float;
8
9 procedure Process
10 (D : in out Data_Container;
11 Init_Func : Init_Function);
12
13 end Data_Processing;
3 procedure Process
4 (D : in out Data_Container;
5 Init_Func : Init_Function) is
6 begin
7 for I in D'Range loop
8 D (I) := Init_Func.all;
9 end loop;
10 end Process;
11
12 end Data_Processing;
In this case, we specify that Init_Function is not null access because we want to always
be able to call this function in the Process procedure (i.e. without raising an exception).
When an access to a subprogram doesn't have parameters — which is the case for the
subprograms of Init_Function type — we need to explicitly dereference it by writing .
all. (In this case, .all isn't optional.) Therefore, we have to write Init_Func.all in the
implementation of the Process procedure of the code example.
Now, let's declare two simple functions — Init_Zero and Init_One — that return 0.0 and
1.0, respectively:
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_Init_Function
MD5: 444110d50ddb430fd5be31cf1b417fc8
Finally, let's see a test application where we select each of the init functions we've just
implemented:
4 procedure Log_Element_Per_Line
5 (D : Data_Container) is
6 begin
7 Put_Line ("Elements: ");
8 for V of D loop
9 Put_Line (V'Image);
10 end loop;
11 Put_Line ("------");
12 end Log_Element_Per_Line;
4 with Init_Zero;
5 with Init_One;
6
7 with Log_Element_Per_Line;
8
9 procedure Show_Access_To_Subprograms is
10 D : Data_Container (1 .. 5) := (others => 1.0);
11 begin
12 Put_Line ("==== Init_Zero ====");
13 Process (D, Init_Zero'Access);
14 Log_Element_Per_Line (D);
15
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Access_Init_Function
MD5: ae0e3fd58e9bb83061248967c709190a
Runtime output
Here, we use the Access attribute to get access to the Init_Zero and Init_One functions.
Also, if we uncomment the call to Process with null as an argument for the init function, we
see that the Constraint_Error exception is raised at run time — as the argument cannot
be null due to the null exclusion.
Note: This example was originally written by Robert A. Duff and was part of the Gem
#24201 .
10 type Iterate_Action is
11 access procedure
12 (X : not null Ref_Element);
13
14 procedure Iterate
15 (T : Table;
16 Action : Iterate_Action := null);
17 -- If Action is null, do nothing.
18
19 end Show_Null_Procedure;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Null_Procedure
MD5: ac21dd76ed9fb7f26839c24210cf4425
10 procedure Do_Nothing
11 (X : not null Ref_Element) is null;
12
13 type Iterate_Action is
14 access procedure
15 (X : not null Ref_Element);
16
17 procedure Iterate
18 (T : Table;
19 Action : not null Iterate_Action
20 := Do_Nothing'Access);
21
22 end Show_Null_Procedure;
MD5: 7341d8f23cd4efe45698481be452a9e8
The style of the second Iterate is clearly better because it makes use of the syntax to indi-
cate that a procedure is expected. This is a complete package that includes both versions
of the Iterate procedure:
8 type Iterate_Action is
9 access procedure
10 (X : not null Ref_Element);
11
12 procedure Iterate
13 (T : Table;
14 Action : Iterate_Action := null);
15 -- If Action is null, do nothing.
16
17 procedure Do_Nothing
18 (X : not null Ref_Element) is null;
19 procedure Iterate_2
(continues on next page)
24 private
25 type Element is limited
26 record
27 Component : Integer;
28 end record;
29 type Table is limited null record;
30 end Example;
5 procedure Iterate
6 (T : Table;
7 Action : Iterate_Action := null)
8 is
9 begin
10 if Action /= null then
11 Action (An_Element'Access);
12 -- In a real program, this would do
13 -- something more sensible.
14 end if;
15 end Iterate;
16
17 procedure Iterate_2
18 (T : Table;
19 Action : not null Iterate_Action
20 := Do_Nothing'Access)
21 is
22 begin
23 Action (An_Element'Access);
24 -- In a real program, this would do
25 -- something more sensible.
26 end Iterate_2;
27
28 end Example;
3 procedure Show_Example is
4 T : Table;
5 begin
6 Iterate_2 (T);
7 end Show_Example;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Complete_Not_Null_Procedure
MD5: ab0a41e0d39a8a16b0b69f8c6b2a43fd
Writing not null Iterate_Action might look a bit more complicated, but it's worthwhile,
and anyway, as mentioned earlier, the compatibility requirement requires that the not null
Up to this point, we've discussed access to normal Ada subprograms. In some situations,
however, we might want to have access to protected subprograms. To do this, we can
simply declare a type using access protected:
3 type Access_Proc is
4 access protected procedure;
5
6 protected Obj is
7
8 procedure Do_Something;
9
10 end Obj;
11
14 end Simple_Protected_Access;
5 procedure Do_Something is
6 begin
7 -- Not doing anything
8 -- for the moment...
9 null;
10 end Do_Something;
11
12 end Obj;
13
14 end Simple_Protected_Access;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Simple_Protected_Access
MD5: d82f7c90355e9810bd1e35f65e278626
Here, we declare the Access_Proc type as an access type to protected procedures. Then,
we declare the variable Acc and assign to it the access to the Do_Something procedure (of
the protected object Obj).
Now, let's discuss a more useful example: a simple system that allows us to register pro-
tected procedures and execute them. This is implemented in Work_Registry package:
5 type Work_Handler is
6 access protected procedure (T : Work_Id);
7
8 subtype Valid_Work_Handler is
9 not null Work_Handler;
10
11 type Work_Handlers is
12 array (Positive range <>) of Work_Handler;
13
20 procedure Reset;
21
22 procedure Process_All;
23
24 private
25
26 D : Work_Handlers (1 .. Last);
27 Curr : Natural := 0;
28
29 end Work_Handler_Registry;
30
31 private
32
35 end Work_Registry;
14 procedure Reset is
15 begin
16 Curr := 0;
17 end Reset;
18
19 procedure Process_All is
20 Dummy_ID : Work_Id;
21 begin
22 for I in D'First .. Curr loop
23 D (I).all (Dummy_ID);
24 end loop;
25 end Process_All;
(continues on next page)
27 end Work_Handler_Registry;
28
29 end Work_Registry;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Protected_Access_Init_Function
MD5: 5dfa8ab098900ab4f6b7575e1cde5e53
Here, we declare the protected Work_Handler_Registry type with the following subpro-
grams:
• Register, which we can use to register a protected procedure;
• Reset, which we can use to reset the system; and
• Process_All, which we can use to call all procedures that were registered in the sys-
tem.
Work_Handler is our access to protected subprogram type. Also, we declare the
Valid_Work_Handler subtype, which excludes null. By doing so, we can ensure
that only valid procedures are passed to the Register procedure. In the protected
Work_Handler_Registry type, we store the procedures in an array (of Work_Handlers
type).
Important
Note that, in the type declaration Work_Handler, we say that the protected procedure must
have a parameter of Work_Id type. In this example, this parameter is just used to bind the
procedure to the Work_Handler_Registry type. The Work_Id type itself is actually declared
as a null record (in the private part of the package), and it isn't really useful on its own.
If we had declared type Work_Handler is access protected procedure; instead, we
would be able to register any protected procedure into the system, even the ones that might
not be suitable for the system. By using a parameter of Work_Id type, however, we make
use of strong typing to ensure that only procedures that were designed for the system can
be registered.
In the next part of the code, we declare the Integer_Storage type, which is a simple
protected type that we use to store an integer value:
3 package Integer_Storage_System is
4
11 private
12
13 I : Integer := 0;
14
15 end Integer_Storage;
(continues on next page)
17 type Integer_Storage_Access is
18 access Integer_Storage;
19
20 type Integer_Storage_Array is
21 array (Positive range <>) of
22 Integer_Storage_Access;
23
24 end Integer_Storage_System;
19 end Integer_Storage;
20
21 end Integer_Storage_System;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Protected_Access_Init_Function
MD5: a388d792bc85709785d324c914d9d236
4 with Integer_Storage_System;
5 use Integer_Storage_System;
6
7 procedure Show_Access_To_Protected_Subprograms is
8
12 begin
13 -- Allocate and initialize integer storage
14 --
15 -- (For the initialization, we're just
16 -- assigning the index here, but we could
17 -- really have used any integer value.)
18
24 -- Register handlers
25
34 WHR.Process_All;
35
36 end Show_Access_To_Protected_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Protected_Access_Init_Function
MD5: 44c24ef07333e1d31844cc2ea6d91ab6
Runtime output
Value: 1
Value: 2
Value: 3
The work handler registry (WHR) has a maximum capacity of five procedures, whereas the
Int_Stor array has a capacity of three elements. By calling WHR.Register and passing
Int_Stor (I).all.Show'Access, we register the Show procedure of each protected object
from Int_Stor.
Important
Note that the components of the Int_Stor array are of Integer_Storage_Access type,
which is declared as an access to Integer_Storage objects. Therefore, we have to derefer-
ence the object (by writing Int_Stor (I).all) before getting access to the Show procedure
(by writing .Show'Access).
We have to use an access type here because we cannot pass the access (to the Show
procedure) of a local object in the call to the Register procedure. Therefore, the protected
objects (of Integer_Storage type) cannot be local.
This issue becomes evident if we replace the declaration of Int_Stor with a local array
(and then adapt the remaining code). If we do this, we get a compilation error in the call to
Register:
4 with Integer_Storage_System;
5 use Integer_Storage_System;
6
7 procedure Show_Access_To_Protected_Subprograms
8 is
9 WHR : Work_Handler_Registry (5);
10
13 begin
14 -- Allocate and initialize integer storage
15 --
16 -- (For the initialization, we're just
17 -- assigning the index here, but we could
18 -- really have used any integer value.)
19
25 -- Register handlers
26
36 WHR.Process_All;
37
38 end Show_Access_To_Protected_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_To_
↪Subprograms.Protected_Access_Init_Function
MD5: 359241c84cd30313fe2d7701b55f303e
Build output
As we've just discussed, this error is due to the fact that Int_Stor is now a "local" protected
object, and the accessibility rules don't allow mixing it with non-local accesses in order to
prevent the possibility of dangling references.
When we call WHR.Process_All, the registry system calls each procedure that has been
registered with the system. When looking at the values displayed by the test application,
we may notice that each call to Show is referring to a different protected object. In fact,
even though we're passing just the access to a protected procedure in the call to Register,
that access is also associated to a specific protected object. (This is different from access
to non-protected subprograms we've discussed previously: in that case, there's no object
associated.) If we replace the argument to Register by Int_Stor (2).all.Show'Access,
for example, the three Show procedures registered in the system will now refer to the same
protected object (stored at Int_Stor (2)).
Also, even though we have registered the same procedure (Show) of the same type
(Integer_Storage) in all calls to Register, we could have used a different protected pro-
cedure — and of a different protected type. As an exercise, we could, for example, create a
new type called Float_Storage (based on the code that we used for the Integer_Storage
type) and register some objects of Float_Storage type into the system (with a couple of
additional calls to Register). If we then call WHR.Process_All, we'd see that the system is
able to cope with objects of both Integer_Storage and Float_Storage types. In fact, the
system implemented with the Work_Handler_Registry can be seen as "type agnostic," as
it doesn't care about which type the protected objects have — as long as the subprograms
we want to register are conformant to the Valid_Work_Handler type.
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 procedure Show_Access_To_Subprogram_Error is
7 Func : Access_To_Function;
8
9 Value : Integer := 0;
10 begin
11 declare
12 function Add_One (I : Integer)
13 return Integer is
14 (I + 1);
15 begin
16 Func := Add_One'Access;
17 -- This assignment is illegal because the
(continues on next page)
MD5: 2a068732606a1fee156e82515febe9c4
Build output
show_access_to_subprogram_error.adb:16:15: error: subprogram must not be deeper␣
↪than access type
Obviously, we can correct this error by putting the Add_One function at the same level as
the Access_To_Function type, i.e. at library level:
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 with Add_One;
7
8 procedure Show_Access_To_Subprogram_Error is
9 Func : Access_To_Function;
10
11 Value : Integer := 0;
(continues on next page)
MD5: 7f7488c541fb457ced653a2e6cc2fad1
Runtime output
Value: 0
Value: 1
Unchecked Access
Previously, we discussed about the Unchecked_Access attribute (page 798), which we can
use to circumvent accessibility issues in specific cases for access-to-objects. We also said
in that section that this attribute only exists for objects, not for subprograms. We can use
the previous example to illustrate this limitation:
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 procedure Show_Access_To_Subprogram_Error is
7 Func : Access_To_Function;
8
13 Value : Integer := 0;
14 begin
15 Func := Add_One'Access;
(continues on next page)
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_Rules_
↪Access_To_Subprograms.Access_To_Subprogram_Accessibility_Error_Same_Lifetime
MD5: c1ee1946f0c979eb30fbf2c72c426f50
Build output
3 type Access_To_Procedure is
4 access procedure (I : in out Integer);
5
6 type Access_To_Function is
7 access function (I : Integer) return Integer;
8
9 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 procedure Show_Access_To_Subprogram_Error is
7 Func : Access_To_Function;
8
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_Rules_
↪Access_To_Subprograms.Unrestricted_Access
MD5: 90e2c57c01463cbe6efee6e093d01e5b
Runtime output
Value: 0
Value: 1
As we can see, the Unrestricted_Access attribute can be safely used in this specific case
to circumvent the accessibility rule limitation.
As we know, an access type is not a pointer, and it doesn't just indicate an address in
memory. In fact, to represent an address in Ada, we use the Address type (page 401).
Also, as we discussed earlier, we can use operators such as <, >, + and - for addresses. In
contrast to that, those operators aren't available for access types — except, of course, for
= and /=.
In certain situations, however, we might need to convert between access types and ad-
dresses. In this section, we discuss how to do so.
202 http://www.ada-auth.org/standards/22rm/html/RM-13-3.html
203 http://www.ada-auth.org/standards/22rm/html/RM-13-7.html
3 with System.Address_To_Access_Conversions;
4 with System.Address_Image;
5
6 procedure Show_Address_Conversion is
7
8 package Integer_AAC is
9 new System.Address_To_Access_Conversions
10 (Object => Integer);
11 use Integer_AAC;
12
13 subtype Integer_Access is
14 Integer_AAC.Object_Pointer;
15 -- This is similar to:
16 --
17 -- type Integer_Access is access all Integer;
18
19 I : aliased Integer := 5;
20 AI : Integer_Access := I'Access;
21 begin
22 Put_Line ("I'Address : "
23 & System.Address_Image (I'Address));
24
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Address.
↪Address_Conversion
MD5: 717532026247044a667b60f6c1e1c7da
Runtime output
I'Address : 00007FFC0FE22A04
AI.all'Address : 00007FFC0FE22A04
To_Address (AI) : 00007FFC0FE22A04
4 with System.Address_To_Access_Conversions;
5 with System.Address_Image;
6
7 procedure Show_Address_Conversion is
8
9 package Integer_AAC is
10 new System.Address_To_Access_Conversions
11 (Object => Integer);
12 use Integer_AAC;
13
14 subtype Integer_Access is
15 Integer_AAC.Object_Pointer;
16
17 I : aliased Integer := 5;
18 AI_1, AI_2 : Integer_Access;
19 A : Address;
20 begin
21 AI_1 := I'Access;
22 A := To_Address (AI_1);
23 AI_2 := To_Pointer (A);
24
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Address.
↪Address_Conversion
MD5: 5c6fc19ca1aa227feba97ea610dd9218
Runtime output
AI_1.all'Address : 00007FFF08F33EEC
AI_2.all'Address : 00007FFF08F33EEC
AI_1 = AI_2
Here, we convert the A address back to an access value by calling To_Pointer (A). (When
running this object, we see that AI_1 and AI_2 have the same access value.)
Note that the conversions might not work in all cases. For instance, when the
designated type — indicated by the formal Object parameter of the generic Ad-
dress_To_Access_Conversions package — is unbounded, the result of a call to
To_Pointer may not have bounds.
Let's adapt the previous code example and replace the Integer type by the (unbounded)
String type:
4 with System.Address_To_Access_Conversions;
5 with System.Address_Image;
6
7 procedure Show_Address_Conversion is
8
9 package String_AAC is
10 new System.Address_To_Access_Conversions
11 (Object => String);
12 use String_AAC;
13
14 subtype Integer_Access is
15 String_AAC.Object_Pointer;
16
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Access_Address.
↪Address_Conversion
MD5: b1adcaa1f2cb4dfbd157aebf7893bd72
Build output
Runtime output
AI_1.all'Address : 00007FFECD8849C8
AI_2.all'Address : 00007FFECD8849C8
AI_1 = AI_2
AI_1: Hello
AI_2: Hello
In this case, the call to To_Pointer (A) might not have bounds, so any operation on AI_2
might lead to unpredictable results.
The previous chapter dealt with access type declarations such as this one:
In addition to named access type declarations such as the one in this example, Ada also
supports anonymous access types, which, as the name implies, don't have an actual type
declaration.
To declare an access object of anonymous type, we just specify the subtype of the object
or subprogram we want to have access to. For example:
When we compare this example with the previous one, we see that the declaration A :
Integer_Access becomes A : access Integer. Here, access Integer is the anonymous
access type declaration, and A is an access object of this anonymous type.
To be more precise, A : access Integer is an access parameter (page 877) and it's specify-
ing an anonymous access-to-object type (page 858). Another flavor of anonymous access
types are anonymous access-to-subprograms (page 901). We discuss all these topics in
more details later.
204 http://www.ada-auth.org/standards/22rm/html/RM-13-7-2.html
3 procedure Show_Anonymous_Access_Types is
4 I_Var : aliased Integer;
5
6 A : access Integer;
7 -- ^ Anonymous access type
8 begin
9 A := I_Var'Access;
10 -- ^ Assignment to object of
11 -- anonymous access type.
12
13 A.all := 22;
14
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Types.Simple_Anonymous_Access_Types
MD5: f0c92c76d970089c1d503c599d6869dd
Runtime output
A.all: 22
Here, A is an access object whose value is initialized with the access to I_Var. Because the
declaration of A includes the declaration of an anonymous access type, we don't declare
an extra Integer_Access type, as we did in previous code examples.
Anonymous access types were not part of the first version of the Ada standard, which only
had support for named access types. They were introduced later to cover some use-cases
that were difficult — or even impossible — with access types.
In this sense, anonymous access types aren't just access types without names. Certain
accessibility rules for anonymous access types are a bit less strict. In those cases, it might
be interesting to consider using them instead of named access types.
In general, however, we should only use anonymous access types in those specific cases
where using named access types becomes too cumbersome. As a general recommenda-
tion, we should give preference to named access types whenever possible. (Anonymous
access-to-object types have drawbacks that we discuss later (page 860).)
205 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
One of the main benefits of anonymous access types is their flexibility: since there isn't
an explicit access type declaration associated with them, we only have to worry about the
subtype S we intend to access.
Also, as long as the subtype S in a declaration access S is always the same, no conversion
is needed between two access objects of that anonymous type, and the S'Access attribute
always works.
Let's see an example:
3 procedure Show_Anonymous_Access_Types is
4 I_Var : aliased Integer;
5 A : access Integer;
6 B : access Integer;
7 begin
8 A := I_Var'Access;
9 B := A;
10
11 A.all := 22;
12
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Types.Anonymous_Access_Object_Assignment
MD5: 2822ca0bd6ac251dccc1ced60747fbe1
Runtime output
A.all: 22
B.all: 22
In this example, we have two access objects A and B. Since they're objects of anonymous
access types that refer to the same subtype Integer, we can assign A to B without a type
conversion, and pass those access objects as an argument to the Show procedure.
(Note that the use of an access parameter in the Show procedure is for demonstration pur-
pose only: a simply Integer as the type of this input parameter would have been more than
sufficient to implement the procedure. Actually, in this case, avoiding the access parameter
would be the recommended approach in terms of clean Ada software design.)
In contrast, if we had used named type declarations, the code would be more complicated
and more limited:
8 end Aux;
12 end Aux;
3 procedure Show_Anonymous_Access_Types is
4 -- I_Var : aliased Integer;
5
6 A : Integer_Access;
7 B : Integer_Access;
8 begin
9 -- A := I_Var'Access;
10 -- ^ ERROR: non-local pointer cannot
11 -- point to local object.
12
13 A := new Integer;
14 B := A;
15
16 A.all := 22;
17
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Types.Anonymous_Access_Object_Assignment
MD5: 681c2cf7f5e8d520490cc5594484ce69
Runtime output
A.all: 22
B.all: 22
Here, apart from the access type declaration (Integer_Access), we had to make two adap-
Important
As mentioned earlier, if we want to use two access objects in an operation, the
rule says that the subtype S of the anonymous type used in their corresponding
declaration must match. In the following example, we can see how this rule
works:
10 B := A;
11 -- ^ ERROR: subtype doesn't match!
12
13 B := I_Var'Access;
14 -- ^ ERROR: subtype doesn't match!
15 end Show_Anonymous_Access_Subtype_Error;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Anonymous_Access_Types.Anonymous_Access_Subtype_Error
MD5: cecfe703ea8b42bad61c45f33cbcb67b
Build output
The later sections on anonymous access-to-object type (page 858) and anonymous access-
to-subprograms (page 901) cover more specific details on anonymous access types.
In the previous chapter (page 735), we introduced named access-to-object types and used
those types throughout the chapter. Also, in the previous section (page 853), we've seen
some simple examples of anonymous access-to-object types:
A : access Integer;
-- ^ Anonymous access type
In addition to parameters and objects, we can use anonymous access types in discriminants,
components of array and record types, renamings and function return types. (We discuss
anonymous access discriminants (page 868) and anonymous access parameters (page 877)
later on.) Let's see a code example that includes all these cases:
6 AI : access Integer;
7 -- ^ Anonymous access type
8
12 type Access_Array is
13 array (Positive range <>) of
14 access Integer;
15 -- ^ Anonymous access type
16
23 function Init_Access_Integer
24 return access Integer is (null);
25 -- ^ Anonymous access type
26
27 private
28
34 end record;
35
36 end All_Anonymous_Access_To_Object_Types;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.All_Anonymous_Access_To_Object_Types
MD5: 6533b22a4e4526702320cb327bf6f69a
As expected, null is a valid value for an anonymous access type. However, we can forbid
null as a valid value by using not null in the anonymous access type declaration. For
example:
7 I : aliased Integer;
8
18 type Access_Array is
19 array (Positive range <>) of
20 not null access Integer;
21 -- ^ Anonymous access type
22
23 Arr : array (1 .. 5) of
24 not null access Integer :=
25 -- ^ Anonymous access type
26 (others => I'Access);
27 -- ^^^^^^^^^^^^^^^^^^
28 -- Initialization required!
29
34 function Init_Access_Integer
35 return not null access Integer is (I'Access);
36 -- ^ Anonymous access type
37 -- ^^^^^^^^
38 -- Initialization required!
39
40 private
41
48 end record;
49
50 end All_Anonymous_Access_To_Object_Types;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.All_Not_Null_Anonymous_Access_To_Object_Types
MD5: 027430aa9d5e19979206110f5e260d13
As you might have noticed, we took the previous code example and used not null for
each usage instance of the anonymous access type. In this sense, this version of the code
example is very similar to the previous one. Note, however, that we now have to explicitly
initialize some elements to avoid the Constraint_Error exception being raised at runtime.
This is the case for example for the AI access object:
If we hadn't initialized AI explicitly with I'Access, it would have been set to null, which
would fail the not null constraint of the anonymous access type. Similarly, we also have
to initialize the Arr array and return a valid access object for the Init_Access_Integer
function.
Anonymous access-to-object types have important drawbacks. For example, some features
that are available for named access types aren't available for the anonymous access types.
Also, most of the drawbacks are related to how anonymous access-to-object types can
potentially make the allocation and deallocation quite complicated or even error-prone.
For starters, some pool-related features aren't available for anonymous access-to-object
types. For example, we cannot specify which pool is going to be used in the allocation of
an anonymous access-to-object. In fact, the memory pool selection is compiler-dependent,
so we cannot rely on an object being allocated from a specific pool when using new with an
anonymous access-to-object type. (In contrast, as we know, each named access type has
an associated pool, so objects allocated via new will be allocated from that pool.) Also, we
cannot identify which pool was selected for the allocation of a specific object, so we don't
have any information to use for the deallocation of that object.
Because the pool selection is hidden from us, this makes the memory deallocation more
complicated. For example, we cannot instantiate the Ada.Unchecked_Deallocation pro-
cedure for anonymous access types. Also, some of the methods we could use to circumvent
this limitation are error-prone, as we discuss in this section.
Also, storage-related features aren't available: specifying the storage size — especially,
specifying that the access type has a storage size of zero — isn't possible.
Missing features
Let's see a code example that shows some of the features that aren't available for anony-
mous access-to-object types:
3 package Missing_Features is
4
11 --
12 -- All the features below aren't available
13 -- for an anonymous access-to-object:
14 --
15
23 type Integer_Access is
24 access Integer
25 with Storage_Pool =>
26 String_Access'Storage_Pool;
27 -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 -- Using the pool from another
29 -- access type.
30
50 end Missing_Features;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Missing_Anonymous_Access_To_Object_Features
MD5: 87a5c1413a720da84fab414cf63236ec
In the Missing_Features package, we see some of the features that we cannot use for
the anonymous access Integer type, but that are available for equivalent named access
types:
• There's no specific memory pool associated with the access object IA. In contrast,
named types — such as String_Access and Integer_Access — have an associated
pool, and we can use the Storage_Pool aspect and the Storage_Pool attribute to
customize them.
• We cannot instantiate the Ada.Unchecked_Deallocation procedure for the access
Integer type. However, we can instantiate it for named access types such as the
Integer_Access type.
• We cannot use the Storage_Size attribute for the access Integer type, but we're
allowed to use it with named access types, which we do in the declaration of the
Integer_Access_Store_128 and Integer_Access_Store_0 types.
We might think that we could make up for the absence of the Ada.
Unchecked_Deallocation procedure for anonymous access-to-object types by converting
those access objects (of anonymous access types) to a named type that has the same
designated subtype. For example, if we have an access object IA of an anonymous access
Integer type, we can convert it to the named Integer_Access type, provided this named
access type is compatible with the anonymous access type, e.g.:
3 procedure Show_Dangerous_Deallocation is
4 type Integer_Access is
5 access all Integer;
6
7 procedure Free is
8 new Ada.Unchecked_Deallocation
9 (Object => Integer,
10 Name => Integer_Access);
11
12 IA : access Integer;
13 begin
14 IA := new Integer;
15 IA.all := 30;
16
21 end Show_Dangerous_Deallocation;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Deallocation_Anonymous_Access_To_Object_Erronoeus
MD5: 91e024a4338e2e4f8d5b308d95499c1c
This example declares the IA access object of the anonymous access Integer type. After
allocating an object for IA via new, we try to deallocate it by first converting it to the Inte-
ger_Access type, so that we can call the Free procedure to actually deallocate the object.
Although this code compiles, it'll only work if both access Integer and Integer_Access
types are using the same memory pool. Since we cannot really determine this, the re-
sult is potentially erroneous: it'll work if the compiler selected the same pool, but it'll fail
otherwise.
Important
Because allocating memory for anonymous access types is potentially dangerous, we can
use the No_Anonymous_Allocators restriction — which is available since Ada 2012 — to
prevent this kind of memory allocation being used in the code. For example:
3 procedure Show_Dangerous_Allocation is
4 IA : access Integer;
5 begin
6 IA := new Integer;
7 IA.all := 30;
8 end Show_Dangerous_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.No_Anonymous_Allocators
MD5: 0976821ce632f9635e33fd4f79c81ecd
Build output
A better solution to avoid issues when allocating and deallocating memory for anonymous
access-to-object types is to allocate the object using a known pool. As mentioned before,
the memory pool associated with a named access type is well-defined, so we can use this
kind of types for memory allocation. In fact, we can use a named memory type to allocate an
object via new, and then associate this allocated object with the access object of anonymous
access type.
Let's see a code example:
3 procedure Show_Successful_Deallocation is
4
5 type Integer_Access is
6 access Integer;
7
8 procedure Free is
9 new Ada.Unchecked_Deallocation
10 (Object => Integer,
11 Name => Integer_Access);
12
13 IA : access Integer;
14 Typed_IA : Integer_Access;
15
16 begin
17 Typed_IA := new Integer;
18 IA := Typed_IA;
19 IA.all := 30;
20
25 end Show_Successful_Deallocation;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Deallocation_Anonymous_Access_To_Object_1
MD5: eff8b54adfcc8cce10920dc3620ff1b9
In this example, all operations related to memory allocation are exclusively making use of
the Integer_Access type, which is a named access type. In fact, new Integer allocates
the object from the pool associated with the Integer_Access type, and the call to Free
deallocates this object back into that pool. Therefore, associating this object with the IA
access object — in the IA := Typed_IA assignment — doesn't creates problems afterwards
in the object's deallocation. (When calling Free, we only refer to the object of named access
type, so the object is deallocated from a known pool.)
Of course, a potential issue here is that IA becomes a dangling reference (page 795) after
the call to Free. Therefore, we can improve this solution by completely hiding the memory
allocation and deallocation for the anonymous access types in subprograms — e.g. as part
of a package. By doing so, we don't expose the named access type, thereby reducing the
possibility of dangling references.
In fact, we can generalize this approach with the following (generic) package:
5 function New_T
6 return not null access T;
7
10 end Hidden_Anonymous_Allocation;
7 procedure T_Access_Free is
8 new Ada.Unchecked_Deallocation
9 (Object => T,
10 Name => T_Access);
11
12 function New_T
13 return not null access T is
14 begin
15 return T_Access'(new T);
16 -- Using allocation of the T_Access type:
17 -- object is allocated from T_Access's pool
18 end New_T;
19
28 end Hidden_Anonymous_Allocation;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Hidden_Alloc_Dealloc_Anonymous_Access_To_Object
MD5: bd3831829f34f06a1d3c25a975c850a3
For example:
3 package Info is
4
11 private
12
26 end Info;
3 procedure Show_Info_Allocation_Deallocation is
4 RA : constant not null access Rec := New_Rec;
5 begin
6 Free (RA);
7 end Show_Info_Allocation_Deallocation;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Hidden_Alloc_Dealloc_Anonymous_Access_To_Object
MD5: d71e8ed70e280c6d5d9fc2d49c1eb6c3
Another approach that we could consider to avoid memory deallocation issues for anony-
mous access-to-object types is by simply using the stack for the object creation. For exam-
ple:
5 IA : access Integer;
6 begin
7 IA := I'Access;
8 -- Indirect allocation:
9 -- object creation on the stack.
10
11 IA.all := 30;
12
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Object_Types.Deallocation_Anonymous_Access_To_Object_2
MD5: 4381db8ba87717978a9629b1e6a5f1fc
In this case, we create the I object on the stack by simply declaring it. Then, we get access
to it and assign it to the IA access object.
With this approach, we're indirectly allocating an object for an anonymous access type by
creating it on the stack. Also, because we know that the I is automatically deallocated
when it gets out of scope, we don't have to worry about explicitly deallocating the object
referred by IA.
In summary, anonymous access-to-object types have many drawbacks that often outweigh
their benefits (page 855). In fact, allocation for those types can quickly become very compli-
cated. Therefore, in general, they're not a good alternative to named access types. Indeed,
the difficulties that we've just seen might make them a much worse option than just using
named access types instead.
We might consider using anonymous access-to-objects types only in cases when we reach
a point in our implementation work where using named access types becomes impossible
— or when using them becomes even more complicated than equivalent solutions using
anonymous access types. This scenario, however, is usually the exception rather than the
rule. Thus, as a general guideline, we should always aim to use named access types.
That being said, an important exception to this advice is when we're interfacing to other
languages (page 880). In this case, as we'll discuss later, using anonymous access-to-
objects types can be significantly simpler (compared to named access types) without the
drawbacks that we've just discussed.
Previously, we've discussed discriminants as access values (page 745). In that section, we
only used named access types. Now, in this section, we see how to use anonymous access
types as discriminants. This feature is also known as access discriminants and it provides
some flexibility that can be interesting in terms of software design, as we'll discuss later.
Let's start with an example:
11 end Custom_Recs;
13 end Custom_Recs;
3 procedure Show_Access_Discriminants is
4 I : aliased Integer := 10;
5 R : Rec (I'Access);
6 begin
7 Show (R);
8
9 I := 20;
10 R.I := 30;
11 Show (R);
12 end Show_Access_Discriminants;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Custom_Recs
MD5: f8e127fda4f7ea0f1593165d6a966df6
Runtime output
R.IA = 10
R.I = 10
R.IA = 20
R.I = 30
In this example, we use an anonymous access type for the discriminant in the declaration of
the Rec type of the Custom_Recs package. In the Show_Access_Discriminants procedure,
we declare R and provide access to the local I integer.
Similarly, we can use unconstrained designated subtypes:
12 end Persons;
13 end Persons;
3 procedure Show_Person is
4 S : aliased String := "John";
5 P : Person (S'Access);
6 begin
7 P.Age := 30;
8 Show (P);
9 end Show_Person;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Persons
MD5: f0149d572e0ec192476836bfdf00dd9e
Runtime output
Name = John
Age = 30
In this example, for the discriminant of the Person type, we use an anonymous access type
whose designated subtype is unconstrained. In the Show_Person procedure, we declare
the P object and provide access to the S string.
In contrast to named access types, we cannot use a default value for the access discriminant
of a non-limited type:
13 end Custom_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Custom_Recs
MD5: 9269cea113f29443a6d7bb719d0616f1
Build output
However, if we change the type declaration to be a limited type, having a default value for
the access discriminant is OK:
13 end Custom_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Custom_Recs
MD5: 9e8683c7a27e9097fd2003ad91bac269
Build output
Note that, if we don't provide a value for the access discriminant when declaring an object
R, the default value is allocated (via new) during R's creation.
3 procedure Show_Access_Discriminants is
4 R : Rec;
5 -- ^^^
6 -- This triggers "new Integer'(0)", so an
7 -- integer object is allocated and stored in
8 -- the R.IA discriminant.
9 begin
10 Show (R);
11
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Custom_Recs
MD5: f5d9dee26044ccab2193ab419638de79
Build output
Runtime output
R.IA = 0
R.I = 0
In this case, the allocated object won't be deallocated when R gets out of scope!
Access discriminants have the same benefits that we've already seen earlier while dis-
cussing discriminants as access values (page 745). An additional benefit is its extended
flexibility: access discriminants are compatible with any access T'Access, as long as T is
of the designated subtype.
Consider the following example using the named access type Access_String:
13 end Persons;
13 end Persons;
3 procedure Show_Person is
4 S : aliased String := "John";
5 P : Person (S'Access);
6 -- ^^^^^^^^ ERROR: cannot use local
7 -- object
8 --
9 -- We can, however, allocate the string via
10 -- new:
11 --
12 -- S : Access_String := new String'("John");
13 -- P : Person (S);
14 begin
15 P.Age := 30;
16 Show (P);
17 end Show_Person;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Persons
MD5: e918db3790c7ffeeb7c0f54ced9f48b9
Build output
This code doesn't compile because we cannot have a non-local pointer (Access_String)
pointing to the local object S. The only way to make this work is by allocating the string via
new (i.e.: S : Access_String := new String).
However, if we use an access discriminant in the declaration of Person, the code compiles
fine:
11 end Persons;
3 procedure Show_Person is
4 S : aliased String := "John";
5 P : Person (S'Access);
6 -- ^^^^^^^^ OK
7
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Access_
↪Discriminants.Persons
MD5: 6516fb4e0cbbac9cfe07a56e48ea9ff3
Runtime output
Name = John
Age = 30
In this case, getting access to the local object S and using it for P's discriminant is perfectly
fine.
Note that the usual rules that prevent dangling pointers still apply here. This ensures that
we can safely use access discriminants. For example:
3 procedure Show_Person is
4
13 P : Person := Local_Init;
14 begin
15 Show (P);
16 end Show_Person;
MD5: 9c8d2aebf60b8bb19e455cb6bc5730eb
Build output
show_person.adb:8:07: error: access discriminant in return object would be a␣
↪dangling reference
In this example, compilation fails in the Local_Init function when trying to return an object
of Person type because S'Access would be a dangling reference.
28.2.4 Self-reference
Previously, we've seen that we can declare self-references (page 762) using named access
types. We can do the same with anonymous access types. Let's revisit the code example
that implements linked lists:
7 procedure Append_Front
8 (L : in out List;
9 E : T);
(continues on next page)
11 procedure Append_Rear
12 (L : in out List;
13 E : T);
14
17 private
18
29 Value : T;
30 end record;
31
34 end Linked_Lists;
7 procedure Append_Front
8 (L : in out List;
9 E : T)
10 is
11 New_First : constant List := new
12 Component'(Value => E,
13 Next => L);
14 begin
15 L := New_First;
16 end Append_Front;
17
18 procedure Append_Rear
19 (L : in out List;
20 E : T)
21 is
22 New_Last : constant List := new
23 Component'(Value => E,
24 Next => null);
25 begin
26 if L = null then
27 L := New_Last;
28 else
29 declare
30 Last : List := L;
31 begin
32 while Last.Next /= null loop
33 Last := List (Last.Next);
(continues on next page)
61 end Linked_Lists;
3 procedure Test_Linked_List is
4 package Integer_Lists is new
5 Linked_Lists (T => Integer);
6 use Integer_Lists;
7
8 L : List;
9 begin
10 Append_Front (L, 3);
11 Append_Rear (L, 4);
12 Append_Rear (L, 5);
13 Append_Front (L, 2);
14 Append_Front (L, 1);
15 Append_Rear (L, 6);
16 Append_Rear (L, 7);
17
18 Show (L);
19 end Test_Linked_List;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Self_
↪Reference.Linked_List_Example
MD5: 9e42bf9fa630a0af8dcf7c85a1565edb
Runtime output
[ 1 2 3 4 5 6 7 ]
Here, in the declaration of the Component type (in the private part of the generic
Linked_Lists package), we declare Next as an anonymous access type that refers to the
Component type. (Note that at this point, we haven't finished the declaration of the Compo-
nent type yet, but we're already using it as the designated subtype of an anonymous access
type.) Then, we declare List as a general access type (with Component as the designated
subtype).
It's worth mentioning that the List type and the anonymous access Component type aren't
the same type, although they share the same designated subtype. Therefore, in the imple-
mentation of the Append_Rear procedure, we have to use type conversion to convert from
the anonymous access Component type to the (named) List type.
In the section on mutually dependent types using access types (page 765), we've seen a
code example that was using named access types. We could now rewrite it using anony-
mous access types:
3 type T2;
4
5 type T1 is record
6 B : access T2;
7 end record;
8
9 type T2 is record
10 A : access T1;
11 end record;
12
13 end Mutually_Dependent;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Mutually_
↪Dependent_Anonymous_Access_Types.Example
MD5: 09f869d99b9c16882554588bb806a113
In this example, T1 and T2 are mutually dependent types. We're using anonymous access
types in the declaration of the B and A components.
In the previous chapter, we talked about parameters as access values (page 752). As
you might have expected, we can also use anonymous access types as parameters of a
subprogram. However, they're limited to be in parameters of a subprogram or return type
of a function (also called the access result type):
13 end Names;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.Names
MD5: 622a76c4b133ed2715f18c175694cbe2
In this example, we have a string as the access result type of the Init function, and another
string as the access parameter of the Show procedure.
This is the complete code example:
8 private
9
14 end Names;
10 end Names;
3 procedure Show_Names is
4 N : access String := Init ("Lily", "Ann");
5 begin
6 Show (N);
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.Names
MD5: 9fe629f29de2898f2b82d9146b22fd1a
Runtime output
Name: Lily-Ann
Note that we're not using the in parameter mode in the Show procedure above. Usually,
this parameter mode can be omitted, as it is the default parameter mode — procedure P
(I : Integer) is the same as procedure P (I : in Integer). However, in the case of
the Show procedure, the in parameter mode isn't just optionally absent. In fact, for access
parameters, the parameter mode is always implied as in, so writing it explicitly is actually
forbidden. In other words, we can only write N : access String or N : access constant
String, but we cannot write N : in access String or N : in access constant String.
8 private
9
14 end Names;
10 end Names;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.Names_String
MD5: 643f193999ef8de9bcefb11d9bdd21d7
Runtime output
Name: Lily-Ann
Although we're using simple strings instead of access types in this version of the code
example, we're still getting a similar behavior. However, there is a small, yet important
difference in the way the string returned by Init is being allocated: while the previous
implementation (which was using an access result type) was allocating the string on the
heap, we're now allocating the string on the stack.
Later on, we talk about the accessibility rules in the case of access parameters (page 900).
In general, we should avoid access parameters whenever possible and simply use objects
and parameter modes directly, as it makes the design simpler and less error-prone. One
exception is when we're interfacing to other languages, especially C: this is our next topic
(page 880). Another time when access parameters are vital is for inherited primitive oper-
ations for tagged types. We discuss this later on (page 884).
We can use access parameters to interface to other languages. This can be particularly
useful when interfacing to C code that makes use of pointers. For example, let's assume
we want to call the add_one function below in our Ada implementation:
209 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.C_Interfacing
MD5: 3270f3b2415266a203a6f4c605c3831b
We could map the int * parameter of add_one to access Integer in the Ada specification:
procedure Add_One (IA : access Integer)
with Import, Convention => C;
6 end Operations;
5 procedure Show_Operations is
6 I : aliased Integer := 42;
7 begin
8 Put_Line (I'Image);
9 Add_One (I'Access);
10 Put_Line (I'Image);
11 end Show_Operations;
MD5: 0219acdbd2dad69962875199ffdd930e
Once again, we can replace access parameters with simpler types by using the appropriate
parameter mode. In this case, we could replace access Integer by aliased in out
Integer. This is the modified version of the code:
3 procedure Add_One
4 (IA : aliased in out Integer)
5 with Import, Convention => C;
6
7 end Operations;
5 procedure Show_Operations is
(continues on next page)
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.C_Interfacing
MD5: 2c5a81b8d77f0fff8a73f7912be6b6fe
However, there are situations where aliased objects cannot be used. For example, suppose
we want to allocate memory inside a C function. In this case, the pointer to that memory
block must be mapped to an access type in Ada.
Let's extend the previous C code example and introduce the alloc_integer and deal-
loc_integer functions, which allocate and deallocate an integer value:
3 int * alloc_integer()
4 {
5 return malloc(sizeof(int));
6 }
7
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.C_Interfacing
MD5: ec6dea12d0a948489cce21b0cc0a1ad2
In this case, we really have to use access types to interface to these C functions. In fact,
we need an access result type to interface to the alloc_integer() function, and an ac-
cess parameter in the case of the dealloc_integer() function. This is the corresponding
specification in Ada:
9 procedure Add_One
10 (IA : aliased in out Integer)
11 with Import, Convention => C;
12
13 end Operations;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.C_Interfacing
MD5: bcbc8a87037b64fc6469e67b928e6172
Note that we're still using an aliased integer type for the Add_One procedure, while we're
using access types for the other two subprograms.
Finally, as expected, we can use this specification in a test application:
5 procedure Show_Operations is
6 I : access Integer := Alloc_Integer;
7 begin
8 I.all := 42;
9 Put_Line (I.all'Image);
10
11 Add_One (I.all);
12 Put_Line (I.all'Image);
13
14 Dealloc_Integer (I);
15 end Show_Operations;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.C_Interfacing
MD5: b2b96a166926528bc44059b56e31fb55
In this application, we get a C pointer from the alloc_integer function and encapsulate it
in an Ada access type, which we then assign to I. In the last line of the procedure, we call
Dealloc_Integer and pass I to it, which deallocates the memory block indicated by the C
pointer.
210 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
In order to declare inherited primitive operations for tagged types that use access types,
we need to use access parameters. The reason is that, to be a primitive operation for some
tagged type — and hence inheritable — the subprogram must reference the tagged type
name directly in the parameter profile. This means that a named access type won't suffice,
because only the access type name would appear in the profile. For example:
14 private
15
20 end Inherited_Primitives;
7 end Inherited_Primitives;
4 procedure Show_Inherited_Primitives is
5 Obj : T_Access := new T;
6 Obj_Child : T_Child_Access := new T_Child;
7 begin
8 Proc (Obj);
9 Proc (Obj_Child);
10 -- ^^^^^^^^^
11 -- ERROR: Proc is not inherited!
12 end Show_Inherited_Primitives;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_Parameters.Inherited_Primitives
MD5: 8235b21caa9f1f105f533d74d891adfe
Build output
show_inherited_primitives.adb:9:10: error: expected type "T_Access" defined at␣
↪inherited_primitives.ads:5
In this example, Proc is not a primitive of type T because it's referring to type T_Access, not
type T. This means that Proc isn't inherited when we derive the T_Child type. Therefore,
when we call Proc (Obj_Child), a compilation error occurs because the compiler expects
type T_Access — there's no Proc (N : T_Child_Access) that could be used here.
If we replace T_Access in the Proc procedure with an an access parameter (access T), the
subprogram becomes a primitive of T:
10 private
11
16 end Inherited_Primitives;
5 end Inherited_Primitives;
4 procedure Show_Inherited_Primitives is
5 Obj : access T := new T;
6 Obj_Child : access T_Child := new T_Child;
7 begin
8 Proc (Obj);
9 Proc (Obj_Child);
10 -- ^^^^^^^^^
11 -- OK: Proc is inherited!
12 end Show_Inherited_Primitives;
Now, the child type T_Child (derived from the T) inherits the primitive operation Proc. This
inherited operation has an access parameter designating the child type:
Implicit dereferencing (page 767) isn't limited to the contexts that Ada supports by
default: we can also add implicit dereferencing to our own types by using the Im-
plicit_Dereference aspect.
To do this, we have to declare:
• a reference type, where we use the Implicit_Dereference aspect to specify the ref-
erence discriminant, which is the record discriminant that will be dereferenced; and
• a reference object, which contains an access value that will be dereferenced.
Also, for the reference type, we have to:
• specify the reference discriminant as an access discriminant (page 868); and
• indicate the name of the reference discriminant when specifying the Im-
plicit_Dereference aspect.
Let's see a simple example:
3 procedure Show_User_Defined_Reference is
4
9 --
10 -- Reference type:
11 --
12 type Id_Ref (Ref : access Id_Number) is
13 -- ^ reference discriminant
14 null record
15 with Implicit_Dereference => Ref;
16 -- ^^^
17 -- name of the reference
18 -- discriminant
19
20 --
(continues on next page)
211 http://www.ada-auth.org/standards/22rm/html/RM-3-9-2.html
26 --
27 -- Reference object:
28 --
29 R : Id_Ref (I);
30 begin
31 Put_Line ("ID: "
32 & Positive'Image (R.Id));
33 -- ^ Equivalent to:
34 -- R.Ref.Id
35 -- or:
36 -- R.Ref.all.Id
37 end Show_User_Defined_Reference;
MD5: 33eaa7e8e75b4eb56d64dcc17e2932aa
Runtime output
ID: 42
Here, we declare a simple record type (Id_Number) and a corresponding reference type
(Id_Ref). Note that:
• the reference discriminant Ref has an access to the Id_Number type; and
• we indicate this reference discriminant in the Implicit_Dereference aspect.
Then, we declare an access value (the I constant) and use it for the Ref discriminant in the
declaration of the reference object R.
Finally, we implicitly dereference R and access the Id component by simply writing R.Id —
instead of the extended forms R.Ref.Id or R.Ref.all.Id.
Important
The extended form mentioned in the example that we just saw (R.Ref.all.Id) makes it
clear that two steps happen when evaluating R.Id:
• First, R.Ref is implied from R because of the Implicit_Dereference aspect.
• Then, R.Ref is implicitly dereferenced to R.Ref.all.
After these two steps, we can access the actual object. (In our case, we can access the Id
component.)
Note that we cannot use access types directly for the reference discriminant. For example,
if we made the following change in the previous code example, it wouldn't compile:
type Id_Number_Access is access Id_Number;
-- Reference type:
type Id_Ref (Ref : Id_Number_Access) is
-- ^ ERROR: it must be
-- an access
(continues on next page)
However, we could use other forms — such as not null access — in the reference dis-
criminant:
-- Reference type:
type Id_Ref (Ref : not null access Id_Number) is
null record
with Implicit_Dereference => Ref;
Naturally, implicit dereferencing is also possible when calling primitives of a tagged type.
For example, let's change the declaration of the Id_Number type from the previous code
example and add a Show primitive.
10 end Info;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.User_
↪Defined_References.Dereferencing_Tagged_Types
MD5: 4de65094963450dc3a7505dbf93c2551
Then, let's declare a reference type and a reference object in the test application:
212 http://www.ada-auth.org/standards/22rm/html/RM-4-1-5.html
3 procedure Show_User_Defined_Reference is
4
5 -- Reference type:
6 type Id_Ref (Ref : access Id_Number) is
7 null record
8 with Implicit_Dereference => Ref;
9
10 -- Access value:
11 I : constant access Id_Number :=
12 new Id_Number (42);
13
14 -- Reference object:
15 R : Id_Ref (I);
16 begin
17
18 R.Show;
19 -- Equivalent to:
20 -- R.Ref.all.Show;
21
22 end Show_User_Defined_Reference;
MD5: 9c5dfc4f2b8e085efde9e61689243f70
Runtime output
ID: 42
Here, we can call the Show procedure by simply writing R.Show instead of R.Ref.all.Show.
Simple container
21 end National_Date_Info;
14 end National_Date_Info;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.User_
↪Defined_References.National_Dates
MD5: 90fd6740d701025e1d5f30c9751a528d
Here, National_Date is a record type that we use to store the national day information.
We can call the Show procedure to display this information.
Now, let's implement the National_Date_Containers with a container for national days:
3 package National_Date_Containers is
4
5 -- Reference type:
6 type National_Date_Reference
7 (Ref : access National_Date) is
8 tagged limited null record
9 with Implicit_Dereference => Ref;
10
23 end National_Date_Containers;
19 return
20 National_Date_Reference'(Ref => null);
21 -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22 -- Returning reference object with a null
23 -- reference in case the country wasn't
24 -- found. This will trigger an exception
25 -- if we try to dereference it.
26 end Find;
27
28 end National_Date_Containers;
MD5: ec37ae93a7052c4bc731b2a7be0763ab
Important
We're declaring the container type (National_Dates) as an array type just to simplify the
code. In many cases, however, this approach isn't recommended! Instead, we should use
a private type in order to encapsulate — and better protect — the information stored in the
actual container.
Finally, let's see a test application that stores information for some countries into the
Nat_Dates container and displays the information for a specific country:
4 with National_Date_Containers;
5 use National_Date_Containers;
6
21 begin
22 Find (Nat_Dates, "FRA").Show;
23 -- ^ implicit dereference
24 end Show_National_Dates;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.User_
↪Defined_References.National_Dates
MD5: 771ecb91e8f890d4bb9b08115ae833f4
Runtime output
Country: FRA
Year: 1789
Here, we call the Find function to retrieve a reference object, whose reference (access
value) has the national day information of France. We then implicitly dereference it to get
the tagged object (of National_Date type) and display its information by calling the Show
procedure.
Relevant topics
The National_Date_Containers package was implemented specifically as an accompany-
ing package for the National_Date_Info package. It is possible, however, to generalize
it, so that we can reuse the container for other record types. In fact, this is actually very
straightforward:
14 type Container is
15 array (Positive range <>) of
16 T_Access;
(continues on next page)
22 end Generic_Containers;
16 end Generic_Containers;
MD5: 94c23a48131a47439b5b41e985c3d6c1
4 procedure Show_National_Dates is
5
6 function Matches_Country
7 (E : National_Date_Access;
8 Elem : Country_Code)
9 return Boolean is
10 (E.Country = Elem);
11
19 use National_Date_Containers;
(continues on next page)
23 Nat_Dates : constant
24 National_Dates (1 .. 5) :=
25 (new National_Date'("USA",
26 Time'(1776, 7, 4)),
27 new National_Date'("FRA",
28 Time'(1789, 7, 14)),
29 new National_Date'("DEU",
30 Time'(1990, 10, 3)),
31 new National_Date'("SPA",
32 Time'(1492, 10, 12)),
33 new National_Date'("BRA",
34 Time'(1822, 9, 7)));
35
36 begin
37 Find (Nat_Dates, "FRA").Show;
38 end Show_National_Dates;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.User_
↪Defined_References.National_Dates
MD5: f4dac1fed69b9bccce5dccbf17844adc
Runtime output
Country: FRA
Year: 1789
with Ada.Iterator_Interfaces;
generic
type Index_Type is range <>;
type Element_Type is private;
with function "=" (Left, Right : Element_Type)
return Boolean is <>;
package Ada.Containers.Vectors
with Preelaborate, Remote_Types,
Nonblocking,
Global => in out synchronized is
-- OMITTED
type Reference_Type
(Element : not null access Element_Type) is
private
with Implicit_Dereference => Element,
Nonblocking,
Global => in out synchronized,
Default_Initial_Condition =>
(raise Program_Error);
function Reference
(Container : aliased in out Vector;
Index : in Index_Type)
return Reference_Type
with Pre => Index in
First_Index (Container) ..
Last_Index (Container)
or else raise
Constraint_Error,
Post =>
Tampering_With_Cursors_Prohibited
(Container),
Nonblocking,
Global => null,
Use_Formal => null;
-- OMITTED
function Reference
(Container : aliased in out Vector;
Position : in Cursor)
return Reference_Type
with Pre => (Position /= No_Element
or else raise
Constraint_Error)
and then
(Has_Element
(Container, Position)
or else raise
Program_Error),
Post =>
Tampering_With_Cursors_Prohibited
(Container),
Nonblocking,
Global => null,
Use_Formal => null;
-- OMITTED
end Ada.Containers.Vectors;
(Note that most parts of the Vectors package were omitted for clarity. Please refer to the
Ada Reference Manual for the complete package specification.)
Here, we see that the Implicit_Dereference aspect is used in the declaration of Refer-
ence_Type, which is the reference type returned by the Reference functions for an index
or a cursor.
Also, note that the Vectors package has a formal equality function (=) instead of the
Matches function we were using in our Generic_Containers package. The purpose of
the formal function, however, is basically the same.
213 http://www.ada-auth.org/standards/22rm/html/RM-A-18-2.html
In general, the accessibility rules (page 790) we've seen earlier also apply to anonymous
access types. However, there are some subtle differences, which we discuss in this section.
Let's adapt the code example from that section (page 790) to make use of anonymous
access types:
7 end Library_Level;
3 procedure Show_Library_Level is
4 L1_Var : aliased Integer;
5
8 procedure Test is
9 L2_AO : access Integer;
10
18 L2_AO := L2_Var'Access;
19 -- ^^^^^^
20 -- LEGAL: L2 object to
21 -- L2 access object
22 end Test;
23
24 begin
25 L0_AO := new Integer'(22);
26 -- ^^^^^^^^^^^
27 -- LEGAL: L0 object to
28 -- L0 access object
29
30 L0_AO := L1_Var'Access;
31 -- ^^^^^^
32 -- ILLEGAL: L1 object to
33 -- L0 access object
34
35 L1_AO := L0_Var'Access;
36 -- ^^^^^^
37 -- LEGAL: L0 object to
38 -- L1 access object
39
40 L1_AO := L1_Var'Access;
41 -- ^^^^^^
42 -- LEGAL: L1 object to
(continues on next page)
59 Test;
60 end Show_Library_Level;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Levels_Rules_Introduction.Accessibility_Library_Level
MD5: 255bdecebdaa735408db082edd583a0c
Build output
As we see in the code, in general, most accessibility rules are the same as the ones we've
discussed when using named access types. For example, an assignment such as L0_AO :=
L1_Var'Access is illegal because we're trying to assign to an access object of less deep
level.
However, assignment such as L0_AO := L1_AO are possible now: we don't get a type
mismatch — as we did with named access types — because both objects are of anonymous
access types. Note that the accessibility level cannot be determined at compile time: L1_AO
can hold an access value at library level (which would make the assignment legal) or at a
deeper level. Therefore, the compiler introduces an accessibility check here.
However, the accessibility check used in L0_AO := L1_AO fails at runtime because the cor-
responding access value (L1_Var'Access) is of a deeper level than L0_AO, which is illegal.
(If you comment out the L1_AO := L1_Var'Access assignment prior to the L0_AO := L1_AO
assignment, this accessibility check doesn't fail anymore.)
In the previous sections, we've discussed accessibility rules for named and anonymous
access types separately. In this section, we see that the same accessibility rules apply
when mixing both flavors together and converting objects of anonymous to named access
types.
Let's adapt parts of the previous code example (page 790) and add anonymous access
types to it:
3 type L0_Integer_Access is
4 access all Integer;
5
8 L0_IA : L0_Integer_Access;
9 L0_AO : access Integer;
10
11 end Library_Level;
3 procedure Show_Library_Level is
4 type L1_Integer_Access is
5 access all Integer;
6
7 L1_IA : L1_Integer_Access;
8 L1_AO : access Integer;
9
12 begin
13 ---------------------------------------
14 -- From named type to anonymous type
15 ---------------------------------------
16
20 L0_AO := L0_IA;
21 -- ^^^^^
22 -- LEGAL: assignment from
23 -- L0 access object (named type)
24 -- to
25 -- L0 access object
26 -- (anonymous type)
27
28 L0_AO := L1_IA;
29 -- ^^^^^
30 -- ILLEGAL: assignment from
31 -- L1 access object (named type)
32 -- to
33 -- L0 access object
34 -- (anonymous type)
35
36 L1_AO := L0_IA;
37 -- ^^^^^
38 -- LEGAL: assignment from
39 -- L0 access object (named type)
40 -- to
41 -- L1 access object
42 -- (anonymous type)
43
44 L1_AO := L1_IA;
45 -- ^^^^^
(continues on next page)
52 ---------------------------------------
53 -- From anonymous type to named type
54 ---------------------------------------
55
56 L0_AO := L0_Var'Access;
57 L1_AO := L1_Var'Access;
58
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Levels_Rules_Introduction.Accessibility_Named_Anonymous_Access_
↪Type_Conversions
MD5: a2e73bb0ed543bc4973850c80f951039
Build output
As we can see in this code example, mixing access objects of named and anonymous access
types doesn't change the accessibility rules. Again, the rules are only violated when the
target object in the assignment is less deep. This is the case in the L0_AO := L1_IA and
the L0_IA := L0_Integer_Access (L1_AO) assignments. Otherwise, mixing those access
objects doesn't impose additional hurdles.
In the previous chapter, we saw that the accessibility rules also apply to access values as
subprogram parameters (page 794). In the case of access parameters, the rules are a bit
less strict (as you may generally expect for anonymous access types), and the accessibility
rules are checked at runtime. This allows use to use access values that would be illegal in
the case of named access types because of their accessibility levels.
Let's adapt a previous code example to make use of access parameters:
5 end Names;
3 -- with Ada.Characters.Handling;
4 -- use Ada.Characters.Handling;
5
16 end Names;
3 procedure Show_Names is
4 S : aliased String := "John";
5 begin
6 Show (S'Access);
7 end Show_Names;
Project: Courses.Advanced_Ada.Resource_Management.Access_Types.Accessibility_
↪Levels_Rules_Introduction.Accessibility_Checks_Parameters
MD5: aa930ba9be3264d01eb9115d27b884eb
Runtime output
Name: John
As we've seen in the previous chapter, compilation fails when we use named access types
in this code example. In the case of access parameters, using S'Access doesn't make the
compilation fail, nor does the accessibility check fail at runtime because S is still in scope
when we call the Show procedure.
In the previous chapter, we talked about named access-to-subprogram types (page 820).
Now, we'll see that the anonymous version of those types isn't much different from the
named version.
Let's start our discussion by declaring a subprogram parameter using an anonymous
access-to-procedure type:
3 procedure Proc
4 (P : access procedure (I : in out Integer));
5
6 end Anonymous_Access_To_Subprogram;
3 procedure Proc
4 (P : access procedure (I : in out Integer))
5 is
6 I : Integer := 0;
7 begin
8 P (I);
9 end Proc;
10
11 end Anonymous_Access_To_Subprogram;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Anonymous_Access_To_Subprogram_Example
MD5: 2cbe76d7e23905d575bd27e29d5e3175
In this example, we use the anonymous access procedure (I : in out Integer) type
as a parameter of the Proc procedure. Note that we need an identifier in the declaration:
we cannot leave I out and write access procedure (in out Integer).
Before we look at a test application that makes use of the Anony-
mous_Access_To_Subprogram package, let's implement two simple procedures that
we'll use later on:
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Anonymous_Access_To_Subprogram_Example
MD5: 50eaeaf27caaa9618b35ecdf8acc11fe
4 with Add_Ten;
5
6 procedure Show_Anonymous_Access_To_Subprograms is
7 begin
8 Proc (Add_Ten'Access);
9 -- ^ Getting access to Add_Ten
10 -- procedure and passing it
11 -- to Proc
12 end Show_Anonymous_Access_To_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Anonymous_Access_To_Subprogram_Example
MD5: 13143ccf9620d26031484ba160a58fe1
Here, we get access to the Add_Ten procedure and pass it to the Proc procedure. Note that
this implementation is not different from the example for named access-to-subprogram
types (page 822). In fact, in terms of usage, anonymous access-to-subprogram types are
very similar to named access-to-subprogram types. The major differences can be found in
the corresponding accessibility rules (page 910).
214 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
In the section about named access-to-subprogram types (page 820), we've seen a couple of
different usages for those types. In all those examples we discussed, we could instead have
used anonymous access-to-subprogram types. Let's see a code example that illustrates
that:
3 --
4 -- Anonymous access-to-subprogram as
5 -- subprogram parameter:
6 --
7 procedure Proc
8 (P : access procedure (I : in out Integer));
9
10 --
11 -- Anonymous access-to-subprogram in
12 -- array type declaration:
13 --
14 type Access_To_Procedure_Array is
15 array (Positive range <>) of
16 access procedure (I : in out Integer);
17
20 procedure Mult_Ten;
21
22 procedure Mult_Twenty;
23
24 private
25 I : Integer := 1;
26 end Protected_Integer;
27
28 --
29 -- Anonymous access-to-subprogram as
30 -- component of a record type.
31 --
32 type Rec_Access_To_Procedure is record
33 AP : access procedure (I : in out Integer);
34 end record;
35
36 --
37 -- Anonymous access-to-subprogram as
38 -- discriminant:
39 --
40 type Rec_Access_To_Procedure_Discriminant
41 (AP : access procedure
42 (I : in out Integer)) is
43 record
44 I : Integer := 0;
45 end record;
46
47 procedure Process
48 (R : in out
49 Rec_Access_To_Procedure_Discriminant);
50
51 generic
52 type T is private;
53
62 end All_Anonymous_Access_To_Subprogram;
5 procedure Proc
6 (P : access procedure (I : in out Integer))
7 is
8 I : Integer := 0;
9 begin
10 Put_Line
11 ("Calling procedure for Proc...");
12 P (I);
13 Put_Line ("Finished.");
14 end Proc;
15
16 procedure Process
17 (R : in out
18 Rec_Access_To_Procedure_Discriminant)
19 is
20 begin
21 Put_Line
22 ("Calling procedure for"
23 & " Rec_Access_To_Procedure_Discriminant"
24 & " type...");
25 R.AP (R.I);
26 Put_Line ("Finished.");
27 end Process;
28
39 procedure Mult_Ten is
40 begin
41 I := I * 10;
42 end Mult_Ten;
43
44 procedure Mult_Twenty is
45 begin
46 I := I * 20;
47 end Mult_Twenty;
48
49 end Protected_Integer;
(continues on next page)
51 end All_Anonymous_Access_To_Subprogram;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Anonymous_Access_To_Subprogram_Example
MD5: 628dcfdc5fe9b712f33fa044057093c2
3 with Add_Ten;
4 with Add_Twenty;
5
6 with All_Anonymous_Access_To_Subprogram;
7 use All_Anonymous_Access_To_Subprogram;
8
9 procedure Show_Anonymous_Access_To_Subprograms is
10 --
11 -- Anonymous access-to-subprogram as
12 -- an object:
13 --
14 P : access procedure (I : in out Integer);
15
16 --
17 -- Array of anonymous access-to-subprogram
18 -- components
19 --
20 PA : constant
21 Access_To_Procedure_Array (1 .. 2) :=
22 (Add_Ten'Access,
23 Add_Twenty'Access);
24
25 --
26 -- Anonymous array of anonymous
27 -- access-to-subprogram components:
28 --
29 PAA : constant
30 array (1 .. 2) of access
31 procedure (I : in out Integer) :=
32 (Add_Ten'Access,
33 Add_Twenty'Access);
34
35 --
36 -- Record with anonymous
37 -- access-to-subprogram components:
(continues on next page)
42 --
43 -- Record with anonymous
44 -- access-to-subprogram discriminant:
45 --
46 RD : Rec_Access_To_Procedure_Discriminant
47 (AP => Add_Twenty'Access) :=
48 (AP => Add_Twenty'Access, I => 0);
49
50 --
51 -- Generic procedure with formal anonymous
52 -- access-to-subprogram:
53 --
54 procedure Process_Integer is new
55 Gen_Process (T => Integer,
56 Proc_T => Add_Twenty'Access);
57
58 --
59 -- Object (APP) of anonymous
60 -- access-to-protected-subprogram:
61 --
62 PI : Protected_Integer;
63 APP : constant access protected procedure :=
64 PI.Mult_Ten'Access;
65
66 Some_Int : Integer := 0;
67 begin
68 Put_Line ("Some_Int: " & Some_Int'Image);
69
70 --
71 -- Using object of
72 -- anonymous access-to-subprogram type:
73 --
74 P := Add_Ten'Access;
75 Proc (P);
76 P (Some_Int);
77
78 P := Add_Twenty'Access;
79 Proc (P);
80 P (Some_Int);
81
84 --
85 -- Using array with component of
86 -- anonymous access-to-subprogram type:
87 --
88 Put_Line
89 ("Calling procedure from PA array...");
90
96 Put_Line ("Finished.");
97
98 Put_Line
(continues on next page)
110 --
111 -- Using record with component of
112 -- anonymous access-to-subprogram type:
113 --
114 RA.AP (Some_Int);
115 Put_Line ("Some_Int: " & Some_Int'Image);
116
117 --
118 -- Using record with discriminant of
119 -- anonymous access-to-subprogram type:
120 --
121 Process (RD);
122 Put_Line ("RD.I: " & RD.I'Image);
123
124 --
125 -- Using procedure instantiated with
126 -- formal anonymous access-to-subprogram:
127 --
128 Process_Integer (Some_Int);
129 Put_Line ("Some_Int: " & Some_Int'Image);
130
131 --
132 -- Using object of anonymous
133 -- access-to-protected-subprogram type:
134 --
135 APP.all;
136 end Show_Anonymous_Access_To_Subprograms;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Anonymous_Access_To_Subprogram_Example
MD5: ec770c17e880a98fd2e9ab0110d4a858
Runtime output
Some_Int: 0
Calling procedure for Proc...
Finished.
Calling procedure for Proc...
Finished.
Some_Int: 30
Calling procedure from PA array...
Some_Int: 40
Some_Int: 60
Finished.
Calling procedure from PAA array...
Some_Int: 70
Some_Int: 90
Finished.
(continues on next page)
In general, there isn't much that speaks against using anonymous access-to-subprogram
types. We can say, for example, that they're much more useful than anonymous access-
to-objects types (page 858), which have many drawbacks (page 860) — as we discussed
earlier.
There isn't much to be concerned when using anonymous access-to-subprogram types.
For example, we cannot allocate or deallocate a subprogram. As a consequence, we won't
have storage management issues affecting these types because the access to those sub-
programs will always be available and no memory leak can occur.
Also, anonymous access-to-subprogram types can be easier to use than named access-to-
subprogram types because of their less strict accessibility rules (page 910). Some of the
accessibility issues we might encounter when using named access-to-subprogram types
can be solved by declaring them as anonymous types. (We discuss the accessibility rules
of anonymous access-to-subprogram types in the next section.)
Readability
3 function F
4 return access
5 function (A : Integer)
6 return access
(continues on next page)
10 end Readability_Issue;
7 function Select_Conversion
8 (A : Integer)
9 return access
10 function (B : Float)
11 return Integer is
12 (To_Integer'Access);
13
14 end Readability_Issue.Functions;
6 function F
7 return access
8 function (A : Integer)
9 return access
10 function (B : Float)
11 return Integer is
12 (Select_Conversion'Access);
13
14 end Readability_Issue;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Readability_Issue
MD5: 9e2ac58942c97b44c0d847c28e39bd11
In this example, the definition of F might compile fine, but it's simply too long to be read-
able. Not only that: we need to carry this chain to other functions as well — such as the
Select_Conversion function above. Also, using these functions in an application is not
straightforward:
4 procedure Show_Readability_Issue is
5 F1 : access
6 function (A : Integer)
7 return access
8 function (B : Float)
(continues on next page)
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.Anonymous_
↪Access_To_Subprograms.Readability_Issue
MD5: 80267b1d673663e3cacba0c4978e6abf
Therefore, our recommendation is to avoid this kind of access cascading by carefully de-
signing your application. In general, you won't need that.
In principle, the accessibility rules for anonymous access types (page 896) that we've seen
before apply to anonymous access-to-subprograms as well. Also, we had a discussion about
accessibility rules and access-to-subprograms (page 845) in the previous chapter. In this
section, we review some of the rules that we already know and discuss how they relate to
anonymous access-to-subprograms.
3 procedure Show_Access_To_Subprogram_Error is
4
5 type PI is access
6 procedure (I : in out Integer);
7
8 P : PI;
9
10 I : Integer := 0;
11 begin
12 declare
13 procedure Add_One (I : in out Integer) is
14 begin
15 I := I + 1;
16 end Add_One;
(continues on next page)
215 http://www.ada-auth.org/standards/22rm/html/RM-3-10.html
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Simple_Example_Named
MD5: 41c36426112e799210b7704dd43b6217
Build output
In this example, we get a compilation error because the lifetime of the Add_One procedure
is shorter than the access type PI.
In contrast, using an anonymous access-to-subprogram type eliminates the compilation
error, i.e. the assignment P := Add_One'Access becomes legal:
3 procedure Show_Access_To_Subprogram_Error is
4 P : access procedure (I : in out Integer);
5
6 I : Integer := 0;
7 begin
8 declare
9 procedure Add_One (I : in out Integer) is
10 begin
11 I := I + 1;
12 end Add_One;
13 begin
14 P := Add_One'Access;
15 -- RUNTIME ERROR: Add_One is out-of-scope
16 -- after this line.
17 end;
18 end Show_Access_To_Subprogram_Error;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Simple_Example_Anonymous
MD5: a5eeb4a716b4f6a932dd74c580a07b66
Runtime output
In this case, the compiler introduces an accessibility check, which fails at runtime because
the lifetime of Add_One is shorter than the lifetime of the access object P.
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 type Process_Procedure is
7 access
8 procedure (Arr : in out Integer_Array);
9
10 procedure Process
11 (Arr : in out Integer_Array;
12 P : Process_Procedure);
13
14 end Access_To_Subprogram_Types;
3 procedure Process
4 (Arr : in out Integer_Array;
5 P : Process_Procedure) is
6 begin
7 P (Arr);
8 end Process;
9
10 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 procedure Show_Access_To_Subprogram_Error is
7
8 procedure Add_One
9 (Arr : in out Integer_Array) is
10 begin
11 for E of Arr loop
(continues on next page)
16 procedure Display
17 (Arr : in out Integer_Array) is
18 begin
19 for I in Arr'Range loop
20 Put_Line ("Arr (" &
21 Integer'Image (I)
22 & "): "
23 & Integer'Image (Arr (I)));
24 end loop;
25 end Display;
26
31 Put_Line ("Add_One...");
32 Process (Arr, Add_One'Access);
33
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Access_To_Subprogram_
↪Parameter_Named
MD5: 76b70b52a0374fe0fd398024fe869876
Build output
3 type Integer_Array is
4 array (Positive range <>) of Integer;
5
6 procedure Process
7 (Arr : in out Integer_Array;
8 P : access procedure
9 (Arr : in out Integer_Array));
10
11 end Access_To_Subprogram_Types;
3 procedure Process
4 (Arr : in out Integer_Array;
5 P : access procedure
6 (Arr : in out Integer_Array)) is
7 begin
8 P (Arr);
9 end Process;
10
11 end Access_To_Subprogram_Types;
3 with Access_To_Subprogram_Types;
4 use Access_To_Subprogram_Types;
5
6 procedure Show_Access_To_Subprogram_Error is
7
8 procedure Add_One
9 (Arr : in out Integer_Array) is
10 begin
11 for E of Arr loop
12 E := E + 1;
13 end loop;
14 end Add_One;
15
16 procedure Display
17 (Arr : in out Integer_Array) is
18 begin
19 for I in Arr'Range loop
20 Put_Line ("Arr (" &
21 Integer'Image (I)
22 & "): "
23 & Integer'Image (Arr (I)));
24 end loop;
25 end Display;
26
31 Put_Line ("Add_One...");
32 Process (Arr, Add_One'Access);
33
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Access_To_Subprogram_
↪Parameter_Anonymous
MD5: a500e0a864f0adadc1d6823c1f50bd64
Runtime output
Arr ( 1): 1
Arr ( 2): 2
Arr ( 3): 3
Add_One...
Arr ( 1): 2
Arr ( 2): 3
Arr ( 3): 4
Iterator
A typical example that illustrates well the necessity of using anonymous access-to-
subprograms is that of a container iterator. In fact, many of the standard Ada containers —
the child packages of Ada.Containers — make use of anonymous access-to-subprograms
for their Iterate subprograms.
216 http://www.ada-auth.org/standards/22rm/html/RM-A-18-2.html
217 http://www.ada-auth.org/standards/22rm/html/RM-A-18-4.html
218 http://www.ada-auth.org/standards/22rm/html/RM-A-18-7.html
Let's start with a simplified container type (Data_Container) using a named access-to-
subprogram type (Process_Element) for iteration:
8 Data_Container_Full : exception;
9
13 type Process_Element is
14 not null access procedure (E : Element);
15
16 procedure Iterate
17 (D : Data_Container;
18 Proc : Process_Element);
19
20 private
21
22 type Data_Container_Storage is
23 array (Positive range <>) of Element;
24
31 end Data_Processing;
19 procedure Iterate
20 (D : Data_Container;
21 Proc : Process_Element) is
(continues on next page)
28 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Iterator_Named
MD5: e48e8200e571b62d027753ee96c47fcb
3 package Float_Data_Processing is
4 new Data_Processing (Element => Float);
3 with Float_Data_Processing;
4 use Float_Data_Processing;
5
6 procedure Show_Access_To_Subprograms is
7
13 D : Data_Container (5);
14 begin
15 Append (D, 1.0);
16 Append (D, 2.0);
17 Append (D, 3.0);
18
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Iterator_Named
MD5: 64ee435aac5f2817b7d9cecf538a1e4c
Build output
Using Display'Access in the call to Iterate triggers a compilation error because its life-
8 Data_Container_Full : exception;
9
13 procedure Iterate
14 (D : Data_Container;
15 Proc : not null access
16 procedure (E : Element));
17
18 private
19
20 type Data_Container_Storage is
21 array (Positive range <>) of Element;
22
29 end Data_Processing;
19 procedure Iterate
20 (D : Data_Container;
(continues on next page)
29 end Data_Processing;
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Iterator_Anonymous
MD5: fa56595ef1734f2f07ad719c36dfd8b5
Note that the only changes we did to the package were to remove the Process_Element
type and replace the type of the Proc parameter of the Iterate procedure from a named
type (Process_Element) to an anonymous type (not null access procedure (E : El-
ement)).
Now, the same test application we used before (Show_Access_To_Subprograms) compiles
as expected:
3 package Float_Data_Processing is
4 new Data_Processing (Element => Float);
3 with Float_Data_Processing;
4 use Float_Data_Processing;
5
6 procedure Show_Access_To_Subprograms is
7
13 D : Data_Container (5);
14 begin
15 Append (D, 1.0);
16 Append (D, 2.0);
17 Append (D, 3.0);
18
Project: Courses.Advanced_Ada.Resource_Management.Anonymous_Access_Types.
↪Accessibility_Rules_Anonymous_Access_To_Subprograms.Iterator_Anonymous
MD5: 64ee435aac5f2817b7d9cecf538a1e4c
Runtime output
F : 1.00000E+00
F : 2.00000E+00
F : 3.00000E+00
Remember that the compiler introduces an accessibility check in the call to Iterate, which
is successful because the lifetime of Display'Access is the same as the lifetime of the
Proc parameter of Iterate.
So far, we discussed nonlimited types in most cases. In this chapter, we discuss limited
types.
We can think of limited types as an easy way to avoid inappropriate semantics. For exam-
ple, a lock should not be copied — neither directly, via assignment, nor with pass-by-copy.
Similarly, a file, which is really a file descriptor, should not be copied. In this chapter, we'll
see example of unwanted side-effects that arise if we don't use limited types for these
cases.
Limited types have the following restrictions, which we discussed in the Introduction to Ada
(page 116) course:
• copying objects of limited types via direct assignments is forbidden; and
• there's no predefined equality operator for limited types.
(Of course, in the case of nonlimited types, assignments are possible and the equality
operator is available.)
By having these restrictions for limited types, we avoid inappropriate side-effects for as-
signment and equality operations. As an example of inappropriate side-effects, consider
the case when we apply those operations on record types that have components of access
types:
15 private
16
21 end Nonlimited_Types;
27 end Nonlimited_Types;
4 procedure Show_Wrong_Assignment_Equality is
5 A, B : Simple_Rec := Init (0);
6
7 procedure Show_Compare is
8 begin
9 if A = B then
10 Put_Line ("A = B");
11 else
12 Put_Line ("A /= B");
13 end if;
14 end Show_Compare;
15 begin
16
47 end Show_Wrong_Assignment_Equality;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Assignment_
↪Equality.Wrong_Assignment_Equality
MD5: 72cf7145cd26a8628580c5a837d9cb61
Runtime output
In this code, we declare the Simple_Rec type in the Nonlimited_Types package and use
it in the Show_Wrong_Assignment_Equality procedure. In principle, we're already doing
many things right here. For example, we're declaring the Simple_Rec type private, so
that the component V of access type is encapsulated. Programmers that declare objects
of this type cannot simply mess up with the V component. Instead, they have to call the
Init function and the Set procedure to initialize and change, respectively, objects of the
Simple_Rec type. That being said, there are two problems with this code, which we discuss
next.
The first problem we can identify is that the first call to Show_Compare shows that A and B
are different, although both have the same value in the V component (A.V.all = 0 and
B.V.all = 0) — this was set by the call to the Init function. What's happening here is
that the A = B expression is comparing the access values (A.V = B.V), while we might
have been expecting it to compare the actual integer values after dereferencing (A.V.all
= B.V.all). Therefore, the predefined equality function of the Simple_Rec type is useless
and dangerous for us, as it misleads us to expect something that it doesn't do.
After the assignment of A to B (B := A), the information that the application displays seems
to be correct — both A.V.all and B.V.all have the same value of two. However, when
assigning the value seven to B by calling Set (B, 7), we see that the value of A.V.all has
also changed. What's happening here is that the previous assignment (B := A) has actually
assigned access values (B.V := A.V), while we might have been expecting it to assign the
dereferenced values (B.V.all := A.V.all). Therefore, we cannot simply directly assign
objects of Simple_Rec type, as this operation changes the internal structure of the type
due to the presence of components of access type.
For these reasons, forbidding these operations for the Simple_Rec type is the most appro-
priate software design decision. If we still need assignment and equality operators, we can
implement custom subprograms for the limited type. We'll discuss this topic in the next
sections.
In addition to the case when we have components of access types, limited types are useful
for example when we want to avoid the situation in which the same information is copied
to multiple objects of the same type.
Assignments
Assignments are forbidden when using objects of limited types. For example:
9 private
10
15 end Limited_Types;
11 end Limited_Types;
3 procedure Show_Limited_Assignment is
4 A, B : Simple_Rec := Init (0);
5 begin
6 B := A;
7 end Show_Limited_Assignment;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Assignment_
↪Equality.Assignment
MD5: 019c16f7feac896fd8c37d40d0522dc8
Build output
In this example, we declare the limited private type Simple_Rec and two objects of this type
(A and B) in the Show_Limited_Assignment procedure. (We discuss more about limited
private types later (page 928)).
As expected, we get a compilation error for the B := A statement (in the
Show_Limited_Assignment procedure). If we need to copy two objects of limited type,
we have to provide a custom procedure to do that. For example, we can implement a Copy
procedure for the Simple_Rec type:
12 private
13
18 end Limited_Types;
19 end Limited_Types;
3 procedure Show_Limited_Assignment is
4 A, B : Simple_Rec := Init (0);
5 begin
6 Copy (From => A, To => B);
7 end Show_Limited_Assignment;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Assignment_
↪Equality.Assignment
MD5: 2c017c3592c93be8c19fe247e9241fcb
The Copy procedure from this example copies the dereferenced values of From to To, which
matches our expectation for the Simple_Rec. Note that we could have also implemented
a Shallow_Copy procedure to copy the actual access values (i.e. To.V := From.V). How-
ever, having this kind of procedure can be dangerous in many case, so this design decision
must be made carefully. In any case, using limited types ensures that only the assignment
subprograms that are explicitly declared in the package specification are available.
Equality
15 end Limited_Types;
11 end Limited_Types;
4 procedure Show_Limited_Equality is
5 A : Simple_Rec := Init (5);
6 B : Simple_Rec := Init (6);
7 begin
8 if A = B then
9 Put_Line ("A = B");
10 else
11 Put_Line ("A /= B");
12 end if;
13 end Show_Limited_Equality;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Assignment_
↪Equality.Equality
MD5: dad31b5e36de0b3b7824f723a60e5aa0
Build output
12 private
13
18 end Limited_Types;
18 end Limited_Types;
4 procedure Show_Limited_Equality is
5 A : Simple_Rec := Init (5);
6 B : Simple_Rec := Init (6);
7 begin
8 if A = B then
9 Put_Line ("A = B");
10 else
11 Put_Line ("A /= B");
12 end if;
13 end Show_Limited_Equality;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Assignment_
↪Equality.Equality
MD5: f56b2229443a5e4e33c402b41b02d318
Runtime output
A /= B
Here, the = operator compares the dereferenced values of Left.V and Right.V, which
matches our expectation for the Simple_Rec type. Declaring types as limited ensures that
we don't have unreasonable equality comparisons, and allows us to create reasonable re-
placements when required.
In other languages
In C++, you can overload the assignment operator. For example:
class Simple_Rec
{
public:
// Overloaded assignment
Simple_Rec& operator= (const Simple_Rec& obj);
private:
int *V;
};
In Ada, however, we can only define the equality operator (=). Defining the assignment
operator (:=) is not possible. The following code triggers a compilation error as expected:
package Limited_Types is
-- ...
end Limited_Types;
As we've seen in code examples from the previous section, we can apply information hiding
(page 307) to limited types. In other words, we can declare a type as limited private
instead of just limited. For example:
5 private
6
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Private
MD5: ececb364f5365a74db43952e9421dee0
In this case, in addition to the fact that assignments are forbidden for objects of this type
(because Rec is limited), we cannot access the record components.
Note that in this example, both partial and full views of the Rec record are of limited type.
In the next sections, we discuss how the partial and full views can have non-matching
declarations.
In principle, only record types can be declared limited, so we cannot use scalar or array
types. For example, the following declarations won't compile:
3 type Limited_Enumeration is
4 limited (Off, On);
5
9 type Integer_Array is
10 array (Positive range <>) of Integer;
11
15 end Non_Record_Limited_Error;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Non_Record_Limited_Error
MD5: c155e02d809caf28352cbbb579deb861
However, we've mentioned in a previous chapter (page 309) that private types don't have
to be record types necessarily. In this sense, limited private types makes it possible for us
to use types other than record types in the full view and still benefit from the restrictions
of limited types. For example:
3 type Limited_Enumeration is
4 limited private;
5
6 type Limited_Integer is
(continues on next page)
220 http://www.ada-auth.org/standards/22rm/html/RM-7-5.html
9 type Limited_Integer_Array_2 is
10 limited private;
11
12 private
13
18 type Integer_Array is
19 array (Positive range <>) of Integer;
20
21 type Limited_Integer_Array_2 is
22 new Integer_Array (1 .. 2);
23
24 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Non_Record_Limited
MD5: 9e65b56a5cb3d7a3da11c7f63ee9bb19
In the previous example, both partial and full views of the Rec type were limited. We may
actually declare a type as limited private (in the public part of a package), while its full
view is nonlimited. For example:
6 private
7
13 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Partial_Full_View
MD5: 5d0dbc3e87531476856f0ac1f9b22c78
In this case, only the partial view of Rec is limited, while its full view is nonlimited. When
deriving from Rec, the view of the derived type is the same as for the parent type:
9 end Simple_Recs.Child;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Partial_Full_View
MD5: fdf0ffa87ac2b8766830bf8e17ac7b5e
Clients must nevertheless comply with their partial view, and treat the type as if it is in fact
limited. In other words, if you use the Rec type in a subprogram or package outside of the
Simple_Recs package (or its child packages), the type is limited from that perspective:
3 procedure Use_Rec_In_Subprogram is
4 R1, R2 : Rec;
5 begin
6 R1.I := 1;
7 R2 := R1;
8 end Use_Rec_In_Subprogram;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Partial_Full_View
MD5: f0af323a951853b97a2b67ce9b13e732
Build output
Here, compilation fails because the type Rec is limited from the procedure's perspective.
Limitations
Note that the opposite — declaring a type as private and its full full view as limited
private — is not possible. For example:
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Partial_Full_View
MD5: ec1c8a2dcf3cc2c49b1497cf4c9d3a5a
Build output
As expected, we get a compilation error in this case. The issue is that the partial view cannot
be allowed to mislead the client about what's possible. In this case, if the partial view allows
assignment, then the full view must actually provide assignment. But the partial view can
restrict what is actually possible, so a limited partial view need not be completed in the full
view as a limited type.
In addition, tagged limited private types cannot have a nonlimited full view. For example:
5 private
6
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Partial_Full_View
MD5: cadb9ca1346a98fb65f9059fdb29f865
Build output
Here, compilation fails because the type Rec is nonlimited in its full view.
Declaring the full view of a type as limited or nonlimited has implications in the way we can
use objects of this type in the package body. For example:
6 procedure Copy
7 (From : Rec_Limited_Full;
8 To : in out Rec_Limited_Full);
9 procedure Copy
10 (From : Rec_Nonlimited_Full;
11 To : in out Rec_Nonlimited_Full);
12
13 private
14
23 end Simple_Recs;
3 procedure Copy
4 (From : Rec_Limited_Full;
5 To : in out Rec_Limited_Full)
6 is
7 begin
8 To := From;
9 -- ERROR: assignment is forbidden because
10 -- Rec_Limited_Full is limited in
11 -- its full view.
12 end Copy;
13
14 procedure Copy
15 (From : Rec_Nonlimited_Full;
16 To : in out Rec_Nonlimited_Full)
17 is
18 begin
19 To := From;
20 -- OK: assignment is allowed because
21 -- Rec_Nonlimited_Full is
22 -- nonlimited in its full view.
23 end Copy;
24
25 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Non_Limited_Partial_Full_View
MD5: 24b75bb97ddd485bd6825bb8647607c1
Build output
Another example mentioned by the Ada Reference Manual (7.3.1221 , 5/1) is about an array
type whose component type is limited private, but nonlimited in its full view. Let's see a
complete code example for that:
3 type Limited_Private is
4 limited private;
5
15 private
16
17 type Limited_Private is
18 record
19 A : Integer;
20 end record;
21
22 -- Limited_Private_Array type is
23 -- nonlimited at this point because
24 -- its component is nonlimited.
25 --
26 -- The assignments below are OK:
27 A1 : Limited_Private_Array (1 .. 5);
28
29 A2 : Limited_Private_Array := A1;
30
31 end Limited_Nonlimited_Arrays;
221 http://www.ada-auth.org/standards/22rm/html/RM-7-3-1.html
6 end Limited_Nonlimited_Arrays;
4 procedure Show_Limited_Nonlimited_Array is
5 A3 : Limited_Private_Array (1 .. 2) :=
6 (others => Init);
7 A4 : Limited_Private_Array (1 .. 2);
8 begin
9 -- ERROR: this assignment is illegal because
10 -- Limited_Private_Array is limited, as
11 -- its component is limited at this point.
12 A4 := A3;
13 end Show_Limited_Nonlimited_Array;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Limited_Nonlimited_Array
MD5: 211670e99e6e3a63a785bb2dde255b58
Build output
As we can see in this example, the limitedness of the array type Limited_Private_Array
depends on the limitedness of its component type Limited_Private. In the private
part of Limited_Nonlimited_Arrays package, where Limited_Private is nonlimited,
the array type Limited_Private_Array becomes nonlimited as well. In contrast, in the
Show_Limited_Nonlimited_Array, the array type is limited because its component is lim-
ited in that scope.
222 http://www.ada-auth.org/standards/22rm/html/RM-7-3-1.html
For tagged private types, the partial and full views must match: if a tagged type is limited
in the partial view, it must be limited in the full view. For example:
5 private
6
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Private_
↪Types.Tagged_Limited_Private_Types
MD5: bee48bd7e0d70ddfd288c0de5e21b039
Here, the tagged Rec type is limited both in its partial and full views. Any mismatch in
one of the views triggers a compilation error. (As an exercise, you may remove any of the
limited keywords from the code example and try to compile it.)
Under certain conditions, limited types can be called explicitly limited — note that using
the limited keyword in a part of the declaration doesn't necessary ensure this, as we'll
see later.
Let's start with an example of an explicitly limited type:
7 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Explicitly_Limited_
↪Types.Explicitly_Limited_Types
MD5: de73a20140628420830ed9fe0b2dedb5
The Rec type is also explicitly limited when it's declared limited in the private type's com-
pletion (in the package's private part):
5 private
6
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Explicitly_Limited_
↪Types.Explicitly_Limited_Types
MD5: ececb364f5365a74db43952e9421dee0
In this case, Rec is limited both in the partial and in the full view, so it's considered explicitly
limited.
However, as we've learned before (page 930), we may actually declare a type as limited
private in the public part of a package, while its full view is nonlimited. In this case, the
limited type is not consider explicitly limited anymore.
For example, if we make the full view of the Rec nonlimited (by removing the limited
keyword in the private part), then the Rec type isn't explicitly limited anymore:
5 private
6
11 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Explicitly_Limited_
↪Types.Explicitly_Limited_Types
MD5: bd54dec4f9b67d3d14d80511b3ac311f
Now, even though the Rec type was declared as limited private, the full view indicates that
it's actually a nonlimited type, so it isn't explicitly limited.
Note that tagged limited private types (page 936) are always explicitly limited types —
because, as we've learned before, they cannot have a nonlimited type declaration in its full
view.
6 subtype Limited_Integer_Array_2 is
7 Limited_Integer_Array (2);
8
9 private
10
11 type Integer_Array is
12 array (Positive range <>) of Integer;
13
19 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Limited_Subtype
MD5: 2a82c3c96fad2a01b9a8c15912d4b974
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Limited_Integer_Array_2;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
223 http://www.ada-auth.org/standards/22rm/html/RM-6-2.html
224 http://www.ada-auth.org/standards/22rm/html/RM-6-4-1.html
225 http://www.ada-auth.org/standards/22rm/html/RM-7-5.html
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Limited_Subtype
MD5: c24d07be96f27298a97e18d955cc6161
Build output
In this section, we discuss the implications of deriving from limited types. As usual, let's
start with a simple example:
7 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Type
MD5: cd23dfb69645ba5f1ebfdd65ee761ebe
In this example, the Rec_Derived type is derived from the Rec type. Note that the
Rec_Derived type is limited because its ancestor is limited, even though the limited key-
word doesn't show up in the declaration of the Rec_Derived type. Note that we could have
actually used the limited keyword here:
Therefore, we cannot use the assignment operator for objects of Rec_Derived type:
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Type
MD5: ce1b5fc8c96c4ede0cc6768b84296b51
Build output
7 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Type_Nonlimited_Ancestor
MD5: 78a7574cc6233ddc826359acb6e644ee
Build output
As expected, the compiler indicates that the ancestor Rec should be of limited type.
In fact, all types in a derivation class are the same — either limited or not. (That is especially
important with dynamic dispatching via tagged types. We discuss this topic in another
chapter.)
Of course, we can also derive from limited private types. However, there are more rules in
this case than the ones we've seen so far. Let's start with an example:
5 private
6
9 end Simple_Recs;
226 http://www.ada-auth.org/standards/22rm/html/RM-7-3.html
227 http://www.ada-auth.org/standards/22rm/html/RM-7-5.html
5 -- OR:
6 --
7 -- type Rec_Derived is
8 -- limited new Rec;
9
10 end Simple_Recs.Ext;
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Private_Type
MD5: c6eed14520589b9c1e11c17bd6179c19
Build output
Here, Rec_Derived is a limited type derived from the (limited private) Rec type. We can
verify that Rec_Derived type is limited because the compilation of the Test_Limitedness
procedure fails.
Up to this point, we have discussed explicitly limited types (page 936). Now, let's see how
derivation works with non-explicitly limited types.
Any type derived from a limited type is always limited, even if the full view of its ancestor is
nonlimited. For example, let's modify the full view of Rec and make it nonlimited (i.e. make
it not explicitly limited):
5 private
6
9 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Private_Type
MD5: 30a2a88ff46b7e528bb8d75d3d6ad6ce
Build output
simple_recs.ads:1: Simple_Recs cannot be used as a main program
gprbind: invocation of gnatbind failed
gprbuild: unable to bind simple_recs.ads
Here, Rec_Derived is a limited type because the partial view of Rec is limited. The fact that
the full view of Rec is nonlimited doesn't affect the Rec_Derived type — as we can verify
with the compilation error in the Test_Limitedness procedure.
Note, however, that a derived type becomes nonlimited in the private part or the body of
a child package if it isn't explicitly limited. In this sense, the derived type inherits the nonlim-
itedness of the parent's full view. For example, because we're declaring Rec_Derived as is
new Rec in the child package (Simple_Recs.Ext), we're saying that Rec_Derived is limited
outside this package, but nonlimited in the private part and body of the Simple_Recs.Ext
package. We can verify this by copying the code from the Test_Limitedness procedure to
a new procedure in the body of the Simple_Recs.Ext package:
15 end Simple_Recs.Ext;
3 procedure Test_Child_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 -- Here, Rec_Derived is a nonlimited
7 -- type because Rec is nonlimited in
8 -- its full view.
9
10 Dummy_2 := Dummy_1;
11 end Test_Child_Limitedness;
12
13 end Simple_Recs.Ext;
13 procedure Test_Limitedness is
14 -- Dummy_1, Dummy_2 : Rec_Derived;
15 begin
16 -- Dummy_2 := Dummy_1;
17 null;
18 end Test_Limitedness;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Limited_Private_Type
MD5: f480cd05afff622e451684a0293cb982
The rules for deriving from tagged limited private types are slightly different than the rules
we've seen so far. This is because tagged limited types are always explicitly limited types
(page 936).
Let's look at an example:
5 private
6
9 end Simple_Recs;
11 end Simple_Recs.Ext;
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Tagged_Limited_Private_Type
MD5: 81c8a010f093d8823b84bb6e69c4114e
Build output
In this example, Rec_Derived is a tagged limited type derived from the Tagged_Rec type.
(Again, we can verify the limitedness of the Rec_Derived type with the Test_Limitedness
procedure.)
As explained previously, the derived type (Rec_Derived) is a limited type, even though
the limited keyword doesn't appear in its declaration. We could, of course, include the
limited keyword in the declaration of Rec_Derived:
6 private
7
11 end Simple_Recs.Ext;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Tagged_Limited_Private_Type
MD5: b82a58a4bf9701b321000c52bf121977
Build output
(Obviously, if we include the limited keyword in the partial view of the derived type, we
must include it in its full view as well.)
The rules for limited interfaces are different from the ones for limited tagged types. In
contrast to the rule we've seen in the previous section, a type that is derived from a limited
type isn't automatically limited. In other words, it does not inherit the limitedness from the
interface. For example:
5 end Simple_Recs;
6 private
7
11 end Simple_Recs.Ext;
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
MD5: d9cf0bd26b86d0caec82eff2a2ec6ead
Here, Rec_Derived is derived from the limited Limited_IF interface. As we can see, the
Test_Limitedness compiles fine because Rec_Derived is nonlimited.
Of course, if we want Rec_Derived to be limited, we can make this explicit in the type
declaration:
6 private
7
11 end Simple_Recs.Ext;
3 procedure Test_Limitedness is
4 Dummy_1, Dummy_2 : Rec_Derived;
5 begin
6 Dummy_2 := Dummy_1;
7 end Test_Limitedness;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Deriving_From_
↪Limited_Types.Derived_Interface_Limited_Private
MD5: abb295cbfd5ade5f351991c2fbaf519c
Build output
According to the Annotated Ada Reference Manual, "an immutably limited type is a type
that cannot become nonlimited subsequently in a private part or in a child unit." In fact,
while we were talking about partial and full view of limited types (page 930), we've seen
that limited private types can become nonlimited in their full view. Such limited types are
not immutably limited.
The Annotated Ada Reference Manual also says that "if a view of the type makes it im-
mutably limited, then no copying (assignment) operations are ever available for objects of
the type. This allows other properties; for instance, it is safe for such objects to have access
discriminants that have defaults or designate other limited objects." We'll see examples of
this later on.
Immutably limited types include:
• explicitly limited types
• tagged limited types (i.e. with the keyword limited);
• tagged limited private type;
• limited private type that have at least one access discriminant (page 868) with a de-
fault expression;
• task types, protected types, and synchronized interfaces;
• any types derived from immutably limited types.
Let's look at a code example that shows instances of immutably limited types:
3 --
4 -- Explicitly limited type
5 --
6 type Explicitly_Limited_Rec is limited
7 record
8 A : Integer;
9 end record;
10
11 --
12 -- Tagged limited type
13 --
14 type Limited_Tagged_Rec is tagged limited
15 record
16 A : Integer;
17 end record;
18
19 --
20 -- Tagged limited private type
21 --
22 type Limited_Tagged_Private is
23 tagged limited private;
24
25 --
26 -- Limited private type with an access
27 -- discriminant that has a default
28 -- expression
29 --
30 type Limited_Rec_Access_D
31 (AI : access Integer := new Integer) is
32 limited private;
33
34 --
35 -- Task type
36 --
37 task type TT is
38 entry Start;
39 entry Stop;
40 end TT;
41
42 --
43 -- Protected type
44 --
45 protected type PT is
46 function Value return Integer;
47 private
48 A : Integer;
49 end PT;
50
51 --
52 -- Synchronized interface
53 --
54 type SI is synchronized interface;
55
56 --
57 -- A type derived from an immutably
58 -- limited type
59 --
60 type Derived_Immutable is new
(continues on next page)
63 private
64
70 type Limited_Rec_Access_D
71 (AI : access Integer := new Integer)
72 is limited
73 record
74 A : Integer;
75 end record;
76
77 end Show_Immutably_Limited_Types;
3 task body TT is
4 begin
5 accept Start;
6 accept Stop;
7 end TT;
8
9 protected body PT is
10 function Value return Integer is
11 (PT.A);
12 end PT;
13
14 end Show_Immutably_Limited_Types;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Immutably_Limited_
↪Types.Example
MD5: 6bcb9582a10eedc96040ab11cd320153
Build output
228 http://www.ada-auth.org/standards/22rm/html/RM-7-5.html
Not every limited type is immutably limited. We already mentioned untagged private lim-
ited types, which can become nonlimited in their full view. In addition, we have nonsyn-
chronized limited interface types. As mentioned earlier in this chapter, a type derived from
a nonsynchronized limited interface (page 945), can be nonlimited, so it's not immutably
limited.
In this section, we discuss the implications of using components of limited type. Let's start
by declaring a record component of limited type:
11 end Simple_Recs;
MD5: 71badd1e38cc4ff37f16d99dd203614b
As soon as we declare a record component of some limited type, the whole record is limited.
In this example, the Rec record is limited due to the presence of the IR component of limited
type.
Also, if we change the declaration of the Rec record from the previous example and remove
the limited keyword, the type itself remains implicitly limited. We can see that when trying
to assign to objects of Rec type in the Show_Implicitly_Limited procedure:
11 end Simple_Recs;
3 procedure Show_Implicitly_Limited is
4 A, B : Rec;
5 begin
6 B := A;
7 end Show_Implicitly_Limited;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Record_Components_
↪Limited_Type.Record_Components_Limited_Type
MD5: 39770daecfc4579407a799e14f9feff9
Build output
Here, the compiler indicates that the assignment is forbidden because the Rec type has
a component of limited type. The rationale for this rule is that an object of a limited type
doesn't allow assignment or equality, including the case in which that object is a component
of some enclosing composite object. If we allowed the enclosing object to be copied or
tested for equality, we'd be doing it for all the components, too.
Note: This section was originally written by Robert A. Duff and published as Gem #1:
Limited Types in Ada 2005232 and Gem #2233 .
Historically
Prior to Ada 2005, aggregates were illegal for limited types. Therefore, we would be faced
with a difficult choice: Make the type limited, and initialize it like this:
231 http://www.ada-auth.org/standards/22rm/html/RM-3-8.html
232 https://www.adacore.com/gems/gem-1
233 https://www.adacore.com/gems/gem-2
4 package Persons is
5
6 type Limited_Person;
7 type Limited_Person_Access is
8 access all Limited_Person;
9
15 end Persons;
6 procedure Show_Non_Aggregate_Init is
7 X : Limited_Person;
8 begin
9 X.Name := To_Unbounded_String ("John Doe");
10 X.Age := 25;
11 end Show_Non_Aggregate_Init;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.Full_Coverage_Rules_Limited_Ada95
MD5: fd3dcb6251f7b6912dafcca052932be2
which has the maintenance problem the full coverage rules are supposed to prevent. Or,
make the type nonlimited, and gain the benefits of aggregates, but lose the ability to pre-
vent copies.
Previously, we discussed full coverage rules for aggregates (page 448). They also apply to
limited types.
Historically
The full coverage rules have been aiding maintenance since Ada 83. However, prior to Ada
2005, we couldn't use them for limited types.
4 package Persons is
5
6 type Limited_Person;
7 type Limited_Person_Access is
8 access all Limited_Person;
9
18 end Persons;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.Full_Coverage_Rules_Limited
MD5: b8ece44a10d512061cb138be21e42034
This type has a self-reference; it doesn't make sense to copy objects, because Self would
end up pointing to the wrong place. Therefore, we would like to make the type limited, to
prevent developers from accidentally making copies. After all, the type is probably private,
so developers using this package might not be aware of the problem. We could also solve
that problem with controlled types, but controlled types are expensive, and add unneces-
sary complexity if not needed.
We can initialize objects of limited type with an aggregate. Here, we can say:
6 procedure Show_Aggregate_Box_Init is
7 X : aliased Limited_Person :=
8 (Self => <>,
9 Name =>
10 To_Unbounded_String ("John Doe"),
11 Age => 25,
12 Shoe_Size => 10);
13 begin
14 null;
15 end Show_Aggregate_Box_Init;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.Full_Coverage_Rules_Limited
MD5: ded40ff29b53ea5528efba94efaadbec
The Self => <> means use the default value of Limited_Person'Unchecked_Access. Since
Limited_Person appears inside the type declaration, it refers to the "current instance" of
the type, which in this case is X. Thus, we are setting X.Self to be X'Unchecked_Access.
One very important requirement should be noted: the implementation is required to build
the value of X in place; it cannot construct the aggregate in a temporary variable and then
copy it into X, because that would violate the whole point of limited objects — you can't
copy them.
Historically
Since Ada 2005, an aggregate is allowed to be limited; we can say:
5 procedure Show_Aggregate_Init is
6
7 X : aliased Limited_Person :=
8 (Self => null, -- Wrong!
9 Name =>
10 To_Unbounded_String ("John Doe"),
11 Age => 25,
12 Shoe_Size => 10);
13 begin
14 X.Self := X'Unchecked_Access;
15 end Show_Aggregate_Init;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.Full_Coverage_Rules_Limited
MD5: 793ee000fd777d0aa5c15e16132ec411
It seems uncomfortable to set the value of Self to the wrong value (null) and then correct
it. It also seems annoying that we have a (correct) default value for Self, but prior to
Ada 2005, we couldn't use defaults with aggregates. Since Ada 2005, a new syntax in
aggregates is available: <> means "use the default value, if any". Therefore, we can replace
Self => null by Self => <>.
Important
Note that using <> in an aggregate can be dangerous, because it can leave some compo-
nents uninitialized. <> means "use the default value". If the type of a component is scalar,
and there is no record-component default, then there is no default value.
For example, if we have an aggregate of type String, like this:
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.String_Box_Init
MD5: 28931ced4e1113d55bdc9dc64b42f70a
we end up with a 10-character string all of whose characters are invalid values. Note that
this is no more nor less dangerous than this:
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Aggregates.Dangerous_String
MD5: 6c26e9c8d5d031d4e6eac1ac8458f17e
Build output
Note: This section was originally written by Robert A. Duff and published as Gem #3234 .
Given that we can use build-in-place aggregates for limited types, the obvious next step is
to allow such aggregates to be wrapped in an abstraction — namely, to return them from
functions. After all, interesting types are usually private, and we need some way for clients
to create and initialize objects.
Historically
Prior to Ada 2005, constructor functions (that is, functions that create new objects and
return them) were not allowed for limited types. Since Ada 2005, fully-general constructor
functions are allowed.
234 https://www.adacore.com/gems/gem-3
4 package P is
5 task type Some_Task_Type;
6
18 end P;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Constructor_
↪Functions_Limited_Types.Constructor_Functions
MD5: 2e73eea0ba7852d45ba96dc1f6fae14d
3 procedure Show_Constructor_Function is
4 My_T : T := Make_T
5 (Name => "Bartholomew Cubbins");
6 begin
(continues on next page)
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Constructor_
↪Functions_Limited_Types.Constructor_Functions
MD5: 52801fafbd58fedbf268a6704008627b
As for aggregates, the result of Make_T is built in place (that is, in My_T), rather than being
created and then copied into My_T. Adding another level of function call, we can do:
3 procedure Show_Rumplestiltskin_Constructor is
4
10 Rumplestiltskin_Is_My_Name : constant T :=
11 Make_Rumplestiltskin;
12 begin
13 null;
14 end Show_Rumplestiltskin_Constructor;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Constructor_
↪Functions_Limited_Types.Constructor_Functions
MD5: d8d9e9f22a0f2f034057fe97f75eacfe
Historically
Note that Rumplestiltskin_Is_My_Name is constant. Prior to Ada 2005, it was impossible
to create a constant limited object, because there was no way to initialize it.
The (<>) on type T means that it has unknown discriminants from the point of view of the
client. This is a trick that prevents clients from creating default-initialized objects (that is,
X : T; is illegal). Thus clients must call Make_T whenever an object of type T is created,
giving package P full control over initialization of objects.
Ideally, limited and nonlimited types should be just the same, except for the essential differ-
ence: you can't copy limited objects (and there's no language-defined equality operator).
By allowing functions and aggregates for limited types, we're very close to this goal. Some
languages have a specific feature called constructor. In Ada, a constructor is just a function
that creates a new object.
Historically
Prior to Ada 2005, constructors only worked for nonlimited types. For limited types, the only
way to construct on declaration was via default values, which limits you to one constructor.
And the only way to pass parameters to that construction was via discriminants.
Consider the following package:
3 package Aux is
4 generic
5 with package OS is new
6 Ada.Containers.Ordered_Sets (<>);
7 function Gen_Singleton_Set
8 (Element : OS.Element_Type)
9 return OS.Set;
10 end Aux;
MD5: b715ae504c49ed59b7fd5ead4cc7bbb4
4 procedure Show_Set_Decl is
5
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Constructor_
↪Functions_Limited_Types.Constructor_Functions_2
MD5: 443fc3390b0f3e5516d91c80f16bed3f
whether or not Set is limited. This_Set : Set := Empty_Set; seems clearer than:
3 procedure Show_Set_Decl is
4
10 This_Set : Set;
11 begin
12 null;
13 end Show_Set_Decl;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Constructor_
↪Functions_Limited_Types.Constructor_Functions_2
MD5: e5b6c0e148cfdb1987ab3002ec1f53bd
which might mean "default-initialize to the empty set" or might mean "leave it uninitialized,
and we'll initialize it in later".
Note: This section was originally written by Robert A. Duff and published as Gem #10:
Limited Types in Ada 2005235 .
Previously, we discussed extended return statements (page 615). For most types, extended
return statements are no big deal — it's just syntactic sugar. But for limited types, this
syntax is almost essential:
8 end Task_Construct_Error;
18 end Task_Construct_Error;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Extended_Return_
↪Statements_Limited_Types.Extended_Return_Limited_Error
MD5: f55b1c367d2931ece4d352d209fe6b3b
The return statement here is illegal, because Result is local to Make_Task, and returning
it would involve a copy, which makes no sense (which is why task types are limited). Since
Ada 2005, we can write constructor functions for task types:
8 end Task_Construct;
19 end Task_Construct;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Extended_Return_
↪Statements_Limited_Types.Extended_Return_Limited
MD5: c91a24f09a76aef1c25d1a55bcbee910
3 procedure Show_Task_Construct is
4 My_Task : Task_Type := Make_Task (Val => 42);
5 begin
6 null;
7 end Show_Task_Construct;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Extended_Return_
↪Statements_Limited_Types.Extended_Return_Limited
MD5: 01809b031a844c829f2ead253864ca75
As mentioned in the previous section, the object of limited type returned by the initialization
function is built in place. In other words, the return object is built in the object that is the
target of the assignment statement.
For example, we can see this when looking at the address of the object returned by the
Init function, which we call to initialize the limited type Simple_Rec:
9 private
10
15 end Limited_Types;
19 end Limited_Types;
7 procedure Show_Limited_Init is
8 begin
9 declare
10 A : Simple_Rec := Init (0);
11 begin
12 Put_Line ("A'Address (local): "
13 & System.Address_Image
14 (A'Address));
15 end;
16 Put_Line ("----");
17
18 declare
19 B : Simple_Rec := Init (0);
20 begin
21 Put_Line ("B'Address (local): "
22 & System.Address_Image
23 (B'Address));
24 end;
25 end Show_Limited_Init;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Extended_Return_
↪Statements_Limited_Types.Initialization_Return_Do
MD5: 67235f804206e07fa4eba3a45cc1096f
Runtime output
When running this code example and comparing the address of the object E in the Init
function and the object that is being initialized in the Show_Limited_Init procedure, we see
that the return object E (of the Init function) and the local object in the Show_Limited_Init
procedure are the same object.
Important
When we use nonlimited types, we're actually copying the returned object — which was
locally created in the function — to the object that we're assigning the function to.
For example, let's modify the previous code and make Simple_Rec nonlimited:
10 private
11
16 end Non_Limited_Types;
19 end Non_Limited_Types;
5 with Non_Limited_Types;
6 use Non_Limited_Types;
7
8 procedure Show_Non_Limited_Init_By_Copy is
9 A, B : Simple_Rec;
10 begin
11 declare
12 A : Simple_Rec := Init (0);
13 begin
14 Put_Line ("A'Address (local): "
15 & System.Address_Image
16 (A'Address));
17 end;
18 Put_Line ("----");
19
20 declare
21 B : Simple_Rec := Init (0);
22 begin
23 Put_Line ("B'Address (local): "
24 & System.Address_Image
25 (B'Address));
26 end;
27 end Show_Non_Limited_Init_By_Copy;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.
↪Extended_Return_Statements_Limited_Types.Initialization_Return_
↪Copy
MD5: 6e224b64b90dabdf5064c70364fa80cb
Runtime output
In this case, we see that the local object E in the Init function is not the same as
the object it's being assigned to in the Show_Non_Limited_Init_By_Copy proce-
dure. In fact, E is being copied to A and B.
Note: This section was originally written by Robert A. Duff and published as Gem #11:
Limited Types in Ada 2005236 .
We've earlier seen examples of constructor functions for limited types similar to this:
4 package P is
5 task type Some_Task_Type;
6
18 end P;
236 https://www.adacore.com/gems/ada-gem-11
8 end P.Aux;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: 1956721292a82899d244afcd10ff63ed
It is useful to consider the various contexts in which these functions may be called. We've
already seen things like:
4 procedure Show_Rumplestiltskin_Constructor is
5 Rumplestiltskin_Is_My_Name : constant T :=
6 Make_Rumplestiltskin;
7 begin
8 null;
9 end Show_Rumplestiltskin_Constructor;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: 2fe193516df6452eccece8132660f8e5
in which case the limited object is built directly in a standalone object. This object will be
finalized whenever the surrounding scope is left.
We can also do:
4 procedure Show_Parameter_Constructor is
5 procedure Do_Something (X : T) is null;
6 begin
7 Do_Something (X => Make_Rumplestiltskin);
8 end Show_Parameter_Constructor;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: 61ccaefb4b7cfc42c065aa15543fc13b
Here, the result of the function is built directly in the formal parameter X of Do_Something.
X will be finalized as soon as we return from Do_Something.
4 procedure Show_Heap_Constructor is
5
8 Global : T_Ref;
9
10 procedure Heap_Alloc is
11 Local : T_Ref;
12 To_Global : Boolean := True;
13 begin
14 Local := new T'(Make_Rumplestiltskin);
15 if To_Global then
16 Global := Local;
17 end if;
18 end Heap_Alloc;
19
20 begin
21 null;
22 end Show_Heap_Constructor;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: 8eb794884f1dfbdbedf1bc4369f45cf8
The result of the function is built directly in the heap-allocated object, which will be finalized
when the scope of T_Ref is left (long after Heap_Alloc returns).
We can create another limited type with a component of type T, and use an aggregate:
4 procedure Show_Outer_Type is
5
11 Outer_Obj : Outer_Type :=
12 (This => Make_Rumplestiltskin,
13 That => Make_T (Name => ""));
14
15 begin
16 null;
17 end Show_Outer_Type;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: 00817649406492b79977d67eb0fd3955
As usual, the function results are built in place, directly in Outer_Obj.This and Outer_Obj.
That, with no copying involved.
The one case where we cannot call such constructor functions is in an assignment state-
ment:
4 procedure Show_Illegal_Constructor is
5 Rumplestiltskin_Is_My_Name : T;
6 begin
7 Rumplestiltskin_Is_My_Name :=
8 Make_T (Name => ""); -- Illegal!
9 end Show_Illegal_Constructor;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Building_Objects_
↪From_Constructors.Building_Objs_From_Constructors
MD5: f7b0c78e9fbe2e104b82dfff25ac3e3a
Build output
which is illegal because assignment statements involve copying. Likewise, we can't copy a
limited object into some other object:
4 procedure Show_Illegal_Constructor is
5 Rumplestiltskin_Is_My_Name : constant T :=
6 Make_T (Name => "");
7 Other : T :=
8 Rumplestiltskin_Is_My_Name; -- Illegal!
9 begin
10 null;
11 end Show_Illegal_Constructor;
Previously, we saw that parameters can be passed by copy or by reference (page 618).
Also, we discussed the concept of by-copy and by-reference types. Explicitly limited types
(page 936) are by-reference types. Consequently, parameters of these types are always
passed by reference.
would be working on copies of this object, not on the original one. This would lead to
timing-dependent bugs.
7 end Simple_Recs;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Parameters.Explicitly_Limited_Types
MD5: de73a20140628420830ed9fe0b2dedb5
In this example, Rec is a by-reference type because the type declaration is an explicit limited
record. Therefore, the parameter R of the Proc procedure is passed by reference.
We can run the Test application below and compare the address of the R object from Test
to the address of the R parameter of Proc to determine whether both R s refer to the same
object or not:
3 package Simple_Recs is
4
12 end Simple_Recs;
10 end Simple_Recs;
6 procedure Test is
7 R : Rec;
8
26 end Test;
Project: Courses.Advanced_Ada.Resource_Management.Limited_Types.Limited_Types_
↪Parameters.Explicitly_Limited_Types
MD5: d4fe2bb47d2223ef013d22aa305403e5
Runtime output
When running the Test application, we confirm that R was passed by reference. Note,
however, that the fact that R was passed by reference doesn't automatically imply that Rec
is a by-reference type: the type could have been ambiguous, and the compiler could have
just decided to pass the parameter by reference in this case.
Therefore, we have to rely on the rules specified in the Ada Reference Manual:
1. If a limited type is explicitly limited, a parameter of this type is a by-reference type.
• The rule applies to all kinds of explicitly limited types. For example, consider
private limited types where the type is declared limited in the private type's com-
pletion (in the package's private part): a parameter of this type is a by-reference
type.
2. If a limited type is not explicitly limited, a parameter of this type is neither a by-copy
nor a by-reference type.
• In this case, the decision whether the parameter is passed by reference or by copy
is made by the compiler.
238 http://www.ada-auth.org/standards/22rm/html/RM-6-4-1.html
239 http://www.ada-auth.org/standards/22rm/html/RM-7-5.html
Introduction To SPARK
971
Learning Ada
This tutorial is an interactive introduction to the SPARK programming language and its for-
mal verification tools. You will learn the difference between Ada and SPARK and how to use
the various analysis tools that come with SPARK.
This document was prepared by Claire Dross and Yannick Moy.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn web-
site241 . The directory structure in the zip file is based on the code block metadata. For
example, if you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
1. Unpack the zip file;
2. Go to target directory;
3. Start GNAT Studio on this directory;
4. Build (or compile) the project;
5. Run the application (if a main procedure is available in the project).
240 http://creativecommons.org/licenses/by-sa/4.0
241 https://learn.adacore.com/zip/learning-ada_code.zip
973
Learning Ada
974
CHAPTER
TWENTYNINE
SPARK OVERVIEW
This tutorial is an introduction to the SPARK programming language and its formal verifica-
tion tools. You need not know any specific programming language (although going over the
Introduction to Ada course (page 5) first may help) or have experience in formal verification.
Version 2012 of Ada introduced the use of aspects, which can be used for subprogram
contracts, and version 2014 of SPARK added its own aspects to further aid static analysis.
975
Learning Ada
The tool for formal verification of the SPARK language is called GNATprove. It checks for
conformance with the SPARK subset and performs flow analysis and proof of the source
code. Several other tools support the SPARK language, including both the GNAT compiler243
and the GNAT Studio integrated development environment244 .
We start with a simple example of a subprogram in Ada that uses SPARK aspects to specify
verifiable subprogram contracts. The subprogram, called Increment, adds 1 to the value
of its parameter X:
Listing 1: increment.ads
1 procedure Increment
2 (X : in out Integer)
3 with
4 Global => null,
5 Depends => (X => X),
6 Pre => X < Integer'Last,
7 Post => X = X'Old + 1;
Listing 2: increment.adb
1 procedure Increment
2 (X : in out Integer)
3 is
4 begin
5 X := X + 1;
6 end Increment;
Project: Courses.Intro_To_Spark.Overview.Trivial_Example
MD5: ce28b1facb44917b6cc208639c187064
242 https://www.adacore.com/codepeer
243 https://www.adacore.com/gnatpro
244 https://www.adacore.com/gnatpro/toolsuite/gps
Prover output
The contracts are written using the Ada aspect feature and those shown specify several
properties of this subprogram:
• The SPARK Global aspect says that Increment does not read or write any global vari-
ables.
• The SPARK Depend aspect is especially interesting for security: it says that the value
of the parameter X after the call depends only on the (previous) value of X.
• The Pre and Post aspects of Ada specify functional properties of Increment:
– Increment is only allowed to be called if the value of X prior to the call is less
than Integer'Last. This ensures that the addition operation performed in the
subprogram body doesn't overflow.
– Increment does indeed perform an increment of X: the value of X after a call is
one greater than its value before the call.
GNATprove can verify all of these contracts. In addition, it verifies that no error can be
raised at runtime when executing Increment's body.
It's important to understand why there are differences between the SPARK and Ada lan-
guages. The aim when designing the SPARK subset of Ada was to create the largest possible
subset of Ada that was still amenable to simple specification and sound verification.
The most notable restrictions from Ada are related to exceptions and access types, both of
which are known to considerably increase the amount of user-written annotations required
for full support. Backwards goto statements and controlled types are also not supported
since they introduce non-trivial control flow. The two remaining restrictions relate to side-
effects in expressions and aliasing of names, which we now cover in more detail.
29.6 Limitations
The SPARK language doesn't allow side-effects in expressions. In other words, evaluating
a SPARK expression must not update any object. This limitation is necessary to avoid un-
predictable behavior that depends on order of evaluation, parameter passing mechanisms,
or compiler optimizations. The expression for Dummy below is non-deterministic due to the
order in which the two calls to F are evaluated. It's therefore not legal SPARK.
Listing 3: show_illegal_ada_code.adb
1 procedure Show_Illegal_Ada_Code is
2
10 Dummy : Integer := 0;
11
12 begin
13 Dummy := F (Dummy) - F (Dummy); -- ??
14 end Show_Illegal_Ada_Code;
Project: Courses.Intro_To_Spark.Overview.Illegal_Ada_Code
MD5: a5cbf1824526857da94791ac1790200c
Build output
Prover output
In fact, the code above is not even legal Ada, so the same error is generated by the GNAT
compiler. But SPARK goes further and GNATprove also produces an error for the following
equivalent code that is accepted by the Ada compiler:
Listing 4: show_illegal_spark_code.adb
1 procedure Show_Illegal_SPARK_Code is
2
3 Dummy : Integer := 0;
4
12 begin
13 Dummy := F - F; -- ??
14 end Show_Illegal_SPARK_Code;
Project: Courses.Intro_To_Spark.Overview.Illegal_SPARK_Code
MD5: e747edb6ee147adb7fba97c9e7c8d5ef
Prover output
The SPARK languages enforces the lack of side-effects in expressions by forbidding side-
effects in functions, which include modifications to either parameters or global variables.
As a consequence, SPARK forbids functions with out or in out parameters in addition to
functions modifying a global variable. Function F below is illegal in SPARK, while Function
Incr might be legal if it doesn't modify any global variables and function Incr_And_Log
might be illegal if it modifies global variables to perform logging.
In most cases, you can easily replace these functions by procedures with an out parameter
that returns the computed value.
When it has access to function bodies, GNATprove verifies that those functions are indeed
free from side-effects. Here for example, the two functions Incr and Incr_And_Log have
the same signature, but only Incr is legal in SPARK. Incr_And_Log isn't: it attempts to
update the global variable Call_Count.
Listing 5: side_effects.ads
1 package Side_Effects is
2
7 end Side_Effects;
Listing 6: side_effects.adb
1 package body Side_Effects is
2
6 Call_Count : Natural := 0;
7
14 end Side_Effects;
Project: Courses.Intro_To_Spark.Overview.Side_Effects
MD5: 1b555e4b7bb519eea4df718a9356a2ed
Prover output
Another restriction imposed by the SPARK subset concerns aliasing245 . We say that two
names are aliased if they refer to the same object. There are two reasons why aliasing is
forbidden in SPARK:
• It makes verification more difficult because it requires taking into account the fact that
modifications to variables with different names may actually update the same object.
• Results may seem unexpected from a user point of view. The results of a subprogram
call may depend on compiler-specific attributes, such as parameter passing mecha-
nisms, when its parameters are aliased.
Aliasing can occur as part of the parameter passing that occurs in a subprogram call. Func-
tions have no side-effects in SPARK, so aliasing of parameters in function calls isn't prob-
lematic; we need only consider procedure calls. When a procedure is called, SPARK verifies
that no out or in out parameter is aliased with either another parameter of the procedure
or a global variable modified in the procedure's body.
Procedure Move_To_Total is an example where the possibility of aliasing wasn't taken into
account by the programmer:
Listing 7: no_aliasing.adb
1 procedure No_Aliasing is
2
3 Total : Natural := 0;
4
13 X : Natural := 3;
14
15 begin
16 Move_To_Total (X); -- OK
17 pragma Assert (Total = 3); -- OK
18 Move_To_Total (Total); -- flow analysis error
19 pragma Assert (Total = 6); -- runtime error
20 end No_Aliasing;
Project: Courses.Intro_To_Spark.Overview.Aliasing
MD5: 91038ef030fe27e3b000ab3db9c134ad
Prover output
245 https://en.wikipedia.org/wiki/Aliasing_(computing)
Runtime output
Move_To_Total adds the value of its input parameter Source to the global variable To-
tal and then resets Source to 0. The programmer has clearly not taken into account the
possibility of an aliasing between Total and Source. (This sort of error is quite common.)
This procedure itself is valid SPARK. When doing verification, GNATprove assumes, like the
programmer did, that there's no aliasing between Total and Source. To ensure this as-
sumption is valid, GNATprove checks for possible aliasing on every call to Move_To_Total.
Its final call in procedure No_Aliasing violates this assumption, which produces both a
message from GNATprove and a runtime error (an assertion violation corresponding to the
expected change in Total from calling Move_To_Total). Note that the postcondition of
Move_To_Total is not violated on this second call since integer parameters are passed by
copy and the postcondition is checked before the copy-back from the formal parameters to
the actual arguments.
Aliasing can also occur as a result of using access types ( pointers246 in Ada). These are
restricted in SPARK so that only benign aliasing is allowed, when both names are only used
to read the data. In particular, assignment between access objects operates a transfer of
ownership, where the source object loses its permission to read or write the underlying
allocated memory.
Procedure Ownership_Transfer is an example of code that is legal in Ada but rejected in
SPARK due to aliasing:
Listing 8: ownership_transfer.adb
1 procedure Ownership_Transfer is
2 type Int_Ptr is access Integer;
3 X : Int_Ptr;
4 Y : Int_Ptr;
5 Dummy : Integer;
6 begin
7 X := new Integer'(1);
8 X.all := X.all + 1;
9 Y := X;
10 Y.all := Y.all + 1;
11 X.all := X.all + 1; -- illegal
12 X.all := 1; -- illegal
13 Dummy := X.all; -- illegal
14 end Ownership_Transfer;
Project: Courses.Intro_To_Spark.Overview.Ownership_Transfer
MD5: 951fe1c930d43a5009e607994ae0dd03
Prover output
After the assignment of X to Y, variable X cannot be used anymore to read or write the
underlying allocated memory.
Note: For more details on these limitations, see the SPARK User's Guide247 .
Since the SPARK language is restricted to only allow easily specifiable and verifiable con-
structs, there are times when you can't or don't want to abide by these limitations over
your entire code base. Therefore, the SPARK tools only check conformance to the SPARK
subset on code which you identify as being in SPARK.
You do this by using an aspect named SPARK_Mode. If you don't explicitly specify otherwise,
SPARK_Mode is Off, meaning you can use the complete set of Ada features in that code and
that it should not be analyzed by GNATprove. You can change this default either selectively
(on some units or subprograms or packages inside units) or globally (using a configuration
pragma, which is what we're doing in this tutorial). To allow simple reuse of existing Ada
libraries, entities declared in imported units with no explicit SPARK_Mode can still be used
from SPARK code. The tool only checks for SPARK conformance on the declaration of those
entities which are actually used within the SPARK code.
Here's a common case of using the SPARK_Mode aspect:
package P
with SPARK_Mode => On
is
-- package spec is IN SPARK, so can be used by SPARK clients
end P;
package body P
with SPARK_Mode => Off
is
-- body is NOT IN SPARK, so is ignored by GNATprove
end P;
The package P only defines entities whose specifications are in the SPARK subset. However,
it wants to use all Ada features in its body. Therefore the body should not be analyzed and
has its SPARK_Mode aspect set to Off.
You can specify SPARK_Mode in a fine-grained manner on a per-unit basis. An Ada package
has four different components: the visible and private parts of its specification and the
declarative and statement parts of its body. You can specify SPARK_Mode as being either On
or Off on any of those parts. Likewise, a subprogram has two parts: its specification and
its body.
247 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/language_restrictions.html#
language-restrictions
A general rule in SPARK is that once SPARK_Mode has been set to Off, it can never be switched
On again in the same part of a package or subprogram. This prevents setting SPARK_Mode
to On for local units of a unit with SPARK_Mode Off and switching back to SPARK_Mode On
for a part of a given unit where it was set fo Off in a previous part.
Note: For more details on the use of SPARK_Mode, see the SPARK User's Guide248 .
29.8.1 Example #1
Here's a package defining an abstract stack type (defined as a private type in SPARK) of
Element objects along with some subprograms providing the usual functionalities of stacks.
It's marked as being in the SPARK subset.
Listing 9: stack_package.ads
1 package Stack_Package
2 with SPARK_Mode => On
3 is
4 type Element is new Natural;
5 type Stack is private;
6
11 private
12 type Stack is record
13 Top : Integer;
14 -- ...
15 end record;
16
17 end Stack_Package;
Project: Courses.Intro_To_Spark.Overview.Example_01
MD5: 2b15e13e850435fb93406054d70b51c6
Prover output
Side-effects in expressions are not allowed in SPARK. Therefore, Pop is not allowed to modify
its parameter S.
248 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/spark_mode.html
29.8.2 Example #2
Let's turn to an abstract state machine version of a stack, where the unit provides a single
instance of a stack. The content of the stack (global variables Content and Top) is not
directly visible to clients. In this stripped-down version, only the function Pop is available
to clients. The package spec and body are marked as being in the SPARK subset.
8 end Global_Stack;
7 Content : Element_Array;
8 Top : Natural;
9
17 end Global_Stack;
Project: Courses.Intro_To_Spark.Overview.Example_02
MD5: 8c4eb564643eef48264b5e43a6f580b9
Prover output
global_stack.ads:6:13: error: function with output global "Top" is not allowed in␣
↪SPARK
As above, functions should be free from side-effects. Here, Pop updates the global variable
Top, which is not allowed in SPARK.
29.8.3 Example #3
We now consider two procedures: Permute and Swap. Permute applies a circular permuta-
tion to the value of its three parameters. Swap then uses Permute to swap the value of X
and Y.
3 procedure Test_Swap
4 with SPARK_Mode => On
5 is
6 A : Integer := 1;
7 B : Integer := 2;
8 begin
9 Swap (A, B);
10 end Test_Swap;
Build output
p.adb:14:19: error: writable actual for "Y" overlaps with actual for "Z"
gprbuild: *** compilation phase failed
Prover output
Phase 1 of 2: generation of Global contracts ...
p.adb:14:19: error: writable actual for "Y" overlaps with actual for "Z"
gnatprove: error during generation of Global contracts
Here, the values for parameters Y and Z are aliased in the call to Permute, which is not
allowed in SPARK. In fact, in this particular case, this is even a violation of Ada rules so the
same error is issued by the Ada compiler.
In this example, we see the reason why aliasing is not allowed in SPARK: since Y and Z are
Positive, they are passed by copy and the result of the call to Permute depends on the
order in which they're copied back after the call.
29.8.4 Example #4
Here, the Swap procedure is used to swap the value of the two record components of R.
16 end P;
Project: Courses.Intro_To_Spark.Overview.Example_04
MD5: ae4d3ebe8dd1a8f67f35cedffdea2ac9
Prover output
This code is correct. The call to Swap is safe: two different components of the same record
can't refer to the same object.
29.8.5 Example #5
Here's a slight modification of the previous example using an array instead of a record:
Swap_Indexes calls Swap on values stored in the array A.
16 end P;
Project: Courses.Intro_To_Spark.Overview.Example_05
MD5: 62a95179572e36443995ff54a2d5ef08
Prover output
GNATprove detects a possible case of aliasing. Unlike the previous example, it has no way
of knowing that the two elements A (I) and A (J) are actually distinct when we call Swap.
GNATprove issues a check message here instead of an error, giving you the possibility of
justifying the message after review (meaning that you've verified manually that this can't,
in fact, occur).
29.8.6 Example #6
We now consider a package declaring a type Dictionary, an array containing a word per
letter. The procedure Store allows us to insert a word at the correct index in a dictionary.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is new Ada.Finalization.Controlled with record
8 Ptr : access String;
9 end record;
10 type Dictionary is array (Letter) of String_Access;
11
Project: Courses.Intro_To_Spark.Overview.Example_06
MD5: 9175bcd1474e2143462b860c01d8602e
Prover output
This code is not correct: controlled types are not part of the SPARK subset. The solution
here is to use SPARK_Mode to separate the definition of String_Access from the rest of the
code in a fine grained manner.
29.8.7 Example #7
Here's a new version of the previous example, which we've modified to hide the controlled
type inside the private part of package P, using pragma SPARK_Mode (Off) at the start of
the private part.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is private;
8 type Dictionary is array (Letter) of String_Access;
9
14 private
15 pragma SPARK_Mode (Off);
16
Project: Courses.Intro_To_Spark.Overview.Example_07
MD5: cb04206c9734eb95f6444757d005dae2
Prover output
Since the controlled type is defined and used inside of a part of the code ignored by GNAT-
prove, this code is correct.
29.8.8 Example #8
Let's put together the new spec for package P with the body of P seen previously.
3 package P
4 with SPARK_Mode => On
5 is
6 subtype Letter is Character range 'a' .. 'z';
7 type String_Access is private;
8 type Dictionary is array (Letter) of String_Access;
9
14 private
15 pragma SPARK_Mode (Off);
16
Prover output
Phase 1 of 2: generation of Global contracts ...
p.adb:1:01: error: incorrect application of SPARK_Mode at /vagrant/frontend/dist/
↪test_output/projects/Courses/Intro_To_Spark/Overview/Example_08/
↪dacb2d50d0ddc6c620ee9945cb819369/main_spark.adc:12
p.adb:1:01: error: value Off was set for SPARK_Mode on "P" at p.ads:15
p.adb:2:08: error: incorrect use of SPARK_Mode
p.adb:2:08: error: value Off was set for SPARK_Mode on "P" at p.ads:15
gnatprove: error during generation of Global contracts
The body of Store doesn't actually use any construct that's not in the SPARK subset, but we
nevertheless can't set SPARK_Mode to On for P's body because it has visibility to P's private
part, which is not in SPARK, even if we don't use it.
29.8.9 Example #9
Next, we moved the declaration and the body of the procedure Store to another package
named Q.
3 package P
4 with SPARK_Mode => On
5 is
(continues on next page)
12 private
13 pragma SPARK_Mode (Off);
14
Project: Courses.Intro_To_Spark.Overview.Example_09
MD5: b397e82987c100de5a53ede16fbef37f
Prover output
And now everything is fine: we've managed to retain the use of the controlled type while
having most of our code in the SPARK subset so GNATprove is able to analyze it.
Our final example is a package with two functions to search for the value 0 inside an array
A. The first raises an exception if 0 isn't found in A while the other simply returns 0 in that
case.
Project: Courses.Intro_To_Spark.Overview.Example_10
MD5: 4b9656698ab1d42cebc72817f8a00637
Prover output
This code is perfectly correct, despite the use of exception handling, because we've care-
fully isolated this non-SPARK feature in a function body marked with a SPARK_Mode of Off
so it's ignored by GNATprove. However, GNATprove tries to show that Not_Found is never
raised in Search_Zero_P, producing a message about a possible exception being raised.
Looking at Search_Zero_N, it's indeed likely that an exception is meant to be raised in
some cases, which means you need to verify that Not_Found is only raised when appropri-
ate using other methods such as peer review or testing.
THIRTY
FLOW ANALYSIS
In this section we present the flow analysis capability provided by the GNATprove tool, a
critical tool for using SPARK.
Flow analysis concentrates primarily on variables. It models how information flows through
them during a subprogram's execution, connecting the final values of variables to their
initial values. It analyzes global variables declared at library level, local variables, and
formal parameters of subprograms.
Nesting of subprograms creates what we call scope variables: variables declared locally to
an enclosing unit. From the perspective of a nested subprogram, scope variables look very
much like global variables
Flow analysis is usually fast, roughly as fast as compilation. It detects various types of
errors and finds violations of some SPARK legality rules, such as the absence of aliasing and
freedom of expressions from side-effects. We discussed these rules in the SPARK Overview
(page 975).
Flow analysis is sound: if it doesn't detect any errors of a type it's supposed to detect, we
know for sure there are no such errors.
We now present each class of errors detected by flow analysis. The first is the reading
of an uninitialized variable. This is nearly always an error: it introduces non-determinism
and breaks the type system because the value of an uninitialized variable may be outside
the range of its subtype. For these reasons, SPARK requires every variable to be initialized
before being read.
Flow analysis is responsible for ensuring that SPARK code always fulfills this requirement.
For example, in the function Max_Array shown below, we've neglected to initialize the value
of Max prior to entering the loop. As a consequence, the value read by the condition of the
if statement may be uninitialized. Flow analysis detects and reports this error.
Listing 1: show_uninitialized.ads
1 package Show_Uninitialized is
2
993
Learning Ada
7 end Show_Uninitialized;
Listing 2: show_uninitialized.adb
1 package body Show_Uninitialized is
2
14 end Show_Uninitialized;
Project: Courses.Intro_To_Spark.Flow_Analysis.Uninitialized
MD5: 82fe32cbe33e25bac5466f86ee2e03c4
Prover output
Note: For more details on how flow analysis verifies data initialization, see the SPARK
User's Guide249 .
Ineffective statements are different than dead code: they're executed, and often even
modify the value of variables, but have no effect on any of the subprogram's visible out-
puts: parameters, global variables or the function result. Ineffective statements should be
avoided because they make the code less readable and more difficult to maintain.
More importantly, they're often caused by errors in the program: the statement may have
been written for some purpose, but isn't accomplishing that purpose. These kinds of errors
can be difficult to detect in other ways.
For example, the subprograms Swap1 and Swap2 shown below don't properly swap their
two parameters X and Y. This error caused a statement to be ineffective. That ineffective
statement is not an error in itself, but flow analysis produces a warning since it can be
indicative of an error, as it is here.
249 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/language_restrictions.html#
data-initialization-policy
Listing 3: show_ineffective_statements.ads
1 package Show_Ineffective_Statements is
2
8 end Show_Ineffective_Statements;
Listing 4: show_ineffective_statements.adb
1 package body Show_Ineffective_Statements is
2
11 Tmp : T := 0;
12
20 end Show_Ineffective_Statements;
Project: Courses.Intro_To_Spark.Flow_Analysis.Ineffective_Statements
MD5: 473a9215e9e98bd25147998d43847a12
Prover output
So far, we've seen examples where flow analysis warns about ineffective statements and
unused variables.
Parameter modes are an important part of documenting the usage of a subprogram and
affect the code generated for that subprogram. Flow analysis checks that each specified
parameter mode corresponds to the usage of that parameter in the subprogram's body. It
checks that an in parameter is never modified, either directly or through a subprogram call,
checks that the initial value of an out parameter is never read in the subprogram (since
it may not be defined on subprogram entry), and warns when an in out parameter isn't
modified or when its initial value isn't used. All of these may be signs of an error.
We see an example below. The subprogram Swap is incorrect and GNATprove warns about
an input which isn't read:
Listing 5: show_incorrect_param_mode.ads
1 package Show_Incorrect_Param_Mode is
2
7 end Show_Incorrect_Param_Mode;
Listing 6: show_incorrect_param_mode.adb
1 package body Show_Incorrect_Param_Mode is
2
10 end Show_Incorrect_Param_Mode;
Project: Courses.Intro_To_Spark.Flow_Analysis.Incorrect_Param_Mode
MD5: 1e33dbf461daab0daed01c83025232fc
Prover output
In SPARK, unlike Ada, you should declare an out parameter to be in out if it's not modified
on every path, in which case its value may depend on its initial value. SPARK is stricter than
Ada to allow more static detection of errors. This table summarizes SPARK's valid parameter
modes as a function of whether reads and writes are done to the parameter.
Initial value read Written on some path Written on every path Parameter mode
X in
X X in out
X X in out
X in out
X out
So far, none of the verifications we've seen require you to write any additional annotations.
However, flow analysis also checks flow annotations that you write. In SPARK, you can
specify the set of global and scoped variables accessed or modified by a subprogram. You
do this using a contract named Global.
When you specify a Global contract for a subprogram, flow analysis checks that it's both
correct and complete, meaning that no variables other than those stated in the contract
are accessed or modified, either directly or through a subprogram call, and that all those
listed are accessed or modified. For example, we may want to specify that the function
Get_Value_Of_X reads the value of the global variable X and doesn't access any other
global variable. If we do this through a comment, as is usually done in other languages,
GNATprove can't verify that the code complies with this specification:
package Show_Global_Contracts is
X : Natural := 0;
end Show_Global_Contracts;
You write global contracts as part of the subprogram specification. In addition to their value
in flow analysis, they also provide useful information to users of a subprogram. The value
you specify for the Global aspect is an aggregate-like list of global variable names, grouped
together according to their mode.
In the example below, the procedure Set_X_To_Y_Plus_Z reads both Y and Z. We indicate
this by specifying them as the value for Input. It also writes X, which we specify using
Output. Since Set_X_To_X_Plus_Y both writes X and reads its initial value, X's mode is
In_Out. Like parameters, if no mode is specified in a Global aspect, the default is Input.
We see this in the case of the declaration of Get_Value_Of_X. Finally, if a subprogram,
such as Incr_Parameter_X, doesn't reference any global variables, you set the value of
the global contract to null.
Listing 7: show_global_contracts.ads
1 package Show_Global_Contracts is
2
3 X, Y, Z : Natural := 0;
4
20 end Show_Global_Contracts;
Project: Courses.Intro_To_Spark.Flow_Analysis.Global_Contracts
MD5: 2cbf90f2d27b6b0043a2e29449e79df9
Prover output
Note: For more details on global contracts, see the SPARK User's Guide250 .
You may also supply a Depends contract for a subprogram to specify dependencies between
its inputs and outputs. These dependencies include not only global variables but also pa-
rameters and the function's result. When you supply a Depends contract for a subprogram,
flow analysis checks that it's correct and complete, that is, for each dependency you list,
the variable depends on those listed and on no others.
For example, you may want to say that the new value of each parameter of Swap, shown
below, depends only on the initial value of the other parameter and that the value of X after
the return of Set_X_To_Zero doesn't depend on any global variables. If you indicate this
through a comment, as you often do in other languages, GNATprove can't verify that this
is actually the case.
package Show_Depends_Contracts is
X : Natural;
procedure Set_X_To_Zero;
-- The value of X after the call depends on no input
end Show_Depends_Contracts;
Like Global contracts, you specify a Depends contract in subprogram declarations using
an aspect. Its value is a list of one or more dependency relations between the outputs
and inputs of the subprogram. Each relation is represented as two lists of variable names
separated by an arrow. On the left of each arrow are variables whose final value depends
on the initial value of the variables you list on the right.
For example, here we indicate that the final value of each parameter of Swap depends only
on the initial value of the other parameter. If the subprogram is a function, we list its result
as an output, using the Result attribute, as we do for Get_Value_Of_X below.
250 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
data-dependencies
Listing 8: show_depends_contracts.ads
1 package Show_Depends_Contracts is
2
5 X, Y, Z : T := 0;
6
33 end Show_Depends_Contracts;
Project: Courses.Intro_To_Spark.Flow_Analysis.Depends_Contracts
MD5: 290866c4208b6deff717a402bc2aef34
Prover output
Often, the final value of a variable depends on its own initial value. You can specify this in
a concise way using the + character, as we did in the specification of Set_X_To_X_Plus_Y
above. If there's more than one variable on the left of the arrow, a + means each variables
depends on itself, not that they all depend on each other. You can write the corresponding
dependency with (=> +) or without (=>+) whitespace.
If you have a program where an input isn't used to compute the final value of any output,
you express that by writting null on the left of the dependency relation, as we did for the
Do_Nothing subprogram above. You can only write one such dependency relation, which
lists all unused inputs of the subprogram, and it must be written last. Such an annotation
also silences flow analysis' warning about unused parameters. You can also write null on
the right of a dependency relation to indicate that an output doesn't depend on any input.
We do that above for the procedure Set_X_To_Zero.
Note: For more details on depends contracts, see the SPARK User's Guide251 .
30.4 Shortcomings
30.4.1 Modularity
Flow analysis is sound, meaning that if it doesn't output a message on some analyzed
SPARK code, you can be assured that none of the errors it tests for can occur in that code.
On the other hand, flow analysis often issues messages when there are, in fact, no errors.
The first, and probably most common reason for this relates to modularity.
To scale flow analysis to large projects, verifications are usually done on a per-subprogram
basis, including detection of uninitialized variables. To analyze this modularly, flow analysis
needs to assume the initialization of inputs on subprogram entry and modification of outputs
during subprogram execution. Therefore, each time a subprogram is called, flow analysis
checks that global and parameter inputs are initialized and each time a subprogram returns,
it checks that global and parameter outputs were modified.
This can produce error messages on perfectly correct subprograms. An example is
Set_X_To_Y_Plus_Z below, which only sets its out parameter X when Overflow is False.
Listing 9: set_x_to_y_plus_z.adb
1 procedure Set_X_To_Y_Plus_Z
2 (Y, Z : Natural;
3 X : out Natural;
4 Overflow : out Boolean)
5 is
6 begin
7 if Natural'Last - Z < Y then
8 Overflow := True; -- X should be initialized on every path
9 else
10 Overflow := False;
11 X := Y + Z;
12 end if;
13 end Set_X_To_Y_Plus_Z;
Project: Courses.Intro_To_Spark.Flow_Analysis.Set_X_To_Y_Plus_Z
MD5: be47cd769d2a7267c0bd1bb2ef0d6328
Prover output
↪[possible fix: initialize "X" on all paths or make "X" an IN OUT parameter]
The message means that flow analysis wasn't able to verify that the program didn't read an
uninitialized variable. To solve this problem, you can either set X to a dummy value when
there's an overflow or manually verify that X is never used after a call to Set_X_To_Y_Plus_Z
that returned True as the value of Overflow.
251 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
flow-dependencies
Another common cause of false alarms is caused by the way flow analysis handles com-
posite types. Let's start with arrays.
Flow analysis treats an entire array as single object instead of one object per element,
so it considers modifying a single element to be a modification of the array as a whole.
Obviously, this makes reasoning about which global variables are accessed less precise
and hence the dependencies of those variables are also less precise. This also affects the
ability to accurately detect reads of uninitialized data.
It's sometimes impossible for flow analysis to determine if an entire array object has been
initialized. For example, after we write code to initialize every element of an unconstrained
array A in chunks, we may still receive a message from flow analysis claiming that the array
isn't initialized. To resolve this issue, you can either use a simpler loop over the full range
of the array, or (even better) an aggregate assignment, or, if that's not possible, verify
initialization of the object manually.
9 end Show_Composite_Types_Shortcoming;
26 end Show_Composite_Types_Shortcoming;
Project: Courses.Intro_To_Spark.Flow_Analysis.Composite_Types_Shortcoming
MD5: a366dcdd141191466027b2b928560c5e
Prover output
↪return] [possible fix: initialize "A" on all paths, make "A" an IN OUT parameter␣
Flow analysis is more precise on record objects because it tracks the value of each compo-
nent of a record separately within a single subprogram. So when a record object is initialized
by successive assignments of its components, flow analysis knows that the entire object is
initialized. However, record objects are still treated as single objects when analyzed as an
input or output of a subprogram.
10 end Show_Record_Flow_Analysis;
10 end Show_Record_Flow_Analysis;
Project: Courses.Intro_To_Spark.Flow_Analysis.Record_Flow_Analysis_1
MD5: 24cd553b87b737536912b1bb780f6402
Prover output
Flow analysis complains when a procedure call initializes only some components of a record
object. It'll notify you of uninitialized components, as we see in subprogram Init_F2 below.
11 end Show_Record_Flow_Analysis;
3 procedure Init_F2
4 (R : in out Rec) is
5 begin
6 R.F2 := 0;
7 end Init_F2;
8
15 end Show_Record_Flow_Analysis;
Project: Courses.Intro_To_Spark.Flow_Analysis.Record_Flow_Analysis_2
MD5: efeecb787bf9d68977ed9701689cd6c4
Prover output
Flow analysis is not value-dependent: it never reasons about the values of expressions, only
whether they have been set to some value or not. As a consequence, if some execution
path in a subprogram is impossible, but the impossibility can only be determined by looking
at the values of expressions, flow analysis still considers that path feasible and may emit
messages based on it believing that execution along such a path is possible.
For example, in the version of Absolute_Value below, flow analysis computes that R is
uninitialized on a path that enters neither of the two conditional statements. Because it
doesn't consider values of expressions, it can't know that such a path is impossible.
Project: Courses.Intro_To_Spark.Flow_Analysis.Absolute_Value_1
MD5: 69c233d22afdfdac679bf379b353a8d4
Prover output
To avoid this problem, you should make the control flow explicit, as in this second version
of Absolute_Value:
Project: Courses.Intro_To_Spark.Flow_Analysis.Absolute_Value_2
MD5: 9c773547f81e82a7aa1b45132b105937
Prover output
The final cause of unexpected flow messages that we'll discuss also comes from inaccuracy
in computations of contracts. As we explained earlier, both Global and Depends contracts
are optional, but GNATprove uses their data for some of its analysis.
For example, flow analysis can't detect reads from uninitialized variables without knowing
the set of variables accessed. It needs to analyze and check both the Depends contracts
you wrote for a subprogram and those you wrote for callers of that subprogram. Since
each flow contract on a subprogram depends on the flow contracts of all the subprograms
called inside its body, this computation can often be quite time-consuming. Therefore, flow
analysis sometimes trades-off the precision of this computation against the time a more
precise computation would take.
This is the case for Depends contracts, where flow analysis simply assumes the worst, that
each subprogram's output depends on all of that subprogram's inputs. To avoid this as-
sumption, all you have to do is supply contracts when default ones are not precise enough.
You may also want to supply Global contracts to further speed up flow analysis on larger
programs.
30.5.1 Example #1
5 procedure Search_Array
6 (A : Array_Of_Positives;
7 E : Positive;
8 Result : out Integer;
9 Found : out Boolean);
10
11 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Integer;
7 Found : out Boolean) is
8 begin
9 for I in A'Range loop
10 if A (I) = E then
11 Result := I;
12 Found := True;
13 return;
14 end if;
15 end loop;
(continues on next page)
19 end Show_Search_Array;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_01
MD5: d2a27a5bde247767e2f6cd2d42a2d629
Prover output
↪parameter]
30.5.2 Example #2
5 Not_Found : exception;
6
7 procedure Search_Array
8 (A : Array_Of_Positives;
9 E : Positive;
10 Result : out Integer);
11 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Integer) is
7 begin
8 for I in A'Range loop
(continues on next page)
17 end Show_Search_Array;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_02
MD5: fa159faeb68974b1af3de2112e086b16
Prover output
Flow analysis doesn't emit any messages in this case, meaning it can verify that Result
can't be read in SPARK code while uninitialized. But why is that, since Result is still not
initialized when E is not in A? This is because the exception, Not_Found, can never be caught
within SPARK code (SPAK doesn't allow exception handlers). However, the GNATprove tool
also tries to ensure the absence of runtime errors in SPARK code, so tries to prove that
Not_Found is never raised. When it can't do that here, it produces a different message.
30.5.3 Example #3
In this example, we're using a discriminated record for the result of Search_Array instead
of conditionally raising an exception. By using such a structure, the place to store the index
at which E was found exists only when E was indeed found. So if it wasn't found, there's
nothing to be initialized.
13 procedure Search_Array
14 (A : Array_Of_Positives;
15 E : Positive;
16 Result : out Search_Result)
17 with Pre => not Result'Constrained;
18
19 end Show_Search_Array;
3 procedure Search_Array
4 (A : Array_Of_Positives;
5 E : Positive;
6 Result : out Search_Result) is
7 begin
8 for I in A'Range loop
9 if A (I) = E then
10 Result := (Found => True,
11 Content => I);
12 return;
13 end if;
14 end loop;
15 Result := (Found => False);
16 end Search_Array;
17
18 end Show_Search_Array;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_03
MD5: 1d5ec5d78185fd75499b90b3d21f8ae2
Prover output
This example is correct and flow analysis doesn't issue any message: it can verify both that
no uninitialized variables are read in Search_Array's body, and that all its outputs are set
on return. We've used the attribute Constrained in the precondition of Search_Array to
indicate that the value of the Result in argument can be set to any variant of the record
type Search_Result, specifically to either the variant where E was found and where it
wasn't.
30.5.4 Example #4
8 end Show_Biggest_Increasing_Sequence;
24 Biggest_Seq : Natural := 0;
25
26 begin
27 for I in A'Range loop
28 Test_Index (I);
29 if End_Of_Seq then
30 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
31 end if;
32 end loop;
33 return Biggest_Seq;
34 end Size_Of_Biggest_Increasing_Sequence;
35
36 end Show_Biggest_Increasing_Sequence;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_04
MD5: e6083665827d9dee4e00bdce4c1e962f
Prover output
However, this example is not correct. Flow analysis emits messages for Test_Index stating
that Max, Beginning, and Size_Of_Seq should be initialized before being read. Indeed,
when you look carefully, you see that both Max and Beginning are missing initializations
because they are read in Test_Index before being written. As for Size_Of_Seq, we only
read its value when End_Of_Seq is true, so it actually can't be read before being written,
but flow analysis isn't able to verify its initialization by using just flow information.
The call to Test_Index is automatically inlined by GNATprove, which leads to another mes-
sages above. If GNATprove couldn't inline the call to Test_Index, for example if it was
defined in another unit, the same messages would be issued on the call to Test_Index.
30.5.5 Example #5
In the following example, we model permutations as arrays where the element at index I is
the position of the I'th element in the permutation. The procedure Init initializes a permu-
tation to the identity, where the I'th elements is at the I'th position. Cyclic_Permutation
calls Init and then swaps elements to construct a cyclic permutation.
12 end Show_Permutation;
30 end Show_Permutation;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_05
MD5: 219b06617c636c18543128d77f90fcee
Prover output
↪initialize "A" on all paths, make "A" an IN OUT parameter or annotate it with␣
↪aspect Relaxed_Initialization]
This program is correct. However, flow analysis will nevertheless still emit messages be-
cause it can't verify that every element of A is initialized by the loop in Init. This message
is a false alarm. You can either ignore it or justify it safely.
30.5.6 Example #6
This program is the same as the previous one except that we've changed the mode of A
in the specification of Init to in out to avoid the message from flow analysis on array
assignment.
12 end Show_Permutation;
30 end Show_Permutation;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_06
MD5: 61406d9a66dda71630c74c12f3d67936
Prover output
This program is not correct. Changing the mode of a parameter that should really be out
to in out to silence a false alarm is not a good idea. Not only does this obfuscate the
specification of Init, but flow analysis emits a message on the procedure where A is not
initialized, as shown by the message in Cyclic_Permutation.
30.5.7 Example #7
9 end Show_Increments;
20 begin
21 for I in A'Range loop
22 if I > A'First then
23 Threshold := A (I - 1);
24 end if;
25 Incr_Until_Threshold (I);
26 end loop;
27 end Incr_Step_Function;
28
29 end Show_Increments;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_07
MD5: 8e28a005cd9d78947e4bfc60db708bf5
Prover output
Everything is fine here. Specifically, the Global contract is correct. It mentions both
Threshold, which is read but not written in the procedure, and A, which is both read and
written. The fact that A is a parameter of an enclosing unit doesn't prevent us from using it
inside the Global contract; it really is global to Incr_Until_Threshold. We didn't mention
Increment since it's a static constant.
30.5.8 Example #8
We now go back to the procedure Test_Index from Example #4 (page 1008) and correct
the missing initializations. We want to know if the Global contract of Test_Index is correct.
8 end Show_Biggest_Increasing_Sequence;
28 Biggest_Seq : Natural := 0;
29
30 begin
31 for I in A'Range loop
32 Test_Index (I);
33 if End_Of_Seq then
34 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
35 end if;
36 end loop;
37 return Biggest_Seq;
38 end Size_Of_Biggest_Increasing_Sequence;
39
40 end Show_Biggest_Increasing_Sequence;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_08
MD5: 86fb934c32a38f6841ef736780b2e3b2
Prover output
30.5.9 Example #9
Next, we change the Global contract of Test_Index into a Depends contract. In general,
we don't need both contracts because the set of global variables accessed can be deduced
from the Depends contract.
8 end Show_Biggest_Increasing_Sequence;
28 Biggest_Seq : Natural := 0;
29
30 begin
31 for I in A'Range loop
32 Test_Index (I);
33 if End_Of_Seq then
34 Biggest_Seq := Natural'Max (Size_Of_Seq, Biggest_Seq);
35 end if;
36 end loop;
37 return Biggest_Seq;
38 end Size_Of_Biggest_Increasing_Sequence;
39
40 end Show_Biggest_Increasing_Sequence;
Project: Courses.Intro_To_Spark.Flow_Analysis.Example_09
MD5: d54ac5d4266738b1bf64869131644b33
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
show_biggest_increasing_sequence.adb:7:07: info: initialization of "End_Of_Seq"␣
↪proved
The subprogram Identity swaps the value of its parameter two times. Its Depends contract
says that the final value of X only depends on its initial value and likewise for Y.
9 end Show_Swap;
16 end Show_Swap;
Prover output
This code is correct, but flow analysis can't verify the Depends contract of Identity because
we didn't supply a Depends contract for Swap. Therefore, flow analysis assumes that all
outputs of Swap, X and Y, depend on all its inputs, both X and Y's initial values. To prevent
this, we should manually specify a Depends contract for Swap.
THIRTYONE
This section presents the proof capability of GNATprove, a major tool for the SPARK lan-
guage. We focus here on the simpler proofs that you'll need to write to verify your pro-
gram's integrity. The primary objective of performing proof of your program's integrity is
to ensure the absence of runtime errors during its execution.
The analysis steps discussed here are only sound if you've previously performed Flow Anal-
ysis (page 993). You shouldn't proceed further if you still have unjustified flow analysis
messages for your program.
There's always the potential for errors that aren't detected during compilation to occur
during a program's execution. These errors, called runtime errors, are those targeted by
GNATprove.
There are various kinds of runtime errors, the most common being references that are out
of the range of an array ( buffer overflow252 in Ada), subtype range violations, overflows in
computations, and divisions by zero. The code below illustrates many examples of possible
runtime errors, all within a single statement. Look at the assignment statement setting the
I + J'th cell of an array A to the value P /Q.
Listing 1: show_runtime_errors.ads
1 package Show_Runtime_Errors is
2
7 end Show_Runtime_Errors;
Listing 2: show_runtime_errors.adb
1 package body Show_Runtime_Errors is
2
8 end Show_Runtime_Errors;
1019
Learning Ada
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Runtime_Errors
MD5: c0718b8cb6138b84a99e0040e2a9164e
Prover output
↪bits machine integer] [possible fix: add precondition (if J >= 0 then I <=␣
↪errors.ads:5]
↪add precondition (if J >= 0 then I <= A'Last - J else I >= A'First - J) to␣
↪subprogram at show_runtime_errors.ads:5]
↪subprogram at show_runtime_errors.ads:5]
↪subprogram at show_runtime_errors.ads:5]
There are quite a number of errors that may occur when executing this code. If we don't
know anything about the values of I, J, P, and Q, we can't rule out any of those errors.
First, the computation of I + J can overflow, for example if I is Integer'Last and J is
positive.
A (Integer'Last + 1) := P / Q;
Next, the sum, which is used as an array index, may not be in the range of the index of the
array.
A (A'Last + 1) := P / Q;
On the other side of the assignment, the division may also overflow, though only in the
very special case where P is Integer'First and Q is -1 because of the asymmetric range
of signed integer types.
A (I + J) := Integer'First / -1;
A (I + J) := P / 0;
Finally, since the array contains natural numbers, it's also an error to store a negative value
in it.
A (I + J) := 1 / -1;
The compiler generates checks in the executable code corresponding to each of those run-
time errors. Each check raises an exception if it fails. For the above assignment statement,
we can see examples of exceptions raised due to failed checks for each of the different
cases above.
A (Integer'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : overflow check failed
A (A'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : index check failed
A (I + J) := Integer'First / (-1);
-- raised CONSTRAINT_ERROR : overflow check failed
A (I + J) := 1 / (-1);
-- raised CONSTRAINT_ERROR : range check failed
A (I + J) := P / 0;
-- raised CONSTRAINT_ERROR : divide by zero
These runtime checks are costly, both in terms of program size and execution time. It may
be appropriate to remove them if we can statically ensure they aren't needed at runtime,
in other words if we can prove that the condition tested for can never occur.
This is where the analysis done by GNATprove comes in. It can be used to demonstrate
statically that none of these errors can ever occur at runtime. Specifically, GNATprove log-
ically interprets the meaning of every instruction in the program. Using this interpretation,
GNATprove generates a logical formula called a verification condition for each check that
would otherwise be required by the Ada (and hence SPARK) language.
A (Integer'Last + 1) := P / Q;
-- medium: overflow check might fail
A (A'Last + 1) := P / Q;
-- medium: array index check might fail
A (I + J) := Integer'First / (-1);
-- medium: overflow check might fail
A (I + J) := 1 / (-1);
-- medium: range check might fail
A (I + J) := P / 0;
-- medium: divide by zero might fail
GNATprove then passes these verification conditions to an automatic prover, stated as con-
ditions that must be true to avoid the error. If every such condition can be validated by a
prover (meaning that it can be mathematically shown to always be true), we've been able
to prove that no error can ever be raised at runtime when executing that program.
31.2 Modularity
In the same way, when a subprogram is called, GNATprove assumes its out and in out pa-
rameters and the global variables it writes can be modified in any way compatible with their
postconditions. For example, since Increment has no postcondition, GNATprove doesn't
know that the value of X after the call is always less than Integer'Last. Therefore, it can't
prove that the addition following the call to Increment can't overflow.
Listing 3: show_modularity.adb
1 procedure Show_Modularity is
2
10 X : Integer;
11 begin
12 X := Integer'Last - 2;
13 Increment (X);
14 -- After the call, GNATprove no longer knows the value of X
15
16 X := X + 1;
17 -- medium: overflow check might fail
18 end Show_Modularity;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_modularity.adb:6:14: info: overflow check proved
show_modularity.adb:10:04: info: initialization of "X" proved
show_modularity.adb:13:04: info: precondition proved
show_modularity.adb:16:11: medium: overflow check might fail, cannot prove upper␣
↪bound for X + 1 [reason for check: result of addition must fit in a 32-bits␣
↪machine integer] [possible fix: call at line 13 should mention X (for argument␣
↪X) in a postcondition]
31.2.1 Exceptions
There are two cases where GNATprove doesn't require modularity and hence doesn't make
the above assumptions. First, local subprograms without contracts can be inlined if they're
simple enough and are neither recursive nor have multiple return points. If we remove the
contract from Increment, it fits the criteria for inlining.
Listing 4: show_modularity.adb
1 procedure Show_Modularity is
2
9 X : Integer;
10 begin
11 X := Integer'Last - 2;
12 Increment (X);
13 X := X + 1;
14 -- info: overflow check proved
15 end Show_Modularity;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Modularity_2
MD5: 448d576897c3e4606cd4b90621aad63a
Prover output
GNATprove now sees the call to Increment exactly as if the increment on X was done outside
that call, so it can successfully verify that neither addition can overflow.
Note: For more details on contextual analysis of subprograms, see the SPARK User's
Guide253 .
The other case involves functions. If we define a function as an expression function, with or
without contracts, GNATprove uses the expression itself as the postcondition on the result
of the function.
In our example, replacing Increment with an expression function allows GNATprove to suc-
cessfully verify the overflow check in the addition.
Listing 5: show_modularity.adb
1 procedure Show_Modularity is
2
8 X : Integer;
9 begin
10 X := Integer'Last - 2;
11 X := Increment (X);
12 X := X + 1;
13 -- info: overflow check proved
14 end Show_Modularity;
253 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/how_to_write_subprogram_
contracts.html#contextual-analysis-of-subprograms-without-contracts
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Modularity_3
MD5: b2b67845362929472e4e23867fcbd5e7
Prover output
Note: For more details on expression functions, see the SPARK User's Guide254 .
31.3 Contracts
Ada contracts are perfectly suited for formal verification, but are primarily designed to be
checked at runtime. When you specify the -gnata switch, the compiler generates code that
verifies the contracts at runtime. If an Ada contract isn't satisfied for a given subprogram
call, the program raises the Assert_Failure exception. This switch is particularly useful
during development and testing, but you may also retain run-time execution of assertions,
and specifically preconditions, during the program's deployment to avoid an inconsistent
state.
Consider the incorrect call to Increment below, which violates its precondition. One way
to detect this error is by compiling the function with assertions enabled and testing it with
inputs that trigger the violation. Another way, one that doesn't require guessing the needed
inputs, is to run GNATprove.
Listing 6: show_precondition_violation.adb
1 procedure Show_Precondition_Violation is
2
9 X : Integer;
10
11 begin
12 X := Integer'Last;
13 Increment (X);
14 end Show_Precondition_Violation;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Precondition_Violation
MD5: 60cb889128fc6bca10e21b1baf041258
Prover output
254 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/specification_features.html#
expression-functions
Runtime output
Similarly, consider the incorrect implementation of function Absolute below, which violates
its postcondition. Likewise, one way to detect this error is by compiling the function with
assertions enabled and testing with inputs that trigger the violation. Another way, one
which again doesn't require finding the inputs needed to demonstrate the error, is to run
GNATprove.
Listing 7: show_postcondition_violation.adb
1 procedure Show_Postcondition_Violation is
2
11 X : Integer;
12
13 begin
14 X := 1;
15 Absolute (X);
16 end Show_Postcondition_Violation;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Postcondition_Violation
MD5: fb1340de7e082d801f177bd8a0cf90a6
Prover output
Runtime output
The benefits of dynamically checking contracts extends beyond making testing easier. Early
failure detection also allows an easier recovery and facilitates debugging, so you may want
to enable these checks at runtime to terminate execution before some damaging or hard-
to-debug action occurs.
GNATprove statically analyses preconditions and postconditions. It verifies preconditions
every time a subprogram is called, which is the runtime semantics of contracts. Postcon-
ditions, on the other hand, are verified once as part of the verification of the subprogram's
body. For example, GNATprove must wait until Increment is improperly called to detect
the precondition violation, since a precondition is really a contract for the caller. On the
other hand, it doesn't need Absolute to be called to detect that its postcondition doesn't
hold for all its possible inputs.
Note: For more details on pre and postconditions, see the SPARK User's Guide255 .
Expressions in Ada contracts have the same semantics as Boolean expressions elsewhere,
so runtime errors can occur during their computation. To simplify both debugging of as-
sertions and combining testing and static verification, the same semantics are used by
GNATprove.
While proving programs, GNATprove verifies that no error can ever be raised during the
execution of the contracts. However, you may sometimes find those semantics too heavy,
in particular with respect to overflow checks, because they can make it harder to specify
an appropriate precondition. We see this in the function Add below.
Listing 8: show_executable_semantics.adb
1 procedure Show_Executable_Semantics
2 with SPARK_Mode => On
3 is
4 function Add (X, Y : Integer) return Integer is (X + Y)
5 with Pre => X + Y in Integer;
6
7 X : Integer;
8 begin
9 X := Add (Integer'Last, 1);
10 end Show_Executable_Semantics;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Executable_Semantics
MD5: d85fa0507d7c35fb98ade7815020117e
Build output
Prover output
Runtime output
255 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
preconditions
GNATprove issues a message on this code warning about a possible overflow when com-
puting the sum of X and Y in the precondition. Indeed, since expressions in assertions have
normal Ada semantics, this addition can overflow, as you can easily see by compiling and
running the code that calls Add with arguments Integer'Last and 1.
On the other hand, you sometimes may prefer GNATprove to use the mathematical seman-
tics of addition in contracts while the generated code still properly verifies that no error is
ever raised at runtime in the body of the program. You can get this behavior by using the
compiler switch -gnato?? (for example -gnato13), which allows you to independently set
the overflow mode in code (the first digit) and assertions (the second digit). For both, you
can either reduce the number of overflow checks (the value 2), completely eliminate them
(the value 3), or preserve the default Ada semantics (the value 1).
Note: For more details on overflow modes, see the SPARK User's Guide256 .
As we've seen, a key feature of SPARK is that it allows us to state properties to check
using assertions and contracts. SPARK supports preconditions and postconditions as well
as assertions introduced by the Assert pragma.
The SPARK language also includes new contract types used to assist formal verification.
The new pragma Assume is treated as an assertion during execution but introduces an
assumption when proving programs. Its value is a Boolean expression which GNATprove
assumes to be true without any attempt to verify that it's true. You'll find this feature useful,
but you must use it with great care. Here's an example of using it.
Listing 9: incr.adb
1 procedure Incr (X : in out Integer) is
2 begin
3 pragma Assume (X < Integer'Last);
4 X := X + 1;
5 end Incr;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Pragma_Assume
MD5: bfbc4b8aca259d7516b6acaee571f8c2
Prover output
Note: For more details on pragma Assume, see the SPARK User's Guide257 .
The Contract_Cases aspect is another construct introduced for GNATprove, but which also
acts as an assertion during execution. It allows you to specify the behavior of a subprogram
256 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/overflow_modes.html
257 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/assertion_pragmas.html#
pragma-assume
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Absolute
MD5: 5ac868f35be18bb6fffe2444ecbea28d
Prover output
Note: For more details on Contract_Cases, see the SPARK User's Guide258 .
GNATprove may report an error while verifying a program for any of the following reasons:
• there might be an error in the program; or
• the property may not be provable as written because more information is required; or
• the prover used by GNATprove may be unable to prove a perfectly valid property.
We spend the remainder of this section discussing the sometimes tricky task of debugging
failed proof attempts.
258 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/subprogram_contracts.html#
contract-cases
First, let's discuss the case where there's indeed an error in the program. There are two
possibilities: the code may be incorrect or, equally likely, the specification may be incor-
rect. As an example, there's an error in our procedure Incr_Until below which makes its
Contract_Cases unprovable.
10 end Show_Failed_Proof_Attempt;
13 end Show_Failed_Proof_Attempt;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Failed_Proof_Attempt_1
MD5: 814636ae9df6f4f66ad69f5099a5729b
Prover output
Since this is an assertion that can be executed, it may help you find the problem if you run
the program with assertions enabled on representative sets of inputs. This allows you to
find bugs in both the code and its contracts. In this case, testing Incr_Until with an input
greater than 1000 raises an exception at runtime.
10 end Show_Failed_Proof_Attempt;
13 end Show_Failed_Proof_Attempt;
3 procedure Main is
4 Dummy : Integer;
5 begin
6 Dummy := 0;
7 Incr_Until (Dummy);
8
9 Dummy := 1000;
10 Incr_Until (Dummy);
11 end Main;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_failed_proof_attempt.ads:7:21: medium: contract case might fail
show_failed_proof_attempt.ads:8:21: medium: contract case might fail
gnatprove: unproved check messages considered as errors
Runtime output
The error message shows that the first contract case is failing, which means that Incre-
mented is True. However, if we print the value of Incremented before returning, we see
that it's False, as expected for the input we provided. The error here is that guards of con-
tract cases are evaluated before the call, so our specification is wrong! To correct this, we
should either write X < 1000 as the guard of the first case or use a standard postcondition
with an if-expression.
Even if both the code and the assertions are correct, GNATprove may still report that it can't
prove a verification condition for a property. This can happen for two reasons:
• The property may be unprovable because the code is missing some assertion. One
category of these cases is due to the modularity of the analysis which, as we discussed
above, means that GNATprove only knows about the properties of your subprograms
that you have explicitly written.
• There may be some information missing in the logical model of the program used by
GNATprove.
Let's look at the case where the code and the specification are correct but there's some
information missing. As an example, GNATprove finds the postcondition of Increase to be
unprovable.
3 C : Natural := 100;
4
8 end Show_Failed_Proof_Attempt;
14 end Show_Failed_Proof_Attempt;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Failed_Proof_Attempt_3
MD5: e01fc27a981bcb80757f30c94768237e
Prover output
This postcondition is a conditional. It says that if the parameter (X) is less than a certain
value (C), its value will be increased by the procedure while if it's greater, its value will be
set to C (saturated). When C has the value 100, the code of Increases adds 10 to the value
of X if it was initially less than 90, increments X by 1 if it was between 90 and 99, and sets
X to 100 if it was greater or equal to 100. This behavior does satisfy the postcondition, so
why is the postcondition not provable?
The values in the counterexample returned by GNATprove in its message gives us a clue:
C = 0 and X = 10 and X'Old = 0. Indeed, if C is not equal to 100, our reasoning above
is incorrect: the values of 0 for C and X on entry indeed result in X being 10 on exit, which
violates the postcondition!
We probably didn't expect the value of C to change, or at least not to go below 90. But,
in that case, we should have stated so by either declaring C to be constant or by adding a
precondition to the Increase subprogram. If we do either of those, GNATprove is able to
prove the postcondition.
Finally, there are cases where GNATprove provides a perfectly valid verification condition
for a property, but it's nevertheless not proved by the automatic prover that runs in the
later stages of the tool's execution. This is quite common. Indeed, GNATprove produces
its verification conditions in first-order logic, which is not decidable, especially in combina-
tion with the rules of arithmetic. Sometimes, the automatic prover just needs more time.
Other times, the prover will abandon the search almost immediately or loop forever without
reaching a conclusive answer (either a proof or a counterexample).
For example, the postcondition of our GCD function below — which calculates the value of the
GCD of two positive numbers using Euclide's algorithm — can't be verified with GNATprove's
default settings.
8 end Show_Failed_Proof_Attempt;
14 end Show_Failed_Proof_Attempt;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Failed_Proof_Attempt_4
MD5: a6f1a39ceb0793df8a00691d59a5d9ce
Prover output
The first thing we try is increasing the amount of time the prover is allowed to spend on
each verification condition using the --timeout option of GNATprove (e.g., by using the
dialog box in GNAT Studio). In this example, increasing it to one minute, which is relatively
high, doesn't help. We can also specify an alternative automatic prover — if we have one
— using the option --prover of GNATprove (or the dialog box). For our postcondition, we
tried Alt-Ergo, cvc5, and Z3 without any luck.
8 end Show_Failed_Proof_Attempt;
25 end Show_Failed_Proof_Attempt;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Failed_Proof_Attempt_5
MD5: 954ecbf2177705770c3a44a477c1de17
Prover output
To better understand the reason for the failure, we added intermediate assertions to simplify
the proof and pin down the part that's causing the problem. Adding such assertions is often
a good idea when trying to understand why a property is not proved. Here, provers can't
verify that if both A - B and B can be divided by Result so can A. This may seem surprising,
but non-linear arithmetic, involving, for example, multiplication, modulo, or exponentiation,
is a difficult topic for provers and is not handled very well in practice by any of the general-
purpose ones like Alt-Ergo, cvc5, or Z3.
Note: For more details on how to investigate unproved checks, see the SPARK User's
Guide259 .
31.5.1 Example #1
The package Lists defines a linked-list data structure. We call Link(I,J) to make a link
from index I to index J and call Goes_To(I,J) to determine if we've created a link from
index I to index J. The postcondition of Link uses Goes_To to state that there must be a
link between its arguments once Link completes.
7 procedure Link (I, J : Index) with Post => Goes_To (I, J);
8
9 private
(continues on next page)
259 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/how_to_investigate_unproved_
checks.html
22 Memory : Cell_Array;
23
24 end Lists;
16 end Lists;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_01
MD5: c2246948c584304d5694b49b4d1fd0fc
Prover output
↪expression function]
This example is correct, but can't be verified by GNATprove. This is because Goes_To itself
has no postcondition, so nothing is known about its result.
31.5.2 Example #2
7 procedure Link (I, J : Index) with Post => Goes_To (I, J);
8
9 private
10
22 Memory : Cell_Array;
23
27 end Lists;
8 end Lists;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_02
MD5: c65953bbe8a5f9fb77a4d94e2dd875f9
Prover output
GNATprove can fully prove this version: Goes_To is an expression function, so its body is
available for proof (specifically, for creating the postcondition needed for the proof).
31.5.3 Example #3
The package Stacks defines an abstract stack type with a Push procedure that adds an
element at the top of the stack and a function Peek that returns the content of the element
at the top of the stack (without removing it).
9 private
10
23 end Stacks;
9 S.Top := S.Top + 1;
10 S.Content (S.Top) := E;
11 end Push;
12
13 end Stacks;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_03
MD5: 917d624916c5ef14c4e454d6c56414fd
Prover output
This example isn't correct. The postcondition of Push is only satisfied if the stack isn't full
when we call Push.
31.5.4 Example #4
We now change the behavior of Push so it raises an exception when the stack is full instead
of returning.
5 Is_Full_E : exception;
6
11 private
12
25 end Stacks;
9 S.Top := S.Top + 1;
10 S.Content (S.Top) := E;
11 end Push;
12
13 end Stacks;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_04
MD5: b573ebe93f85ea171166b6953cbb8956
Prover output
The postcondition of Push is now proved because GNATprove only considers execution paths
leading to normal termination. But it issues a message warning that exception Is_Full_E
may be raised at runtime.
31.5.5 Example #5
Let's add a precondition to Push stating that the stack shouldn't be full.
5 Is_Full_E : exception;
6
13 private
14
28 end Stacks;
12 end Stacks;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_05
MD5: 63c2dfd68dd5acd91d8d497206e7423e
Prover output
This example is correct. With the addition of the precondition, GNATprove can now verify
that Is_Full_E can never be raised at runtime.
31.5.6 Example #6
The package Memories defines a type Chunk that models chunks of memory. Each element
of the array, represented by its index, corresponds to one data element. The procedure
Read_Record reads two pieces of data starting at index From out of the chunk represented
by the value of Memory.
10 end Memories;
20 begin
21 Data1 := Read_One (From, 1);
22 Data2 := Read_One (From, 2);
23 end Read_Record;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_06
MD5: aec8014dc291708999092fa123ee7416
Prover output
↪machine integer] [possible fix: use pragma Overflow_Mode or switch -gnato13 or␣
↪unit Ada.Numerics.Big_Numerics.Big_Integers]
This example is correct, but it can't be verified by GNATprove, which analyses Read_One on
its own and notices that an overflow may occur in its precondition in certain contexts.
31.5.7 Example #7
10 end Memories;
21 begin
22 Data1 := Read_One (From, 1);
23 Data2 := Read_One (From, 2);
24 end Read_Record;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_07
MD5: 6b4c6a41b652ad76bc7ef8934dcd9bfc
Prover output
This example is also not correct: unfortunately, our attempt to correct Read_One's precon-
dition failed. For example, an overflow will occur at runtime if First is Integer'Last and
Memory'Last is negative. This is possible here because type Chunk uses Integer as base
index type instead of Natural or Positive.
31.5.8 Example #8
10 end Memories;
18 begin
19 Data1 := Read_One (From, 1);
20 Data2 := Read_One (From, 2);
21 end Read_Record;
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_08
MD5: 5a806fb84b50d2dc1f2af428b1bc8d0a
Prover output
This example is correct and fully proved. We could have fixed the contract of Read_One to
correctly handle both positive and negative values of Memory'Last, but we found it simpler
to let the function be inlined for proof by removing its precondition.
31.5.9 Example #9
The procedure Compute performs various computations on its argument. The computation
performed depends on its input range and is reflected in its contract, which we express
using a Contract_Cases aspect.
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_09
MD5: 51962d1bb6dd1b081ed498dd11559685
Prover output
This example isn't correct. We duplicated the content of Compute's body in its contract.
This is incorrect because the semantics of Contract_Cases require disjoint cases, just like
a case statement. The counterexample returned by GNATprove shows that X = 0 is covered
by two different case-guards (the first and the second).
Project: Courses.Intro_To_Spark.Proof_of_Program_Integrity.Example_10
MD5: 01d33b10fd60f384ffa4ae8fea1e7d87
Prover output
This example is still not correct. GNATprove can successfully prove the different cases
are disjoint and also successfully verify each case individually. This isn't enough, though:
a Contract_Cases must cover all cases. Here, we forgot the value -200, which is what
GNATprove reports in its counterexample.
THIRTYTWO
STATE ABSTRACTION
Abstraction is a key concept in programming that can drastically simplify both the imple-
mentation and maintenance of code. It's particularly well suited to SPARK and its modular
analysis. This section explains what state abstraction is and how you use it in SPARK. We
explain how it impacts GNATprove's analysis both in terms of information flow and proof of
program properties.
State abstraction allows us to:
• express dependencies that wouldn't otherwise be expressible because some data
that's read or written isn't visible at the point where a subprogram is declared — ex-
amples are dependencies on data, for which we use the Global contract, and on flow,
for which we use the Depends contract.
• reduce the number of variables that need to be considered in flow analysis and proof,
a reduction which may be critical in order to scale the analysis to programs with thou-
sands of global variables.
Listing 1: increase.ads
1 procedure Increase (X : in out Integer) with
2 Global => null,
3 Pre => X <= 100,
4 Post => X'Old < X;
Listing 2: increase.adb
1 procedure Increase (X : in out Integer) is
2 begin
3 X := X + 1;
4 end Increase;
1045
Learning Ada
Project: Courses.Intro_To_Spark.State_Abstraction.No_Abstraction
MD5: c4c8f229aeb1b5c12744d26369a8603f
Prover output
We've written a specification of the subprogram Increase to say that it's called with a single
argument, a variable of type Integer whose initial value is less than 100. Our contract says
that the only effect of the subprogram is to increase the value of its argument.
Listing 3: increase.ads
1 procedure Increase (X : in out Integer) with
2 Global => null,
3 Pre => X <= 100,
4 Post => X'Old < X;
Listing 4: client.adb
1 with Increase;
2 procedure Client is
3 X : Integer := 0;
4 begin
5 while X <= 100 loop -- The loop will terminate
6 Increase (X); -- Increase can be called safely
7 end loop;
8 pragma Assert (X = 101); -- Will this hold?
9 end Client;
Project: Courses.Intro_To_Spark.State_Abstraction.Using_Abstraction
MD5: 9cd07cb04ae2194343931f0561693be4
Prover output
Callers can also assume that the implementation of Increase won't cause any runtime er-
rors when called in the loop. On the other hand, nothing in the specification guarantees that
the assertion show above is correct: it may fail if Increase's implementation is changed.
If you follow this basic principle, abstraction can bring you significant benefits. It simplifies
both your program's implementation and verification. It also makes maintenance and code
reuse much easier since changes to the implementation of an object shouldn't affect the
code using this object. Your goal in using it is that it should be enough to understand the
specification of an object in order to use that object, since understanding the specification
is usually much simpler than understanding the implementation.
GNATprove relies on the abstraction defined by subprogram contracts and therefore doesn't
prove the assertion after the loop in Client above.
Subprograms aren't the only objects that benefit from abstraction. The state of a package
— the set of persistent variables defined in it — can also be hidden from external users. You
achieve this form of abstraction — called state abstraction — by defining variables in the
body or private part of a package so they can only be accessed through subprogram calls.
For example, our Stack package shown below provides an abstraction for a Stack object
which can only be modified using the Pop and Push procedures.
package Stack is
procedure Pop (E : out Element);
procedure Push (E : in Element);
end Stack;
The fact that we implemented it using an array is irrelevant to the caller. We could change
that without impacting our callers' code.
Hidden state influences a program's behavior, so SPARK allows that state to be declared.
You can use the Abstract_State aspect, an abstraction that names a state, to do this, but
you aren't required to use it even for a package with hidden state. You can use several
state abstractions to declare the hidden state of a single package or you can use it for a
package with no hidden state at all. However, since SPARK doesn't allow aliasing, different
state abstractions must always refer to disjoint sets of variables. A state abstraction isn't a
variable: it doesn't have a type and can't be used inside expressions, either those in bodies
or contracts.
As an example of the use of this aspect, we can optionally define a state abstraction for the
entire hidden state of the Stack package like this:
package Stack with
Abstract_State => The_Stack
is
...
Remember: a state abstraction isn't a variable (it has no type) and can't be used inside
expressions. For example:
pragma Assert (Stack.Top_State = ...);
-- compilation error: Top_State is not a variable
Once you've declared an abstract state in a package, you must refine it into its constituents
using a Refined_State aspect. You must place the Refined_State aspect on the package
body even if the package wouldn't otherwise have required a body. For each state abstrac-
tion you've declared for the package, you list the set of variables represented by that state
abstraction in its refined state.
If you specify an abstract state for a package, it must be complete, meaning you must
have listed every hidden variable as part of some state abstraction. For example, we must
add a Refined_State aspect on our Stack package's body linking the state abstraction
(The_Stack) to the entire hidden state of the package, which consists of both Content and
Top.
Listing 5: stack.ads
1 package Stack with
2 Abstract_State => The_Stack
3 is
4 type Element is new Integer;
5
9 end Stack;
Listing 6: stack.adb
1 package body Stack with
2 Refined_State => (The_Stack => (Content, Top))
3 is
4 Max : constant := 100;
5
25 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Refined_State
MD5: 3a794c7a4e4920dab7d01248e50901ab
Prover output
You can refine state abstractions in the package body, where all the variables are visible.
When only the package's specification is available, you need a way to specify which state
abstraction each private variable belongs to. You do this by adding the Part_Of aspect to
the variable's declaration.
Part_Of annotations are mandatory: if you gave a package an abstract state annotation,
you must link all the hidden variables defined in its private part to a state abstraction. For
example:
Listing 7: stack.ads
1 package Stack with
2 Abstract_State => The_Stack
3 is
4 type Element is new Integer;
5
9 private
10
18 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Private_Variables
MD5: 3b5f7edca8a4511071d2397197b01fda
Prover output
Since we chose to define Content and Top in Stack's private part instead of its body, we
had to add a Part_Of aspect to both of their declarations, associating them with the state
abstraction The_Stack, even though it's the only state abstraction. However, we still need
to list them in the Refined_State aspect in Stack's body.
So far, we've only discussed hidden variables. But variables aren't the only component of
a package's state. If a package P contains a nested package, the nested package's state is
also part of P's state. If the nested package is hidden, its state is part of P's hidden state
and must be listed in P's state refinement.
We see this in the example below, where the package Hidden_Nested's hidden state is part
of P's hidden state.
Listing 8: p.ads
1 package P with
2 Abstract_State => State
3 is
4 package Visible_Nested with
5 Abstract_State => Visible_State
6 is
7 procedure Get (E : out Integer);
8 end Visible_Nested;
9 end P;
Listing 9: p.adb
1 package body P with
2 Refined_State => (State => Hidden_Nested.Hidden_State)
3 is
4 package Hidden_Nested with
5 Abstract_State => Hidden_State,
6 Initializes => Hidden_State
7 is
8 function Get return Integer;
9 end Hidden_Nested;
10
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
p.adb:6:07: info: flow dependencies proved
p.ads:7:22: info: initialization of "E" proved
Any visible state of Hidden_Nested would also have been part of P's hidden state. How-
ever, if P contains a visible nested package, that nested package's state isn't part of P's
hidden state. Instead, you should declare that package's hidden state in a separate state
abstraction on its own declaration, like we did above for Visible_Nested.
Some constants are also possible components of a state abstraction. These are constants
whose value depends either on a variable or a subprogram parameter. They're handled as
variables during flow analysis because they participate in the flow of information between
variables throughout the program. Therefore, GNATprove considers these constants to be
part of a package's state just like it does for variables.
If you've specified a state abstraction for a package, you must list such hidden constants
declared in that package in the state abstraction refinement. However, constants that don't
depend on variables don't participate in the flow of information and must not appear in a
state refinement.
Let's look at this example.
28 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Constants_And_Variables
MD5: 109a6340ef0f3b0dc88e0fe5888b9a53
Prover output
Here, Max — the maximum number of elements that can be stored in the stack — is initial-
ized from a variable in an external package. Because of this, we must include Max as part
of the state abstraction The_Stack.
Note: For more details on state abstractions, see the SPARK User's Guide260 .
260 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/package_contracts.html#
state-abstraction
Hidden variables can only be accessed through subprogram calls, so you document how
state abstractions are modified during the program's execution via the contracts of those
subprograms. You use Global and Depends contracts to specify which of the state abstrac-
tions are used by a subprogram and how values flow through the different variables. The
Global and Depends contracts that you write when referring to state abstractions are often
less precise than contracts referring to visible variables since the possibly different depen-
dencies of the hidden variables contained within a state abstraction are collapsed into a
single dependency.
Let's add Global and Depends contracts to the Pop procedure in our stack.
12 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Global_Depends
MD5: a7b383c35508d6a8294bf7cf0fe332ac
Prover output
In this example, the Pop procedure only modifies the value of the hidden variable Top, while
Content is unchanged. By using distinct state abstractions for the two variables, we're able
to preserve this semantic in the contract.
Let's contrast this example with a different representation of Global and Depends contracts,
this time using a single abstract state.
10 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Global_Single_Abstract_State
MD5: f89f6026fa5ee3c18baf0af9d7c3dbca
Prover output
Here, Top_State and Content_State are merged into a single state abstraction,
The_Stack. By doing so, we've hidden the fact that Content isn't modified (though we're
still showing that Top may be modified). This loss in precision is reasonable here, since
it's the whole point of the abstraction. However, you must be careful not to aggregate
unrelated hidden state because this risks their annotations becoming meaningless.
Even though imprecise contracts that consider state abstractions as a whole are perfectly
reasonable for users of a package, you should write Global and Depends contracts that are
as precise as possible within the package body. To allow this, SPARK introduces the notion
of refined contracts, which are precise contracts specified on the bodies of subprograms
where state refinements are visible. These contracts are the same as normal Global and
Depends contracts except they refer directly to the hidden state of the package.
When a subprogram is called inside the package body, you should write refined contracts
instead of the general ones so that the verification can be as precise as possible. However,
refined Global and Depends are optional: if you don't specify them, GNATprove will compute
them to check the package's implementation.
For our Stack example, we could add refined contracts as shown below.
14 end Stack;
31 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Global_Refined
MD5: b7e700645885155ea7faf2f4170f0462
Prover output
15 end Stack;
25 end Stack;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
stack.adb:15:23: info: index check proved
stack.adb:16:18: info: range check proved
stack.adb:21:28: info: range check proved
stack.adb:22:16: info: index check proved
stack.ads:7:19: info: initialization of "E" proved
stack.ads:9:14: info: postcondition proved
stack.ads:13:14: info: postcondition proved
Just like we saw for Global and Depends contracts, you may often find it useful to have
a more precise view of functional contracts in the context where the hidden variables are
visible. You do this using expression functions in the same way we did for the functions
Is_Empty and Is_Full above. As expression function, bodies act as contracts for GNAT-
prove, so they automatically give a more precise version of the contracts when their imple-
mentation is visible.
You may often need a more constraining contract to verify the package's implementation
but want to be less strict outside the abstraction. You do this using the Refined_Post
aspect. This aspect, when placed on a subprogram's body, provides stronger guarantees to
internal callers of a subprogram. If you provide one, the refined postcondition must imply
the subprogram's postcondition. This is checked by GNATprove, which reports a failing
postcondition if the refined postcondition is too weak, even if it's actually implied by the
subprogram's body. SPARK doesn't peform a similar verification for normal preconditions.
For example, we can refine the postconditions in the bodies of Pop and Push to be more
detailed than what we wrote for them in their specification.
15 end Stack;
29 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Pre_Postconditions_2
MD5: 4691565d58ba039b3cbd06e65cecfa88
Prover output
Note: For more details on refinement in contracts, see the SPARK User's Guide261 .
As part of flow analysis, GNATprove checks for the proper initialization of variables. There-
fore, flow analysis needs to know which variables are initialized during the package's elab-
oration.
You can use the Initializes aspect to specify the set of visible variables and state ab-
stractions that are initialized during the elaboration of a package. An Initializes aspect
can't refer to a variable that isn't defined in the unit since, in SPARK, a package can only
initialize variables declared immediately within the package.
Initializes aspects are optional. If you don't supply any, they'll be derived by GNATprove.
For our Stack example, we could add an Initializes aspect.
9 end Stack;
state-abstraction-and-contracts
17 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Local_Init
MD5: 710e74959fd2ef8f5089c4636d7ec13b
Prover output
Flow analysis also checks for dependencies between variables, so it must be aware of how
information flows through the code that performs the initialization of states. We discussed
one use of the Initializes aspect above. But you also can use it to provide flow infor-
mation. If the initial value of a variable or state abstraction is dependent on the value of
another visible variable or state abstraction from another package, you must list this de-
pendency in the Initializes contract. You specify the list of entities on which a variable's
initial value depends using an arrow following that variable's name.
Let's look at this example:
Project: Courses.Intro_To_Spark.State_Abstraction.Initializes
MD5: c8aa7f21729f3b926bf3d25a826cccb2
Prover output
Here we indicated that V2's initial value depends on the value of Q.External_Variable by
including that dependency in the Initializes aspect of P. We didn't list any dependency
for V1 because its initial value doesn't depend on any external variable. We could also have
stated that lack of dependency explicitly by writing V1 => null.
GNATprove computes dependencies of initial values if you don't supply an Initializes
aspect. However, if you do provide an Initializes aspect for a package, it must be com-
plete: you must list every initialized state of the package, along with all its external depen-
dencies.
Note: For more details on Initializes, see the SPARK User's Guide262 .
32.10.1 Example #1
3 External_Variable : Natural := 1;
4
5 end Configuration;
9 private
10
17 end Communication;
10 end Communication;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_01
MD5: 207e999f85a5b39fa2b9aebbc836b479
Prover output
This example isn't correct. Capacity is declared in the private part of Communication.
Therefore, we should have linked it to State by using the Part_Of aspect in its declaration.
32.10.2 Example #2
Let's add Part_Of to the state of hidden local package Ring_Buffer, but this time we hide
variable Capacity inside the private part of Ring_Buffer.
3 External_Variable : Natural := 1;
4
5 end Configuration;
18 end Communication;
11 end Communication;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_02
MD5: b8d31fcfbd11bf305646efe07baeb91b
Prover output
32.10.3 Example #3
6 procedure Reset_Black_Count is
7 begin
8 Black_Counter := 0;
9 end Reset_Black_Count;
10
11 procedure Reset_Red_Count is
12 begin
13 Red_Counter := 0;
14 end Reset_Red_Count;
15 end Counting;
3 procedure Main is
4 begin
5 Reset_Black_Count;
6 Reset_Red_Count;
7 end Main;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_03
MD5: bc2d7ccd7419d34f7156a16dfc484229
Prover output
This program doesn't read any uninitialized data, but GNATprove fails to verify that. This is
because we provided a state abstraction for package Counting, so flow analysis computes
the effects of subprograms in terms of this state abstraction and thus considers State to
be an in-out global consisting of both Black_Counter and Red_Counter. So it issues the
message requiring that State be initialized after elaboration as well as the warning that no
procedure in package Counting can initialize its state.
32.10.4 Example #4
4 procedure Reset_Black_Count is
5 begin
6 Black_Counter := 0;
7 end Reset_Black_Count;
8
9 procedure Reset_Red_Count is
10 begin
11 Red_Counter := 0;
12 end Reset_Red_Count;
13 end Counting;
3 procedure Main is
4 begin
5 Reset_Black_Count;
6 Reset_Red_Count;
7 end Main;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_04
MD5: 3ddd934b6ede6df7b823e46828694d12
Prover output
This example is correct. Because we didn't provide a state abstraction, GNATprove reasons
in terms of variables, instead of states, and proves data initialization without any problem.
32.10.5 Example #5
Let's restore the abstract state to package Counting, but this time provide a proce-
dure Reset_All that calls the initialization procedures Reset_Black_Counter and Re-
set_Red_Counter.
6 procedure Reset_Black_Count is
7 begin
8 Black_Counter := 0;
9 end Reset_Black_Count;
10
11 procedure Reset_Red_Count is
12 begin
13 Red_Counter := 0;
14 end Reset_Red_Count;
15
16 procedure Reset_All is
17 begin
18 Reset_Black_Count;
(continues on next page)
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
counting.ads:4:37: info: data dependencies proved
counting.ads:5:37: info: data dependencies proved
counting.ads:6:14: info: initialization of "Black_Counter" constituent of "State"␣
↪proved
This example is correct. Flow analysis computes refined versions of Global contracts for
internal calls and uses these to verify that Reset_All indeed properly initializes State.
The Refined_Global and Global annotations are not mandatory and can be computed by
GNATprove.
32.10.6 Example #6
Build output
stack.ads:15:39: error: there is no applicable operator "=" for package or␣
↪procedure name
Prover output
Phase 1 of 2: generation of Global contracts ...
stack.ads:15:39: error: there is no applicable operator "=" for package or␣
↪procedure name
This example isn't correct. There's a compilation error in Push's postcondition: The_Stack
is a state abstraction, not a variable, and therefore can't be used in an expression.
32.10.7 Example #7
In this version of our abstract stack unit, a copy of the stack is returned by function
Get_Stack, which we call in the postcondition of Push to specify that the stack shouldn't
be modified if it's full. We also assert that after we push an element on the stack, either
the stack is unchanged (if it was already full) or its top element is equal to the element just
pushed.
22 private
23
29 end Stack;
22 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_07
MD5: 4831aa7f018f2e2d4e6d102095f8f631
Prover output
This program is correct, but GNATprove can't prove the assertion in Use_Stack. Indeed,
even if Get_Stack is an expression function, its body isn't visible outside of Stack's body,
where it's defined.
32.10.8 Example #8
Let's move the definition of Get_Stack and other expression functions inside the private
part of the spec of Stack.
22 private
23
39 end Stack;
14 end Stack;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_08
MD5: 7e5204d3f69e71c212e7263906a89da4
Prover output
This example is correct. GNATprove can verify the assertion in Use_Stack because it has
visibility to Get_Stack's body.
32.10.9 Example #9
Package Data defines three variables, Data_1, Data_2 and Data_3, that are initialized at
elaboration (in Data's package body) from an external interface that reads the file system.
8 Data_1 : Data_Type_1;
9 Data_2 : Data_Type_2;
10 Data_3 : Data_Type_3;
11
12 end Data;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_09
MD5: 0ca44501f0c991865ea50d2ef663d992
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
data.adb:9:07: high: "External_Interface.File_System" must be mentioned as an␣
↪input of the Initializes aspect of "Data" (SPARK RM 7.1.5(11))
This example isn't correct. The dependency between Data_1's, Data_2's, and Data_3's
initial values and File_System must be listed in Data's Initializes aspect.
3 package Data is
4 pragma Elaborate_Body;
5
6 Data_1 : Data_Type_1;
7 Data_2 : Data_Type_2;
8 Data_3 : Data_Type_3;
9
10 end Data;
Project: Courses.Intro_To_Spark.State_Abstraction.Example_10
MD5: 60cba2c920c7b1031d13c82a982ed0e9
Prover output
This example is correct. Since Data has no Initializes aspect, GNATprove computes the
set of variables initialized during its elaboration as well as their dependencies.
THIRTYTHREE
When we speak about the correctness of a program or subprogram, we mean the extent
to which it complies with its specification. Functional correctness is specifically concerned
with properties that involve the relations between the subprogram's inputs and outputs, as
opposed to other properties such as running time or memory consumption.
For functional correctness, we usually specify stronger properties than those required to just
prove program integrity. When we're involved in a certification processes, we should derive
these properties from the requirements of the system, but, especially in non-certification
contexts, they can also come from more informal sources, such as the program's documen-
tation, comments in its code, or test oracles.
For example, if one of our goals is to ensure that no runtime error is raised when using the
result of the function Find below, it may be enough to know that the result is either 0 or in
the range of A. We can express this as a postcondition of Find.
Listing 1: show_find.ads
1 package Show_Find is
2
8 end Show_Find;
Listing 2: show_find.adb
1 package body Show_Find is
2
1073
Learning Ada
13 end Show_Find;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_find.adb:7:20: info: range check proved
show_find.ads:6:14: info: postcondition proved
Listing 3: show_find.ads
1 package Show_Find is
2
11 end Show_Find;
Listing 4: show_find.adb
1 package body Show_Find is
2
13 end Show_Find;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
(continues on next page)
This time, GNATprove can't prove this postcondition automatically, but we'll see later that
we can help GNATprove by providing a loop invariant, which is checked by GNATprove and
allows it to automatically prove the postcondition for Find.
Writing at least part of your program's specification in the form of contracts has many
advantages. You can execute those contracts during testing, which improves the maintain-
ability of the code by detecting discrepancies between the program and its specification in
earlier stages of development. If the contracts are precise enough, you can use them as
oracles to decide whether a given test passed or failed. In that case, they can allow you to
verify the outputs of specific subprograms while running a larger block of code. This may,
in certain contexts, replace the need for you to perform unit testing, instead allowing you
to run integration tests with assertions enabled. Finally, if the code is in SPARK, you can
also use GNATprove to formally prove these contracts.
The advantage of a formal proof is that it verifies all possible execution paths, something
which isn't always possible by running test cases. For example, during testing, the post-
condition of the subprogram Find shown below is checked dynamically for the set of inputs
for which Find is called in that test, but just for that set.
Listing 5: show_find.ads
1 package Show_Find is
2
11 end Show_Find;
Listing 6: show_find.adb
1 package body Show_Find is
2
13 end Show_Find;
Listing 7: use_find.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Show_Find; use Show_Find;
3
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_find.ads:9:14: medium: postcondition might fail, cannot prove Find'Result in A
↪'range
Runtime output
Found 3 in index # 3 of array
However, if Find is formally verified, that verification checks its postcondition for all possible
inputs. During development, you can attempt such verification earlier than testing since it's
performed modularly on a per-subprogram basis. For example, in the code shown above,
you can formally verify Use_Find even before you write the body for subprogram Find.
Contracts for functional correctness are usually more complex than contracts for program
integrity, so they more often require you to use the new forms of expressions introduced
by the Ada 2012 standard. In particular, quantified expressions, which allow you to specify
properties that must hold for all or for at least one element of a range, come in handy when
specifying properties of arrays.
As contracts become more complex, you may find it useful to introduce new abstractions
to improve the readability of your contracts. Expression functions are a good means to this
end because you can retain their bodies in your package's specification.
Finally, some properties, especially those better described as invariants over data than as
properties of subprograms, may be cumbersome to express as subprogram contracts. Type
predicates, which must hold for every object of a given type, are usually a better match for
this purpose. Here's an example.
Listing 8: show_sort.ads
1 package Show_Sort is
2
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Sort
MD5: d3b3d26d62074d11b19d9282cc548c1b
Prover output
We can use the subtype Sorted_Nat_Array as the type of a variable that must remain
sorted throughout the program's execution. Specifying that an array is sorted requires a
rather complex expression involving quantifiers, so we abstract away this property as an
expression function to improve readability. Is_Sorted's body remains in the package's
specification and allows users of the package to retain a precise knowledge of its meaning
when necessary. (You must use Nat_Array as the type of the operand of Is_Sorted. If you
use Sorted_Nat_Array, you'll get infinite recursion at runtime when assertion checks are
enabled since that function is called to check all operands of type Sorted_Nat_Array.)
As the properties you need to specify grow more complex, you may have entities that are
only needed because they are used in specifications (contracts). You may find it important
to ensure that these entities can't affect the behavior of the program or that they're com-
pletely removed from production code. This concept, having entities that are only used for
specifications, is usually called having ghost code and is supported in SPARK by the Ghost
aspect.
You can use Ghost aspects to annotate any entity including variables, types, subprograms,
and packages. If you mark an entity as Ghost, GNATprove ensures it can't affect the pro-
gram's behavior. When the program is compiled with assertions enabled, ghost code is
executed like normal code so it can execute the contracts using it. You can also instruct
the compiler to not generate code for ghost entities.
Consider the procedure Do_Something below, which calls a complex function on its input,
X, and wants to check that the initial and modified values of X are related in that complex
way.
Listing 9: show_ghost.ads
1 package Show_Ghost is
2
3 type T is record
4 A, B, C, D, E : Boolean;
5 end record;
6
15 end Show_Ghost;
16 end Show_Ghost;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
show_ghost.adb:12:22: info: assertion proved
3 type T is record
4 A, B, C, D, E : Boolean;
5 end record;
6
15 end Show_Ghost;
14 X := X_Init; -- ERROR
15
16 end Do_Something;
17
18 end Show_Ghost;
3 procedure Use_Ghost is
4 X : T := (True, True, False, False, True);
5 begin
6 Do_Something (X);
7 end Use_Ghost;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Ghost_2
MD5: 464bb4bc355a648e2b92940ec80b4717
Build output
Prover output
When compiling this example, the compiler flags the use of X_Init as illegal, but more
complex cases of interference between ghost and normal code may sometimes only be
detected when you run GNATprove.
Functions used only in specifications are a common occurrence when writing contracts for
functional correctness. For example, expression functions used to simplify or factor out
common patterns in contracts can usually be marked as ghost.
But ghost functions can do more than improve readability. In real-world programs, it's often
the case that some information necessary for functional specification isn't accessible in the
package's specification because of abstraction.
Making this information available to users of the packages is generally out of the question
because that breaks the abstraction. Ghost functions come in handy in that case since
they provide a way to give access to that information without making it available to normal
client code.
Let's look at the following example.
18 private
19
27 end Stacks;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Ghost_Functions
MD5: e287612bd66753f07ac3eecb36c693de
Prover output
Here, the type Stack is private. To specify the expected behavior of the Push procedure,
we need to go inside this abstraction and access the values of the elements stored in S.
For this, we introduce a function Get_Model that returns an array as a representation of
the stack. However, we don't want code that uses the Stack package to use Get_Model in
normal code since this breaks our stack's abstraction.
Here's an example of trying to break that abstraction in the subprogram Peek below.
21 private
22
30 end Stacks;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Ghost_Model
MD5: c00b5d86c9d0b665ccdda7f68f16f07a
Prover output
We see that marking the function as Ghost achieves this goal: it ensures that the subpro-
gram Get_Model is never used in production code.
Though it happens less frequently, you may have specifications requiring you to store ad-
ditional information in global variables that isn't needed in normal code. You should mark
these global variables as ghost, allowing the compiler to remove them when assertions
aren't enabled. You can use these variables for any purpose within the contracts that make
up your specifications. A common scenario is writing specifications for subprograms that
modify a complex or private global data structure: you can use these variables to provide
a model for that structure that's updated by the ghost code as the program modifies the
data structure itself.
You can also use ghost variables to store information about previous runs of subprograms
to specify temporal properties. In the following example, we have two procedures, one
that accesses a state A and the other that accesses a state B. We use the ghost variable
Last_Accessed_Is_A to specify that B can't be accessed twice in a row without accessing
A in between.
15 end Call_Sequence;
3 procedure Access_A is
4 begin
5 -- ...
6 Last_Accessed_Is_A := True;
7 end Access_A;
8
9 procedure Access_B is
10 begin
11 -- ...
12 Last_Accessed_Is_A := False;
13 end Access_B;
14
15 end Call_Sequence;
3 procedure Main is
4 begin
5 Access_A;
6 Access_B;
7 Access_B; -- ERROR
8 end Main;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Global_Ghost_Vars
MD5: f33fa2ad2bd31eb03d4400c78f22eb71
Prover output
Runtime output
14 end Action_Sequence;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Intermediate_Values
MD5: 2ffbd2cb187c0a81423c78e0989d62f0
Prover output
Note: For more details on ghost code, see the SPARK User's Guide263 .
263 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/specification_features.html#
ghost-code
Since properties of interest for functional correctness are more complex than those involved
in proofs of program integrity, we expect GNATprove to initially be unable to verify them
even though they're valid. You'll find the techniques we discussed in Debugging Failed
Proof Attempts (page 1028) to come in handy here. We now go beyond those techniques
and focus on more ways of improving results in the cases where the property is valid but
GNATprove can't prove it in a reasonable amount of time.
In those cases, you may want to try and guide GNATprove to either complete the proof or
strip it down to a small number of easily-reviewable assumptions. For this purpose, you can
add assertions to break complex proofs into smaller steps.
pragma Assert (Assertion_Checked_By_The_Tool);
-- info: assertion proved
One such intermediate step you may find useful is to try to prove a theoretically-equivalent
version of the desired property, but one where you've simplified things for the prover, such
as by splitting up different cases or inlining the definitions of functions.
Some intermediate assertions may not be proved by GNATprove either because it's miss-
ing some information or because the amount of information available is confusing. You
can verify these remaining assertions by other means such as testing (since they're exe-
cutable) or by review. You can then choose to instruct GNATprove to ignore them, either
by turning them into assumptions, as in our example, or by using a pragma Annotate. In
both cases, the compiler generates code to check these assumptions at runtime when you
enable assertions.
You can use ghost code to enhance what you can express inside intermediate assertions in
the same way we did above to enhance our contracts in specifications. In particular, you'll
commonly have local variables or constants whose only purpose is to be used in assertions.
You'll mostly use these ghost variables to store previous values of variables or expressions
you want to refer to in assertions. They're especially useful to refer to initial values of
parameters and expressions since the 'Old attribute is only allowed in postconditions.
In the example below, we want to help GNATprove verify the postcondition of P. We do
this by introducing a local ghost constant, X_Init, to represent this value and writing an
assertion in both branches of an if statement that repeats the postcondition, but using
X_Init.
13 end Show_Local_Ghost;
3 procedure P (X : in out T) is
4 X_Init : constant T := X with Ghost;
5 begin
6 if Condition (X) then
7 X := X + 1;
8 pragma Assert (F (X, X_Init));
9 else
10 X := X * 2;
11 pragma Assert (F (X, X_Init));
12 end if;
13 end P;
14
15 end Show_Local_Ghost;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Local_Ghost
MD5: 071ee53a06a6b5880eee6e9ea06dbcf3
Prover output
You can also use local ghost variables for more complex purposes such as building a data
structure that serves as witness for a complex property of a subprogram. In our example,
we want to prove that the Sort procedure doesn't create new elements, that is, that all the
elements present in A after the sort were in A before the sort. This property isn't enough
to ensure that a call to Sort produces a value for A that's a permutation of its value before
the call (or that the values are indeed sorted). However, it's already complex for a prover
to verify because it involves a nesting of quantifiers. To help GNATprove, you may find it
useful to store, for each index I, an index J that has the expected property.
Ghost procedures can't affect the value of normal variables, so they're mostly used to per-
form operations on ghost variables or to group together a set of intermediate assertions.
Abstracting away the treatment of assertions and ghost variables inside a ghost procedure
has several advantages. First, you're allowed to use these variables in any way you choose
in code inside ghost procedures. This isn't the case outside ghost procedures, where the
only ghost statements allowed are assignments to ghost variables and calls to ghost pro-
cedures.
As an example, the for loop contained in Increase_A couldn't appear by itself in normal
code.
11 end Show_Ghost_Proc;
3 procedure Increase_A is
4 begin
5 for I in A'Range loop
6 A (I) := A (I) + 1;
7 end loop;
8 end Increase_A;
9
10 end Show_Ghost_Proc;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Ghost_Proc
MD5: 4b9cfe25011169a0cd3b4a3b03135dc4
Prover output
Using the abstraction also improves readability by hiding complex code that isn't part of
the functional behavior of the subprogram. Finally, it can help GNATprove by abstracting
away assertions that would otherwise make its job more complex.
In the example below, calling Prove_P with X as an operand only adds P (X) to the proof
context instead of the larger set of assertions required to verify it. In addition, the proof of P
need only be done once and may be made easier not having any unnecessary information
present in its context while verifying it. Also, if GNATprove can't fully verify Prove_P, you
can review the remaining assumptions more easily since they're in a smaller context.
When the program involves a loop, you're almost always required to provide additional
annotations to allow GNATprove to complete a proof because the verification techniques
used by GNATprove don't handle cycles in a subprogram's control flow. Instead, loops are
flattened by dividing them into several acyclic parts.
As an example, let's look at a simple loop with an exit condition.
Stmt1;
loop
Stmt2;
exit when Cond;
Stmt3;
end loop;
Stmt4;
The first, shown in yellow, starts earlier in the subprogram and enters the loop statement.
The loop itself is divided into two parts. Red represents a complete execution of the loop's
body: an execution where the exit condition isn't satisfied. Blue represents the last execu-
tion of the loop, which includes some of the subprogram following it. For that path, the exit
condition is assumed to hold. The red and blue parts are always executed after the yellow
one.
GNATprove analyzes these parts independently since it doesn't have a way to track how
variables may have been updated by an iteration of the loop. It forgets everything it knows
about those variables from one part when entering another part. However, values of con-
stants and variables that aren't modified in the loop are not an issue.
In other words, handling loops in that way makes GNATprove imprecise when verifying a
subprogram involving a loop: it can't verify a property that relies on values of variables
modified inside the loop. It won't forget any information it had on the value of constants or
unmodified variables, but it nevertheless won't be able to deduce new information about
them from the loop.
For example, consider the function Find which iterates over the array A and searches for
an element where E is stored in A.
7 end Show_Find;
17 end Show_Find;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Loop
MD5: cb9cd0cb102c3baba3b21a788b6e4ae3
Prover output
↪precondition]
At the end of each loop iteration, GNATprove knows that the value stored at index I in A
must not be E. (If it were, the loop wouldn't have reached the end of the interation.) This
proves the second assertion. But it's unable to aggregate this information over multiple
loop iterations to deduce that it's true for all the indexes smaller than I, so it can't prove
the first assertion.
To overcome these limitations, you can provide additional information to GNATprove in the
form of a loop invariant. In SPARK, a loop invariant is a Boolean expression which holds true
at every iteration of the loop. Like other assertions, you can have it checked at runtime by
compiling the program with assertions enabled.
The major difference between loop invariants and other assertions is the way it's treated
for proofs. GNATprove performs the proof of a loop invariant in two steps: first, it checks
that it holds for the first iteration of the loop and then it checks that it holds in an arbitrary
iteration assuming it held in the previous iteration. This is called proof by induction264 .
As an example, let's add a loop invariant to the Find function stating that the first element
of A is not E.
7 end Show_Find;
16 end Show_Find;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Loop_Invariant_1
MD5: 8d5fefdca9deacd4eb50850be91fbefe
Prover output
264 https://en.wikipedia.org/wiki/Mathematical_induction
To verify this invariant, GNATprove generates two checks. The first checks that the assertion
holds in the first iteration of the loop. This isn't verified by GNATprove. And indeed there's
no reason to expect the first element of A to always be different from E in this iteration.
However, the second check is proved: it's easy to deduce that if the first element of A was
not E in a given iteration it's still not E in the next. However, if we move the invariant to
the end of the loop, then it is successfully verified by GNATprove.
Not only do loop invariants allow you to verify complex properties of loops, but GNATprove
also uses them to verify other properties, such as the absence of runtime errors over both
the loop's body and the statements following the loop. More precisely, when verifying a
runtime check or other assertion there, GNATprove assumes that the last occurrence of the
loop invariant preceding the check or assertion is true.
Let's look at a version of Find where we use a loop invariant instead of an assertion to state
that none of the array elements seen so far are equal to E.
7 end Show_Find;
16 end Show_Find;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Loop_Invariant_2
MD5: 21588161eaddb82f54c3cb3dcc14a6ac
Prover output
This version is fully verified by GNATprove! This time, it proves that the loop invariant holds
in every iteration of the loop (separately proving this property for the first iteration and then
for the following iterations). It also proves that none of the elements of A are equal to E
after the loop exits by assuming that the loop invariant holds in the last iteration of the
loop.
Note: For more details on loop invariants, see the SPARK User's Guide265 .
Finding a good loop invariant can turn out to be quite a challenge. To make this task easier,
let's review the four good properties of a good loop invariant:
Prop- Description
erty
INIT It should be provable in the first iteration of the loop.
INSIDE It should allow proving the absence of run-time errors and local assertions inside
the loop.
AFTER It should allow proving absence of run-time errors, local assertions, and the
subprogram postcondition after the loop.
PRE- It should be provable after the first iteration of the loop.
SERVE
Let's look at each of these in turn. First, the loop invariant should be provable in the first
iteration of the loop (INIT). If your invariant fails to achieve this property, you can debug the
loop invariant's initialization like any failing proof attempt using strategies for Debugging
Failed Proof Attempts (page 1028).
Second, the loop invariant should be precise enough to allow GNATprove to prove absence
of runtime errors in both statements from the loop's body (INSIDE) and those following the
loop (AFTER). To do this, you should remember that all information concerning a variable
modified in the loop that's not included in the invariant is forgotten by GNATprove. In
particular, you should take care to include in your invariant what's usually called the loop's
frame condition, which lists properties of variables that are true throughout the execution
of the loop even though those variables are modified by the loop.
Finally, the loop invariant should be precise enough to prove that it's preserved through
successive iterations of the loop (PRESERVE). This is generally the trickiest part. To under-
stand why GNATprove hasn't been able to verify the preservation of a loop invariant you
provided, you may find it useful to repeat it as local assertions throughout the loop's body
to determine at which point it can no longer be proved.
As an example, let's look at a loop that iterates through an array A and applies a function F
to each of its elements.
10 end Show_Map;
265 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/assertion_pragmas.html#
loop-invariants
14 end Show_Map;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Loop_Invariant_3
MD5: 1a4583c9b2b772f79bcf29cff0caa96a
Prover output
After the loop, each element of A should be the result of applying F to its previous value.
We want to prove this. To specify this property, we copy the value of A before the loop into
a ghost variable, A_I. Our loop invariant states that the element at each index less than K
has been modified in the expected way. We use the Loop_Entry attribute to refer to the
value of A on entry of the loop instead of using A_I.
Does our loop invariant have the four properties of a good loop-invariant? When launching
GNATprove, we see that INIT is fulfilled: the invariant's initialization is proved. So are
INSIDE and AFTER: no potential runtime errors are reported and the assertion following the
loop is successfully verified.
The situation is slightly more complex for the PRESERVE property. GNATprove manages to
prove that the invariant holds after the first iteration thanks to the automatic generation of
frame conditions. It was able to do this because it completes the provided loop invariant
with the following frame condition stating what part of the array hasn't been modified so
far:
pragma Loop_Invariant
(for all J in K .. A'Last => A (J) = (if J > K then A'Loop_Entry (J)));
GNATprove then uses both our and the internally-generated loop invariants to prove PRE-
SERVE. However, in more complex cases, the heuristics used by GNATprove to generate the
frame condition may not be sufficient and you'll have to provide one as a loop invariant. For
example, consider a version of Map where the result of applying F to an element at index K
is stored at index K-1:
10 end Show_Map;
20 end Show_Map;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Loop_Invariant_4
MD5: 6c51768547d3baa2c19d0e33959388fe
Prover output
You need to uncomment the second loop invariant containing the frame condition in order
to prove the assertion after the loop.
Note: For more details on how to write a loop invariant, see the SPARK User's Guide266 .
33.4.1 Example #1
We implement a ring buffer inside an array Content, where the contents of a ring buffer of
length Length are obtained by starting at index First and possibly wrapping around the
end of the buffer. We use a ghost function Get_Model to return the contents of the ring
buffer for use in contracts.
13 end Ring_Buffer;
html
39 end Ring_Buffer;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_01
MD5: 3afd7d58f97001618acc05062115f1a3
Prover output
This is correct: Get_Model is used only in contracts. Calls to Get_Model make copies of the
buffer's contents, which isn't efficient, but is fine because Get_Model is only used for verifi-
cation, not in production code. We enforce this by making it a ghost function. We'll produce
the final production code with appropriate compiler switches (i.e., not using -gnata) that
ensure assertions are ignored.
33.4.2 Example #2
Instead of using a ghost function, Get_Model, to retrieve the contents of the ring buffer,
we're now using a global ghost variable, Model.
23 end Ring_Buffer;
20 end Ring_Buffer;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_02
MD5: 144f58bd95cd460e4ed388d4f3351fe3
Build output
Prover output
This example isn't correct. Model, which is a ghost variable, must not influence the return
value of the normal function Valid_Model. Since Valid_Model is only used in specifications,
we should have marked it as Ghost. Another problem is that Model needs to be updated
inside Push_Last to reflect the changes to the ring buffer.
33.4.3 Example #3
23 end Ring_Buffer;
22 end Ring_Buffer;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_03
MD5: 08b74f5fe560d238550a06c6323959cf
Prover output
This example is correct. The ghost variable Model can be referenced both from the body
of the ghost function Valid_Model and the non-ghost procedure Push_Last as long as it's
only used in ghost statements.
33.4.4 Example #4
We're now modifying Push_Last to share the computation of the new length between the
operational and ghost code.
23 end Ring_Buffer;
23 end Ring_Buffer;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_04
MD5: e27f0b4729be72d83f2cb981b1d00412
Build output
Prover output
This example isn't correct. We didn't mark local constant New_Length as Ghost, so it can't
be computed from the value of ghost variable Model. If we made New_Length a ghost
constant, the compiler would report the problem on the assignment from New_Length to
Length. The correct solution here is to compute New_Length from the value of the non-
ghost variable Length.
33.4.5 Example #5
Let's move the code updating Model inside a local ghost procedure, Update_Model, but still
using a local variable, New_Length, to compute the length.
23 end Ring_Buffer;
19 begin
20 if First + Length <= Max_Size then
21 Content (First + Length) := E;
22 else
23 Content (Length - Max_Size + First) := E;
24 end if;
25 Length := Length + 1;
26 Update_Model;
27 end Push_Last;
28
29 end Ring_Buffer;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
ring_buffer.adb:8:21: info: range check proved
ring_buffer.adb:13:61: info: range check proved, in call inlined at ring_buffer.
↪adb:26
Everything's fine here. Model is only accessed inside Update_Model, itself a ghost proce-
dure, so it's fine to declare local variable New_Length without the Ghost aspect: everything
inside a ghost procedure body is ghost. Moreover, we don't need to add any contract to
Update_Model: it's inlined by GNATprove because it's a local procedure without a contract.
33.4.6 Example #6
The function Max_Array takes two arrays of the same length (but not necessarily with the
same bounds) as arguments and returns an array with each entry being the maximum
values of both arguments at that index.
8 end Array_Util;
18 end Array_Util;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_06
MD5: 4b8a6a9b1a3d4d228fe1e944914084fe
Prover output
array_util.adb:13:17: medium: overflow check might fail, cannot prove upper bound␣
↪for J + 1 [reason for check: result of addition must fit in a 32-bits machine␣
This program is correct, but GNATprove can't prove that J is always in the index range of
B (the unproved index check) or even that it's always within the bounds of its type (the
unproved overflow check). Indeed, when checking the body of the loop, GNATprove for-
gets everything about the current value of J because it's been modified by previous loop
iterations. To get more precise results, we need to provide a loop invariant.
33.4.7 Example #7
Let's add a loop invariant that states that J stays in the index range of B and let's protect
the increment to J by checking that it's not already the maximal integer value.
8 end Array_Util;
21 end Array_Util;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_07
MD5: 917629e0683725c23198f8a905a73c57
Prover output
The loop invariant now allows verifying that no runtime error can occur in the loop's body
(property INSIDE seen in section Loop Invariants (page 1089)). Unfortunately, GNATprove
fails to verify that the invariant stays valid after the first iteration of the loop (property
PRESERVE). Indeed, knowing that J is in B'Range in a given iteration isn't enough to prove
it'll remain so in the next iteration. We need a more precise invariant, linking J to the value
of the loop index I, like J = I - A'First + B'First.
33.4.8 Example #8
We now consider a version of Max_Array which takes arguments that have the same
bounds. We want to prove that Max_Array returns an array of the maximum values of
both its arguments at each index.
10 end Array_Util;
18 end Array_Util;
3 procedure Main is
4 A : Nat_Array := (1, 1, 2);
5 B : Nat_Array := (2, 1, 0);
6 R : Nat_Array (1 .. 3);
7 begin
8 R := Max_Array (A, B);
9 end Main;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_08
MD5: d0a04c214a632466a4fe4ec6cb7f8842
Prover output
Runtime output
Here, GNATprove doesn't manage to prove the loop invariant even for the first loop iteration
(property INIT seen in section Loop Invariants (page 1089)). In fact, the loop invariant is
incorrect, as you can see by executing the function Max_Array with assertions enabled: at
each loop iteration, R contains the maximum of A and B only until I - 1 because the I'th
index wasn't yet handled.
33.4.9 Example #9
We now consider a procedural version of Max_Array which updates its first argument in-
stead of returning a new array. We want to prove that Max_Array sets the maximum values
of both its arguments into each index in its first argument.
10 end Array_Util;
17 end Array_Util;
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
array_util.adb:7:13: info: loop invariant preservation proved
array_util.adb:7:13: info: loop invariant initialization proved
array_util.adb:7:39: info: overflow check proved
array_util.adb:8:18: info: index check proved
array_util.adb:8:50: info: index check proved
array_util.adb:8:57: info: index check proved
array_util.adb:10:13: info: loop invariant initialization proved
array_util.adb:10:13: info: loop invariant preservation proved
array_util.adb:10:44: info: index check proved
array_util.adb:10:63: info: index check proved
array_util.adb:11:25: info: index check proved
array_util.adb:12:25: info: index check proved
array_util.ads:7:14: info: postcondition proved
array_util.ads:8:20: info: index check proved
(continues on next page)
Everything is proved. The first loop invariant states that the values of A before the loop
index contains the maximum values of the arguments of Max_Array (referring to the input
value of A with A'Loop_Entry). The second loop invariant states that the values of A beyond
and including the loop index are the same as they were on entry. This is the frame condition
of the loop.
10 end Array_Util;
15 end Array_Util;
Project: Courses.Intro_To_Spark.Proof_of_Functional_Correctness.Example_10
MD5: 8bdc8432cbb3f26f58f63457408c7172
Prover output
Everything is still proved. GNATprove internally generates the frame condition for the loop,
so it's sufficient here to state that A before the loop index contains the maximum values of
the arguments of Max_Array.
Introduction to Embedded
Systems Programming
1109
Learning Ada
This course will teach you the basics of the Embedded Systems Programming using Ada.
This document was written by Patrick Rogers, with review by Stephen Baird, Tucker Taft,
Filip Gajowniczek, and Gustavo A. Hoffmann.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
267 http://creativecommons.org/licenses/by-sa/4.0
1111
Learning Ada
1112
CHAPTER
THIRTYFOUR
INTRODUCTION
This is a course about embedded systems programming. Embedded systems are every-
where today, including — just to name a few — the thermostats that control a building's
temperature, the power-steering controller in modern automobiles, and the control systems
in charge of jet engines.
Clearly, much can depend on these systems operating correctly. It might be only a matter of
comfort if the thermostat fails. But imagine what might happen if one of the critical control
systems in your car failed when you're out on the freeway. When a jet engine controller is
designed to have absolute control, it is known as a Full Authority Digital Engine Controller,
or FADEC for short. If a FADEC fails, the result can make international news.
Using Ada can help you get it right, and for less cost than other languages, if you use it
well. Many industrial organizations developing critical embedded software use Ada for that
reason. Our goal is to get you started in using it well.
The course is based on the assumption that you know some of the Ada language already,
preferably even some of the more advanced concepts. You don't need to know how to
use Ada constructs for embedded systems, of course, but you do need to know at least the
language basics. If you need that introduction, see the course Introduction to Ada (page 5).
We also assume that you already have some programming experience so we won't cover
CS-101.
Ideally, you also have some experience with low-level programming, because we will focus
on "how to do it in Ada." If you do, feel free to gloss over the introductory material. If not,
don't worry. We will cover enough for the course to be of value in any case.
We will introduce you to using Ada to do low level programming, such as how to specify
the layout of types, how to map variables of those types to specific addresses, when and
how to do unchecked programming (and how not to), and how to determine the validity of
incoming data, e.g., data from sensors that are occasionally faulty.
We will discuss development using more than Ada alone, nowadays a quite common ap-
proach. Specifically, how to interface with code and data written in other languages, and
how (and why) to work with assembly language.
Embedded systems interact with the outside world via embedded devices, such as A/D
converters, timers, actuators, sensors, and so forth. Frequently these devices are mapped
into the target memory address space. We will cover how to define and interact with these
memory-mapped devices.
Finally, we will show how to handle interrupts in Ada, using portable constructs.
1113
Learning Ada
34.2 Definitions
Before we go any further, what do we mean by "embedded system" anyway? It's time
to be specific. We're talking about a computer that is part of a larger system, in which
the capability to compute is not the larger system's primary function. These computers
are said to be "embedded" in the larger system: the enclosing thermostat controlling the
temperature, the power steering controller in the enclosing automobile, and the FADEC
embedded in the enclosing aircraft. So these are not stand-alone computers for general
purpose application execution.
As such, embedded systems typically have reduced resources available, especially power,
which means reduced processor speed and reduced memory on-board. For an example at
the small end of the spectrum, consider the computer embedded in a wearable device: it
must run for a long time on a very little battery, with comparatively little memory available.
But that's often true of bigger systems too, such as systems on aircraft where power (and
heat) are directly limiting factors.
As a result, developing embedded systems software can be more difficult than general
application development, not to mention that this software is potentially safety-critical.
Ada is known for use in very large, very long-lived projects (e.g., deployed for decades),
but it can also be used for very small systems with tight resource constraints. We'll show
you how.
We used the term "computer" above. You already know what that means, but you may be
thinking of your laptop or something like that, where the processor, memory, and devices
are all distinct, separate components. That can be the case for embedded systems too,
albeit in a different form-factor such as rack-mounted boards. However, be sure to expand
your definition to include the notion of a system-on-chip (SoC), in which the processor,
memory, and various useful devices are all on a single chip. Embedded systems don't
necessarily involve SoC computers but they frequently do. The techniques and information
in this course work on any of these kinds of computer.
Ada has always had facilities designed specifically for embedded systems. The language
includes constructs for directly manipulating hardware, for example, and direct interac-
tion with assembly language. These constructs are as effective as those of any high-level
programming language (yes, including C). These constructs are expressively powerful, well-
specified (so there are few surprises), efficient, and portable (within reason).
We say "within reason" because portability is a difficult goal for embedded systems. That's
because the hardware is so much a part of the application itself, rather than being ab-
stracted away as in a general-purpose application. That said, the hardware details can be
managed in Ada so that portability is maximized to the extent possible for the application.
But strictly speaking, not all software can or should be absolutely portable! If a specific
device is required, well, the program won't work with some other device. But to the extent
possible portability is obviously a good thing.
THIRTYFIVE
This section introduces a number of topics in low-level programming, in which the hard-
ware and the compiler's representation choices are much more in view at the source code
level. In comparatively high level code these topics are "abstracted away" in that the pro-
grammer can assume that the compiler does whatever is necessary on the current target
machine so that their code executes as intended. That approach is not sufficient in low-level
programming.
Note that we do not cover every possibility or language feature. Instead, we cover the
necessary concepts, and also potential surprises or pitfalls, so that the parts not covered
can be learned on your own.
There is a language design principle underlying the Ada facilities intended for implementing
embedded software. This design principle directly affects how the language is used, and
therefore, the portability and readability of the resulting application code.
This language design principle is known as the "separation principle." What's being sepa-
rated? The low-level, less portable aspects of some piece of code are separated from the
usage of that piece of code.
Don't confuse this with hiding unnecessary implementation details via compile-time visibil-
ity control (i.e., information hiding and encapsulation). That certainly should be done too.
Instead, because of the separation principle, we specify the low-level properties of some-
thing once, when we declare it. From then on, we can use regular Ada code to interact with
it. That way the bulk of the code — the usage — is like any other Ada code, and doesn't
propagate the low-level details all over the client code. This greatly simplifies usage and
understandability as well as easing porting to new hardware-specific aspects. You change
things in one place, rather than everywhere.
For example, consider a device mapped to the memory address space of the processor. To
interact with the device we interact with one or more memory cells. Reading input from the
device amounts to reading the value at the associated memory location. Likewise, sending
output to the device amounts to writing to that location.
To represent this device mapping we declare a variable of an appropriate type and specify
the starting address the object should occupy. (There are other ways too, but for a single,
statically mapped object this is the simplest approach.) We'd want to specify some other
characteristics as well, but let's focus on the address.
1117
Learning Ada
If the hardware presents an interface consisting of multiple fields within individual memory
cells, we can use a record type instead of a single unsigned type representing a single
word. Ada allows us to specify the exact record layout, down to the individual bit level, for
any types we may need to use for the record components. When we declare the object
we use that record type, again specifying the starting address. Then we can just refer to
the object's record components as usual, having the compiler compute the address offsets
required to access the components representing the individual hardware fields.
Note that we aren't saying that other languages cannot do this too. Many can, using good
programming practices. What we're saying is that those practices are designed into the
Ada way of doing it.
The Ada reference manual has an entire section dedicated to low-level programming. That's
section 13, "Representation Issues," which provides facilities for developers to query and
control aspects of various entities in their code, and for interfacing to hardware. Want to
specify the exact layout for a record type's components? Easy, and the compiler will check
your layout too. Want to specify the alignment of a type? That's easy too. And that's just
the beginning. We'll talk about these facilities as we go, but there's another point to make
about this section.
In particular, section 13 includes recommended levels of support to be provided by lan-
guage implementations, i.e., compilers and other associated tools. Although the word "rec-
ommended" is used, the recommendations are meant to be followed.
For example, section 13.3 says that, for some entity named X, "X'Address should produce
a useful result if X is an object that is aliased or of a by-reference type, or is an entity whose
Address has been specified." So, for example, if the programmer specifies the address
for a memory-mapped variable, the compiler cannot ignore that specification and instead,
for the sake of performance, represent that variable using a register. The object must be
represented as an addressable entity, as requested by the programmer. (Registers are not
addressable.)
We mention this because, although the recommended levels of support are intended to
be followed, those recommendations become requirements if the Systems Programming
(SP) Annex is implemented by the vendor. In that case the vendor's implementation of
section 13 must support at least the recommended levels. The SP Annex defines additional,
optional functionality oriented toward this programming domain; you want it anyway. (Like
all the annexes it adds no new syntax.) Almost all vendors, if not literally all, implement
the Annex so you can rely on the recommended levels of support.
Sometimes you need to know more about the underlying machine than is typical for general
purpose applications. For example, your numerical analysis algorithm might need to know
the maximum number of digits of precision that a floating-point number can have on this
specific machine. For networking code, you will need to know the "endianness" of the
machine so you can know whether to swap the bytes in an Ethernet packet. You'd go look
in the limits.h file in C implementations, but in Ada we go to a package named System to
get this information.
Clearly, these implementation values will vary with the hardware, so the package declares
constants with implementation-defined values. The names of the constants are what's
portable, you can count on them being the same in any Ada implementation.
However, vendors can add implementation-defined declarations to the language-defined
content in package System. You might require some of those additions, but portability
could then suffer when moving to a new vendor's compiler. Try not to use them unless it is
unavoidable. Ideally these additions will appear in the private part of the package, so the
implementation can use them but application code cannot.
For examples of the useful, language-defined constants, here are those for the numeric
limits of an Ada compiler for an Arm 32-bit SoC:
Min_Int and Max_Int supply the most-negative and most-positive integer values supported
by the machine.
Max_Binary_Modulus is the largest power of two allowed as the modulus of a modular type
definition.
But a modular type need not be defined in terms of powers of two. An arbitrary modulus
is allowed, as long as it is not bigger than the machine can handle. That's specified by
Max_Nonbinary_Modulus, the largest non-power-of-two value allowed as the modulus of a
modular type definition.
Max_Base_Digits is the largest value allowed for the requested decimal precision in a
floating-point type's definition.
We won't go over all of the above, you get the idea. Let's examine the more important
contents.
Two of the most frequently referenced constants in System are the following, especially the
first. (The values here are again for the Arm 32-bit SoC):
Storage_Unit : constant := 8;
Word_Size : constant := 32;
Storage_Unit is the number of bits per memory storage element. Storage elements are
the components of memory cells, and typically correspond to the individually addressable
memory elements. A "byte" would correspond to a storage element with the above constant
value.
Consider a typical idiom for determining the number of whole storage elements an object
named X occupies:
Remember that 'Size returns a value in terms of bits. There are more direct ways to
determine that size information but this will serve as an example of the sort of thing you
might do with that constant.
A machine "word" is the largest amount of storage that can be conveniently and efficiently
manipulated by the hardware, given the implementation's run-time model. A word consists
of some number of storage elements, maybe one but typically more than one. As the unit
the machine natively manipulates, words are expected to be independently addressable.
(On some machines only words are independently addressable.)
Word_Size is the number of bits in the machine word. On a 32-bit machine we'd expect
Word_Size to have a value of 32; on a 64-bit machine it would probably be 64, and so on.
Storage_Unit and Word_Size are obviously related.
Another frequently referenced declaration in package System is that of the type represent-
ing memory addresses, along with a constant for the null address designating no storage
element.
You may be wondering why type Address is a private type, since that choice means that
we programmers cannot treat it like an ordinary (unsigned) integer value. Portability is
of course the issue, because addressing, and thus address representation, varies among
computer architectures. Not all architectures have a flat address space directly referenced
by numeric values, although that is common. Some are represented by a base address plus
an offset, for example. Therefore, the representation for type Address is hidden from us,
the clients. Consequently we cannot simply treat address values as numeric values. Don't
worry, though. The operations we need are provided.
Package System declares these comparison functions, for example:
These functions are intrinsic, i.e., built-in, meaning that the compiler generates the code
for them directly at the point of calls. There is no actual function body for any of them so
there is no performance penalty.
Any private type directly supports the equality function, and consequently the inequality
function, as well as assignment. What we don't get here is address arithmetic, again be-
cause we don't have a compile-time view of the actual representation. That functionality is
provided by package System.Storage_Elements, a child package we will cover later. We
should say though, that the need for address arithmetic in Ada is rare, especially compared
to C.
Having type Address presented as a private type is not, strictly speaking, required by the
language. Doing so is a good idea for the reasons given above, and is common among
vendors. Not all vendors do, though.
Note that Address is the type of the result of the query attribute Address.
...
begin
Value := ...
We have elided the code that gets the bytes into Value, for the sake of simplicity. How
the bytes are actually swapped by function Byte_Swapped is also irrelevant. The point
here is the if-statement: the expression compares the Default_Bit_Order constant to
High_Order_First to see if this execution is on a Big Endian machine. If not, it swaps
the bytes because the incoming bytes are always received in "wire-order," i.e., Big Endian
order.
Another important set of declarations in package System define the values for priorities,
including interrupt priorities. We will ignore them until we get to the section on interrupt
handling.
Finally, and perhaps surprisingly, a few declarations in package System are almost always
(if not actually always) ignored.
Values of type Name are the names of alternative machine configurations supported by the
implementation. System_Name represents the current machine configuration. We've never
seen any actual use of this.
Memory_Size is an implementation-defined value that is intended to reflect the memory
size of the configuration, in units of storage elements. What the value actually refers to
is not specified. Is it the size of the address space, i.e., the amount possible, or is it the
amount of physical memory actually on the machine, or what? In any case, the amount
of memory available to a given computer is neither dependent upon, nor reflected by, this
constant. Consequently, Memory_Size is not useful either.
Why have something defined in the language that nobody uses? In short, it seemed like a
good idea at the time when Ada was first defined. Upward-compatibility concerns propagate
these declarations forward as the language evolves, just in case somebody does use them.
As we mentioned in the introduction, in low-level programming the hardware and the com-
piler's representation choices can come to the forefront. You can, therefore, query many
such choices.
For example, let's say we want to query the addresses of some objects because we are
calling the imported C memcpy function. That function requires two addresses to be passed
to the call: one for the source, and one for the destination. We can use the 'Address
attribute to get those values.
We will explore importing routines and objects implemented in other languages elsewhere.
For now, just understand that we will have an Ada declaration for the imported routine that
tells the compiler how it should be called. Let's assume we have an Ada function declared
like so:
function MemCopy
(Destination : System.Address;
Source : System.Address;
Length : Natural)
return Address
with
Import,
Convention => C,
Link_Name => "memcpy",
Pre => Source /= Null_Address and then
Destination /= Null_Address and then
not Overlapping (Destination, Source, Length),
Post => MemCopy'Result = Destination;
-- Copies Length bytes from the object designated by Source to the object
-- designated by Destination.
The three aspects that do the importing are specified after the reserved word with but
can be ignored for this discussion. We'll talk about them later. The preconditions make
explicit the otherwise implicit requirements for the arguments passed to memcpy, and the
postcondition specifies the expected result returned from a successful call. Neither the
preconditions nor the postconditions are required for importing external entities but they
are good "guard-rails" for using those entities. If we call it incorrectly the precondition will
inform us, and likewise, if we misunderstand the result the postcondition will let us know
(at least to the extent that the return value does that).
For a sample call to our imported routine, imagine that we have a procedure that copies
the bytes of a String parameter into a Buffer parameter, which is just a contiguous array
of bytes. We need to tell MemCopy the addresses of the arguments passed so we apply the
'Address attribute accordingly:
procedure Put (This : in out Buffer; Start : Index; Value : String) is
Result : System.Address with Unreferenced;
begin
Result := MemCopy (Destination => This (Start)'Address,
Source => Value'Address,
Length => Value'Length);
end Put;
The order of the address parameters is easily confused so we use the named association
format for specifying the actual parameters in the call.
Although we assign Result we don't otherwise use it, so we tell the compiler this is not
a mistake via the Unreferenced aspect. And if we do turn around and reference it the
compiler will complain, as it should. Note that Unreferenced is defined by GNAT, so usage
is not necessarily portable. Other vendors may or may not implement something like it,
perhaps with a different name.
(We don't show the preconditions for Put, but they would have specified that Start must be
a valid index into this particular buffer, and that there must be room in the Buffer argument
for the number of bytes in Value when starting at the Start index, so that we don't copy
past the end of the Buffer argument.)
There are other characteristics we might want to query too.
We might want to ask the compiler what alignment it chose for a given object (or type, for
all such objects).
For a type, when Alignment returns a non-zero value we can be sure that the compiler
will allocate storage for objects of the type at correspondingly aligned addresses (unless
we force it to do otherwise). Similarly, references to dynamically allocated objects of the
type will be to properly aligned locations. Otherwise, an Alignment of zero means that the
guarantee does not hold. That could happen if the type is packed down into a composite
object, such as an array of Booleans. We'll discuss "packing" soon. More commonly, the
smallest likely value is 1, meaning that any storage element's address will suffice. If the
machine has no particular natural alignments, then all type alignments will probably be
1 by default. That would be somewhat rare today, though, because modern processors
usually have comparatively strict alignment requirements.
We can ask for the amount of storage associated with various entities. For example, when
applied to a task, 'Storage_Size tells us the number of storage elements reserved for the
task's execution. The value includes the size of the task's stack, if it has one. We aren't
told if other required storage, used internally in the implementation, is also included in this
number. Often that other storage is not included in this number, but it could be.
Storage_Size is also defined for access types. The meaning is a little complicated. Access
types can be classified into those that designate only variables and constants ("access-
to-object") and those that can designate subprograms. Each access-to-object type has an
associated storage pool. The storage allocated by new comes from the pool, and instances
of Unchecked_Deallocation return storage to the pool.
When applied to an access-to-object type, Storage_Size gives us the number of storage
elements reserved for the corresponding pool.
Note that Storage_Size doesn't tell us how much available, unallocated space remains in
a pool. It includes both allocated and unallocated space. Note, too, that although each
access-to-object type has an associated pool, that doesn't mean that each one has a dis-
tinct, dedicated pool. They might all share one, by default. On an operating system, such
as Linux, the default shared pool might even be implicit, consisting merely of calls to the
OS routines in C.
As a result, querying Storage_Size for access types and tasks is not necessarily all that
useful. Specifying the sizes, on the other hand, definitely can be useful.
That said, we can create our own pool types and define precisely how they are sized and
how allocation and deallocation work, so in that case querying the size for access types
could be more useful.
For an array type or object, 'Component_Size provides the size in bits of the individual
components.
More useful are the following two attributes that query a degree of memory sharing between
objects.
Applied to an object, 'Has_Same_Storage is a Boolean function that takes another object
of any type as the argument. It indicates whether the two objects' representations occupy
exactly the same bits.
Consider type Boolean, which has two possible values. One bit will suffice, and indeed the
language standard requires Boolean'Size to be the value 1.
This meaning also applies to subtypes, which can constrain the number of values for a
scalar type. Consider subtype Natural. That's a subtype defined by the language to be
type Integer but with a range of 0 .. Integer'Last. On a 32-bit machine we would expect
Integer to be a native type, and thus 32-bits. On such a machine if we say Integer'Size
we will indeed get 32. But if we say Natural'Size we will get 31, not 32, because only 31
bits are needed to represent that range on that machine.
The size of objects, on the other hand, cannot be just a matter of the possible values. Con-
sider type Boolean again, where Boolean'Size is required to be 1. No compiler is likely to
allocate one bit to a Boolean variable, because typical machines don't support individually-
addressable bits. Instead, addresses refer to storage elements, of a size indicated by the
Storage_Unit constant. The compiler will allocate the smallest number of storage ele-
ments necessary, consistent with other considerations such as alignment. Therefore, for
a machine that has Storage_Unit set to a value of eight, we can assume that a compiler
for that machine will allocate an entire eight-bit storage element to a stand-alone Boolean
variable. The other seven bits are simply not used by that variable. Moreover, those seven
bits are not used by any other stand-alone object either, because access would be far less
efficient, and such sharing would require some kind of locking to prevent tasks from inter-
fering with each other when accessing those stand-alone objects. (Stand-alone objects are
independently addressable; they wouldn't stand alone otherwise.)
By the same token (and still assuming a 32-bit machine), a compiler will allocate more than
31 bits to a variable of subtype Natural because there is no 31-bit addressable unit. The
variable will get all 32-bits.
Note that we're talking about individual, stand-alone variables. Components of composite
types, on the other hand, might indeed share bytes if the individual components don't
require all the bits of their storage elements. You'd have to request that representation,
though, with most implementations, because accessing the components at run-time would
require more machine instructions. We'll go into the details of that later.
Let's talk further about sizes of types.
For record types, 'Size gives the minimum number of bits required to represent the whole
composite value. But again, that's not necessarily the number of bits required for the ob-
jects' in-memory representation. The order of the components within the record can make
a difference, as well as their alignments. The compiler will respect the alignment require-
ments of the components, and may add padding bytes within the record and also at the end
to ensure components start at addresses compatible with their alignment requirements. As
a result the overall size could be larger.
Note that Ada compilers are allowed to reorder the components; the order in memory might
not match the order in the source code.
For example, consider this record type and its components:
In the figure, we see a record type with some components, and a sample layout for that
record type assuming the compiler does not reorder the components. Observe that some
bytes allocated to objects of type R are unused (the darkly shaded ones). In this case that's
because the alignment of subtype S happens to be 4 on this machine. The component X of
that subtype S cannot start at byte offset 1, or 2, or 3, because those addresses would not
satisfy the alignment constraint of S. (We're assuming byte 0 is at a word-aligned address.)
Therefore, X starts at the object's starting address plus 4. Components B and C are of types
that have an alignment of 1, so they can start at any storage element. They immediately
follow the bytes allocated to component X. Therefore, R'Size is 80, or 10 bytes. The three
bytes following component M are simply not used.
But what about the two bytes following the last component C? They could be allocated to
stand-alone objects if they would fit. More likely, though, the compiler will allocate those
two bytes to objects of type R, that is, 12 bytes instead of 10 are allocated. As a result, 96
bits are actually used in memory. The extra, unused 16 bits are "padding."
Why add unused padding? It simplifies the memory allocation of objects of type R. Suppose
some array type has components of record type R. Assuming the first component is aligned
properly, every following component will also be aligned properly, automatically, because
the two padding bytes are considered parts of the components.
To make that work, the compiler takes the most stringent alignment of all the record type's
components and uses that for the alignment of the overall record type. That way, any
address that satisfies the record object's alignment will satisfy the components' alignment
requirements. The alignment is component X, of subtype S, is 4. The other components
have an alignment of 1, therefore R'Alignment is 4. An aligned address plus 12 will also
be an aligned address.
This rounding up based on alignment is recommended behavior for the compiler, not a
requirement, but is reasonable and typical among vendors. Although it can result in unused
storage, that's the price paid for speed of access (or even correctness for machines that
would fault on misaligned component accesses).
As you can see, alignment is a critical factor in the sizes of composite objects. If you
care about the layout of the type you very likely need to care about the alignment of the
components and overall record type.
Ada compilers are allowed to reorder the components of record types in order to minimize
these gaps or satisfy the alignment requirements of the components. Some compilers do,
some don't. Consider the type R again, this time with the first two components switched in
the component declaration order:
Now R'Size will report 56 bits instead of 80. The one trailing byte will still be padding, but
only that one.
What about unbounded types, for example type String? Querying the 'Size in that case
would provide an implementation-defined result. A somewhat silly thing to do, really, since
the type — by definition — doesn't specify how many components are involved.
Usually, though, you don't want to query the size of a type. Most of the time what you want
is the size of objects of the type. Going back to sending values over the wire, the code
should query the size of the parameter holding the value to be sent. That will tell you how
many bits are really needed.
One last point: GNAT, and now Ada 202x, define an attribute named Object_Size. It does
just what the name suggests: what 'Size does when applied to objects rather than types.
GNAT also defines another attribute, named Value_Size, that does what 'Size does when
applied to types. The former is far more useful so Ada has standardized it.
Recall that we said Boolean'Size is required to be 1, and that stand-alone objects of type
Boolean are very likely allocated some integral number of storage elements (e.g., bytes)
in memory, typically one. What about arrays of Booleans? Suppose we have an array of 16
Boolean components. How big are objects of the array type? It depends on the machine.
Continuing with our hypothetical (but typical) byte-addressable machine, for the sake of ef-
ficient access each component is almost certainly allocated an individual byte rather than
a single bit, just like stand-alone objects. Consequently, our array of 16 Booleans will be
reported by 'Size to be 128 bits, i.e., 16 bytes. If you wanted a bit-mask, in which each
Boolean component is allocated a single bit and the total array size is 16 bits, you'd have
a problem. The compiler assumes you want speed of access rather than storage minimiza-
tion, and normally that would be the right assumption.
Naturally there is a solution. Ada allows us to specify the representation characteristics of
types, and thus objects of those types, including their bit-wise layouts. It also allows us to
specify the representation of individual objects. You should understand, though, that the
compiler is not required to do what you ask, because you might ask the impossible. For
example, if you specify that the array of 16 Booleans is to be represented completely in 15
bits, what can the compiler do? Rejecting that specification is the only reasonable response.
But if you specify something possible, the compiler must do what you ask, absent some
compelling reason to the contrary.
With that in mind, let's examine setting the size for types.
So, how do we specify that we want our array of 16 Boolean components to be allocated
one bit per component, for a total allocation of 16 bits? There are a couple of ways, one
somewhat better than the other.
First, you can ask that the compiler "pack" the components into as small a number of bits
as it can:
That likely does what you want: Bits16'Size will probably be 16.
But realize that the Pack aspect (and corresponding pragma) is merely a request that the
compiler do its best to minimize the number of bits allocated, not necessarily that it do
exactly what you expected or required.
We could set the size of the entire array type:
But the language standard says that a Size clause on array and record types should not
affect the internal layout of their components. That's Implementation Advice, so not norma-
tive, but implementations are really expected to follow the advice, absent some compelling
reason. That's what the Pack aspect, record representation clauses, and Component_Size
clauses are for. (We'll talk about record representation clauses momentarily.) That said, at
least one other vendor's compiler would have changed the size of the array type because of
the Size clause, so GNAT defines a configuration pragma named Implicit_Packing that
overrides the default behavior. With that pragma applied, the Size clause would compile
and suffice to make the overall size be 16. That's a vendor-defined pragma though, so not
portable.
Therefore, the best way to set the size for the array type is to set the size of the individual
components, via the Component_Size aspect as the Implementation Advice indicates. That
will say what we really want, rather than a "best effort" request for the compiler, and is
portable:
With this approach the compiler must either use the specified size for each component or
refuse to compile the code. If it compiles, objects of the array type will be 16 bits total (plus
any padding bits required to make objects have a size that is a multiple of Storage_Unit,
typically zero on modern machines).
Now that we have a bit-mask array type, let's put it to use.
Let's say that you have an object that is represented as a simple signed integer because,
for most usage, that's the appropriate representation. Sometimes, though, let's say you
need to access individual bits of the object instead of the whole numeric value. Signed
integer types don't provide bit-level access. In Ada we'd say that the "view" presented by
the object's type doesn't include bit-oriented operations. Therefore, we need to add a view
to the object that does provide them. A different view will require an additional type for the
same object.
Applying different types, and thus their operations, to the same object is known as type
punning268 in computer programming. Realize that doing so circumvents the static strong
typing we harness to protect us from ourselves and from others. Use it with care! (For
example, limit the compile-time visibility to such code.)
One way to add a view is to express an "overlay," in which an object of one type is placed
at the same memory location as a distinct object of a different type, thus "overlaying" one
object over the other in memory. The different types present different views, therefore
different operations available for the shared memory cells. Our hypothetical example uses
two views, but you can overlay as many different views as needed. (That said, requiring a
large number of different views of the same object would be suspect.)
There are other ways in Ada to apply different views, some more flexible than others, but
an overlay is a simple one that will often suffice.
Here is an implementation of the overlay approach, using our bit-mask array type:
X : Integer;
Y : Bits32 with Address => X'Address;
We can query the addresses of objects, and other things too, but objects, especially vari-
ables, are the most common case. In the above, we say X'Address to query the starting
address of object X. With that information we know what address to specify for our bit-mask
overlay object Y. Now X and Y are aliases for the same memory cells, and therefore we can
manipulate and query that memory as either a signed integer or as an array of bits. Reading
or updating individual array components accesses the individual bits of the overlaid object.
Instead of the Bits32 array type, we could have specified a modular type for the overlay
Y to get a view providing bit-oriented operations. Overlaying such an array was a common
idiom prior to the introduction of modular "unsigned" types in Ada, and remains useful
for accessing individual bits. In other words, using a modular type for Y, you could indeed
access an individual bit by passing a mask value to the and operator defined in any modular
type's view. Using a bit array representation lets the compiler do that work for you, in the
generated code. The source code will be both easier to read and more explicit about what
it is doing when using the bit array overlay.
268 https://en.wikipedia.org/wiki/Type_punning
One final issue remains: in our specific overlay example the compiler would likely generate
code that works. But strictly speaking it might not.
The Ada language rules say that for such an overlaid object — Y in the example above — the
compiler should not perform optimizations regarding Y that it would otherwise apply in the
absence of aliases. That's necessary, functionally, but may imply degraded performance
regarding Y, so keep it in mind. Aliasing precludes some desirable optimizations.
But what about X in the example above? We're querying that object's address, not specify-
ing it, so the RM rule precluding optimizations doesn't apply to X. That can be problematic.
The compiler might very well place X in a register, for example, for the sake of the sig-
nificant performance increase (another way of being friendly). But in that case System.
Null_Address will be returned by the X'Address query and, consequently, the declaration
for Y will not result in the desired overlaying.
Therefore, we should mark X as explicitly aliased to ensure that X'Address is well-defined:
X : aliased Integer;
Y : Bits32 with Address => X'Address;
The only difference in the version above is the addition of aliased in the declaration of X.
Now we can be certain that the optimizer will not represent X in some way incompatible
with the idiom, and X'Address will be well-defined.
In our example X and Y are clearly declared in the same compilation unit. Most compilers
will be friendly in this scenario, representing X in such a way that querying the address will
return a non-null address value even if aliased is not applied. Indeed, aliased is relatively
new to Ada, and earlier compilers typically emitted code that would handle the overlay as
intended.
But suppose, instead of being declared in the same declarative part, that X was declared
in some other compilation unit. Let's say it is in the visible part of a package declara-
tion. (Assume X is visible to clients for some good reason.) That package declaration can
be, and usually will be, compiled independently of clients, with the result that X might be
represented in some way that cannot supporting querying the address meaningfully.
Therefore, the declaration of X in the package spec should be marked as aliased, explicitly:
package P is
X : aliased Integer;
end P;
Then, in the client code declaring the overlay, we only declare Y, assuming a with-clause
for P:
All well and good, but how did the developer of the package know that some other unit, a
client of the package, would query the address of X, such that it needed to be marked as
aliased? Indeed, the package developer might not know. Yet the programmer is respon-
sible for ensuring a valid and appropriate Address value is used in the declaration of Y.
Execution is erroneous otherwise, so we can't say what would happen in that case. Maybe
an exception is raised or a machine trap, maybe not.
Worse, the switches that were applied when compiling the spec for package P can make a
difference: P.X might not be placed in a register unless the optimizer is enabled. Hence the
client code using Y might work as expected when built for debugging, with the optimizer
disabled, and then not do so when re-built for the final release. You'd probably have to
solve this issue by debugging the application.
On a related note, you may be asking yourself how to know that type Integer is 32 bits
wide, so that we know what size array to use for the bit-mask. The answer is that you just
have to know the target well when doing low-level programming. The hardware becomes
much more visible, as we mentioned.
That said, you could at least verify the assumption:
That's a vendor-defined pragma so this is not fully portable. It isn't an unusual pragma,
though, so at least you can probably get the same functionality even if the pragma name
varies.
Overlays aren't always structured like our example above, i.e., with two objects declared
at the same time. We might apply a different type to the same memory locations at differ-
ent times. Here's an example from the ADL to illustrate the idea. We'll elaborate on this
example later, in another section.
First, a package declaration, with two functions that provide a device-specific unique iden-
tifier located in shared memory. Each function provides the same Id value in a distinct
format. One format is a string of 12 characters, the other is a sequence of three 32-bit
values. Hence both representations are the same size.
package STM32.Device_Id is
end STM32.Device_Id;
In the package body we implement the functions as two ways to access the same shared
memory, specified by ID_Address:
with System;
end STM32.Device_Id;
That will compile successfully, because there will be a signed integer hardware type with
at least that range. (Not necessarily, legally speaking, but realistically speaking, there
will be such a hardware type.) Indeed, it may be an 8-bit signed integer, in which case
Device_Register'Size will give us 5, but objects of the type will have a size of 8, unavoid-
ably, even though we set Size to 5.
The difference between the type and object sizes can lead to potentially problematic code:
My_Device : Device_Register
with Address => To_Address (...);
The code compiles successfully, and tries to map a byte to a hardware device that is physi-
cally connected to one storage element in the processor memory space. The actual address
is elided as it is not important here.
That code might work too, but it might not. We might think that My_Device'Size is 8, and
that My_Device'Address points at an 8-bit location. However, this isn't necessarily so, as
we saw with the supposedly 5-bit example earlier. Maybe the smallest signed integer the
hardware has is 16-bits wide. The code would compile because a 16-bit signed numeric
type can certainly handle the 8-bit range requested. My_Device'Size would be then 16,
and because 'Address gives us the starting storage element, My_Device'Address might
designate the high-order byte of the overall 16-bit object. When the compiler reads the
two bytes for My_Device what will happen? One of the bytes will be the data presented by
the hardware device mapped to the memory. The other byte will contain undefined junk,
whatever happens to be in the memory cell at the time. We might have to debug the code
a long time to identify that as the problem. More likely we'll conclude we have a failed
device.
The correct way to write the code is to specify the size of the object instead of the type:
If the compiler cannot support stand-alone 8-bit objects for the type, the code won't com-
pile.
Alternatively, we could change the earlier Size clause on the type to apply Object_Size
instead:
The choice between the two approaches comes down to personal preference, at least if
only a small number of stand-alone objects of the type are going to be declared. With
either approach, if the implementation cannot support 8-bit stand-alone objects, we find
out that there is a problem at compile-time. That's always cheaper than debugging.
You might conclude that setting the Size for a type serves no purpose. That's not an
unreasonable conclusion, given what you've seen, but in fact there are reasons to do so.
However, there are only a few specific cases so we will save the reasons for the discussions
of the specific cases.
There is one general case, though, for setting the 'Size of a type. Specifically, you may
want to specify the size that you think is the minimum possible, and you want the com-
piler to confirm that belief. This would be one of the so-called "confirming" representation
clauses, in which the representation detail is what the compiler would have chosen anyway,
absent the specification. You're not actually changing anything, you're just getting confir-
mation via Size whether or not the compiler accepts the clause. Suppose, for example,
that you have an enumeration type with 256 values. For enumeration types, the compiler
allocates the smallest number of bits required to represent all the values, rounded up to
the nearest storage element. (It's not like C, where enums are just named int values.) For
256 values, an eight-bit byte would suffice, so setting the size to 8 would be confirming.
But suppose we actually had 257 enumerals, accidentally? Our size clause set to 8 would
not compile, and we'd be told that something is amiss.
However, note that if your supposedly "confirming" size clause actually specifies a size
larger than what the compiler would have chosen, you won't know, because the compiler
will silently accept sizes larger than necessary. It just won't accept sizes that are too small.
There are other confirming representation clauses as well. Thinking again of enumeration
types, the underlying numeric values are integers, starting with zero and consecutively
increasing from there up to N-1, where N is the total number of enumeral values.
For example:
But it was not guaranteed in the original version of the language, so if you wanted to be
sure of the encoding values you would have specified the above. It wasn't necessarily
confirming before Ada 95, in other words.
But let's also say that the underlying numeric values are not what you want because you're
interacting with some device and the commands are encoded with values other than 0 and
1. Maybe you want to use an enumeration type because you want to specify all the possible
values actually used by clients. If you just used some numeric type instead and made up
constants for On and Off, there's nothing to keep clients from using other numeric values
in place of the two constants (absent some comparatively heavy code to prevent that from
happening). Better to use the compiler to make that impossible in the first place, rather
than debug the code to find the incorrect values used. Therefore, we could specify different
encodings:
Now the compiler will use those encoding values instead of 0 and 1, transparently to client
code.
The encoding values specified must maintain the relative ordering, otherwise the relational
operators won't work correctly. For example, for type Commands above, Off is less than
On, so the specified encoding value for Off must be less than that of On.
Note that the values given in the example no longer increase consecutively, i.e., there's a
gap. That gap is OK, in itself. As long as we use the two enumerals the same way we'd use
named constants, all is well. Otherwise, there is both a storage issue and a performance
issue possible. Let's say that we use that enumeration type as the index for an array type.
Perfectly legal, but how much storage is allocated to objects of this array type? Enough for
exactly two components? Four, with two unused? The answer depends on the compiler,
and is therefore not portable. The bigger the gaps, the bigger the overall storage difference
possible. Likewise, imagine we have a for-loop iterating over the index values of one of
these array objects. The for-loop parameter cannot be coded by the compiler to start at
0, clearly, because there is no index (enumeration) value corresponding to 0. Similarly, to
get the next index, the compiler cannot have the code simply increment the current value.
Working around that takes some extra code, and takes some extra time that would not be
required if we did not have the gaps.
The performance degradation can be significant compared to the usual code generated for a
for-loop. Some coding guidelines say that you shouldn't use an enumeration representation
clause for this reason, with or without gaps. Now that Ada has type predicates we could
limit the values used by clients for a numeric type, so an enumeration type is not the only
way to get a restricted set of named, encoded values.
On : constant Commands := 2;
Off : constant Commands := 4;
The storage and performance issues bring us back to confirming clauses. We want the com-
piler to recognize them as such, so that it can generate the usual code, thereby avoiding
the unnecessary portability and performance issues. Why would we have such a confirming
clause now? It might be left over from the original version of the language, written before
the Ada 95 change. Some projects have lifetimes of several decades, after all, and chang-
ing the code can be expensive (certified code, for example). Whether the compiler does
recognize confirming clauses is a feature of the compiler implementation. We can expect
a mature compiler to do so, but there's no guarantee.
Now let's turn to what is arguably the most common representation specification, that of
record type layouts.
Recall from the discussion above that Ada compilers are allowed to reorder record com-
ponents in physical memory. In other words, the textual order in the source code is not
necessarily the physical order in memory. That's different from, say, C, where what you
write is what you get, and you better know what you're doing. On some targets a misaligned
struct component access will perform very poorly, or even trap and halt, but that's not
the C compiler's fault. In Ada you'd have to explicitly specify the problematic layout. Oth-
erwise, if compilation is successful, the Ada compiler must find a representation that will
work, either by reordering the components or by some other means. Otherwise it won't
compile.
GNAT did not reorder components until relatively recently but does now, at least for the
more egregious performance cases. It does this reordering silently, too, although there is
a switch to have it warn you when it does. To prevent reordering, GNAT defines a pragma
named No_Component_Reorder that does what the name suggests. You can apply it to
individual record types, or globally, as a configuration pragma. But of course because the
pragma is vendor defined it is not portable.
Therefore, if you care about the record components' layout in memory, the best approach
is to specify the layout explicitly. For example, perhaps you are passing data to code writ-
ten in C. In that case, you need the component order in memory to match the order given
in the corresponding C struct declaration. That order in memory is not necessarily guar-
anteed from the order in the Ada source code. The Ada compiler is allowed to chose the
representation unless you specify it, and it might chose a different layout from the one
given. (Ordinarily, letting the compiler chose the layout is the most desirable approach,
but in this case we have an external layout requirement.)
Fortunately, specifying a record type's layout is straightforward. The record layout speci-
fication consists of the storage places for some or all components, specified with a record
representation clause. This clause specifies the order, position, and size of components
(including discriminants, if any).
The approach is to first define the record type, as usual, using any component order you
like — you're about to specify the physical layout explicitly, in the next step.
Let's reuse that record type from the earlier discussion:
type R is record
M : My_Int;
X : S;
B : Boolean;
C : Character;
end record;
The resulting layout might be like so, assuming the compiler doesn't reorder the compo-
nents:
As a result, R'Size will be 80 bits (10 bytes), but those last two bytes will be will be allocated
to objects, for an Object_Size of 96 bits (12 bytes). We'll change that with an explicit layout
specification.
Having declared the record type, the second step consists of defining the corresponding
record representation clause giving the components' layout. The clause uses syntax that
somewhat mirrors that of a record type declaration. The components' names appear, as
in a record type declaration. But now, we don't repeat the components' types, instead we
give their relative positions within the record, in terms of a relative offset that starts at zero.
We also specify the bits we want them to occupy within the storage elements starting at
that offset.
Now we'll get the optimized order, and we'll always get that order, or the layout specification
won't compile in the first place. In the following diagram, both layouts, the default, and the
one resulting from the record representation clause, are depicted for comparison:
R'Size will be 56 bits (7 bytes), but that last padding byte will also be allocated to objects,
so the Object_Size will be 64 bits (8 bytes).
Notice how we gave each component an offset, after the reserved word at. These offsets
are in terms of storage elements, and specify their positions within the record object as a
whole. They are relative to the beginning of the memory allocated to the record object so
they are numbered starting at zero. We want the X component to be the very first com-
ponent in the allocated memory so the offset for that one is zero. The M component, in
comparison, starts at an offset of 4 because we are allocating 4 bytes to the prior com-
ponent X: bytes 0 through 3 specifically. M just occupies one storage element so the next
type R is record
S : String (1 .. 11);
B : Boolean;
end record;
Component S is to be the first component in memory in this example, hence the position
offset is 0, for the first byte of S. Next, S is 11 characters long, or 88 bits, so the bit range
is 0 .. 87. That's 11 bytes of course, so S occupies storage elements 0 .. 10. Therefore, the
next component position must be at least 11, unless there is to be a gap, in which case it
would be greater than 11. We'll place B immediately after the last character of S, so B is at
storage element offset 11 and occupying all that one byte's bits.
We'll have more to say about record type layouts but first we need to talk about alignment.
Modern target architectures are comparatively strict about the address alignments for some
of their types. If the alignment is off, an access to the memory for objects of the type can
have highly undesirable consequences. Some targets will experience seriously degraded
performance. On others, the target will halt altogether. As you can see, getting the align-
ment correct is a low-level, but vital, part of correct code on these machines.
Normally the compiler does this work for us, choosing an alignment that is both possible for
the target and also optimal for speed of access. You can, however, override the compiler's
alignment choice using an attribute definition clause or the Alignment aspect. You can do
so on types other than record types, but specifying it on record types is typical. Here's our
example record type with the alignment specified via the aspect:
type R is record
M : My_Int;
X : S;
B : Boolean;
C : Character;
end record with
Alignment => 1;
Alignment values are in terms of storage elements. The effect of the aspect or attribute
clause is to ensure that the starting address of the memory allocated to objects of the type
will be a multiple of the specified value.
In fact, whenever we specify a record type layout we really should also specify the record
type's alignment, even though doing so is optional. Why? The alignment makes a difference
in the overall record object's size. We've seen that already, with the padding bytes: the
compiler will respect the alignment requirements of the components, and may add padding
bytes within the record and also at the end to ensure components start at addresses com-
patible with their alignment requirements. The alignment also affects the size allocated
to the record type even when the components are already aligned. As a result the overall
size could be larger than we want for the sake of space. Additionally, when we pass such
objects to code written in other languages, we want to ensure that the starting address of
these objects is aligned as the external code expects. The compiler might not choose that
required alignment by default.
Specifying alignment for record types is so useful that in the first version of Ada there was
no syntax to specify alignment for anything other than record types (via the obsolete at
mod clause on record representation clauses).
For that reason GNAT provides a pragma named Optimize_Alignment. This is a configu-
ration pragma that affects the compiler's choice of default alignments where no alignment
is explicitly specified. There is a time/space trade-off in the selection of these values, as
we've seen. The normal choice tries to balance these two characteristics, but with an ar-
gument to the pragma you can give more weight to one or the other. The best approach
is to specify the alignments explicitly, per type, for those that require specific alignment
values. The pragma has the nice property of giving general guidance to the compiler for
what should be done for the other types and objects not explicitly specified.
Now let's look into the details. We'll use a case study for this purpose, including specifying
sizes as well as alignments.
The code for the case study is as follows. It uses Size clauses to specify the Sizes, instead
of the Size aspect, just to emphasize that the Size clause approach is not obsolete.
package Some_Types is
end Some_Types;
When we compile this, the compiler will complain that the size for List is too small, i.e.,
that the minimum allowed is 96 bits instead of the 72 we specified. We specified 24 * 3
because we said the record size should be 24 bits, and we want our array to contain 3
record components of that size, so 72 seems right.
What's wrong? As we've shown earlier, specifying the record type size doesn't necessarily
mean that objects (in this case array components) are that size. The object size could be
bigger than we specified for the type. In this case, the compiler says we need 96 total bits
for the array type, meaning that each of the 3 array components is 32 bits wide instead of
24.
Why is it 32 bits? Because the alignment for Info is 2 (on this machine). The record
alignment is a multiple of the largest alignment of the enclosed components. The alignment
for type Temperature (2), is larger than the alignment for type Identity (1), therefore the
alignment for the whole record type is 2. We need to go from that number of storage
elements to a number of bits for the size.
Here's where it gets subtle. The alignment is in terms of storage elements. Each storage
element is of a size in bits given by System.Storage_Unit. We've said that on our hypo-
thetical machine Storage_Unit is 8, so storage elements are 8 bits wide on this machine.
Bytes, in other words. Therefore, to get the required size in bits, we have to find a multiple
of the two 8-bit bytes (specified by the alignment) that has at least the number of bits we
gave in the Size clause. Two bytes only provides 16 bits, so that's not big enough, we need
at least 24 bits. The next multiple of 2 bytes is 4 bytes, providing 32 bits, which is indeed
larger than 24. Therefore, the overall size of the record type, consistent with the alignment,
is 4 bytes, or 32 bits. That's why the compiler says each array component is 32 bits wide.
But for our example let's say that we really want to use only 72 total bits for the array type
(and that we want three array components). That's the size we specified, after all. So how
do we get the record type to be 24 bits instead of 32? Yes, you guessed it, we change the
alignment for the record type. If we change it from 2 to 1, the size of 24 bits will work.
Adding this Alignment clause line will do that:
An alignment of 1 means that any address will work, assuming that addresses refer to
entire storage elements. (An alignment of 0 would mean that the address need not start
on a storage element boundary, but we know of no such machines.)
We can even entirely replace the Size clause with the Alignment clause, because the Size
clause specifying 24 bits is just confirming: it's the value that 'Size would return anyway.
The problem is the object size.
Now, you may be wondering why an alignment of 1 would work, given that the alignment
of the Temperature component is 2. Wouldn't it slow down the code, or even trap? Well,
maybe. It depends on the machine. If it doesn't work we would just have to use 32 bits for
the record type, with the original alignment of 2, for a larger total array size. Of course, if
the compiler recognizes that a representation cannot be supported it must reject the code,
but the compiler might not recognize the problem.
We said earlier that there are only a small number of reasons to specify 'Size for a type.
We can mention one of them now. Setting 'Size can be useful to give the minimum number
of bits to use for a component of a packed composite type, that is, within either a record
type or an array type that is explicitly packed via the aspect or pragma Pack. It says that
the compiler, when giving its best effort, shouldn't compress components of the type any
smaller than the number of bits specified. No, it isn't earth-shattering, but other uses are
more valuable, to be discussed soon.
One thing we will leave unaddressed (pun intended) is the question of bit ordering and
byte ordering within our record layouts. In other words, the "endian-ness". That's a sub-
ject beyond the scope of this course. Suffice it to say that GNAT provides a way to spec-
ify record layouts that are independent of the endian-ness of the machine, within some
implementation-oriented limits. That's obviously useful when the code might be com-
piled for a different ISA in the future. On the other hand, if your code is specifically
for a single ISA, e.g. Arm, even if different boards and hardware vendors are involved,
there's no need to be independent of the endian-ness. It will always be the same in that
case. (Those are "famous last words" though.) For an overview of the GNAT facility, an
attribute named attribute Scalar_Storage_Order see https://www.adacore.com/papers/
lady-ada-mediates-peace-treaty-in-endianness-war.
Although specifying record type layouts and alignments are perhaps the most common
representation characteristics expressed, there are a couple of other useful cases. Both
involve storage allocation.
One useful scenario concerns tasking. We can specify the number of storage elements
reserved for the execution of a task object, or all objects of a task type. You use the Stor-
age_Size aspect to do so:
task Servo is
pragma Storage_Size (1 * 1024);
end Servo;
The aspect seems textually cleaner and lighter unless you have task entries to declare as
well. In that case the line for the pragma wouldn't add all that much. That's a matter of
personal aesthetics anyway.
The specified number of storage elements includes the size of the task's stack (GNAT does
have one, per task). The language does not specify whether or not it includes other storage
associated with the task used for implementing and managing the task execution. With
GNAT, the extent of the primary stack size is the value returned, ignoring any other storage
used internally in the run-time library for managing the task.
The GNAT run-time library allocates a default stack amount to each task, with different
defaults depending on the underlying O.S., or lack thereof, and the target. You need to
read the documentation to find the actual amount, or, with GNAT, read the code.
You would need to specify this amount in order to either increase or decrease the allocated
storage. If the task won't run properly, perhaps crashing at strange and seemingly random
places, there's a decent chance it is running out of stack space. That might also be the
reason if you have a really deep series of subprogram calls that fails. The correction is to
increase the allocation, as shown above. How much? Depends on the application code. The
quick-and-dirty approach is to iteratively increase the allocation until the task runs properly.
Then, reverse the approach until it starts to fail again. Add a little back until it runs, and
leave it there. We'll mention a much better approach momentarily (GNATstack).
Even if the task doesn't seem to run out of task stack, you might want to reduce it anyway,
to the extent possible, because the total amount of storage on your target might be lim-
ited. Some of the GNAT bare-metal embedded targets have very small amounts of memory
available, so much so that the default task stack allocations would exhaust the memory
available quickly. That's what the example above does: empirical data showed that the
Servo task could run with just 1K bytes allocated, so we reduced it from the default ac-
cordingly. (We specified the size with that expression for the sake of readability, relative to
using literals directly.)
Notice we said "empirical data" above. How do we know that we exercised the task's thread
of control exhaustively, such that the arrived-at allocation value covers the worst case? We
don't, not with certainty. If we really must know the allocation will suffice for all cases,
say because this is a high-integrity application, we would use GNATstack. GNATstack is an
offline tool that exploits data generated by the compiler to compute worst-case stack re-
quirements per subprogram and per task. As a static analysis tool, its computation is based
on information known at compile time. It does not rely on empirical run-time information.
The other useful scenario for allocating storage concerns access types, specifically access
types whose values designate objects, as opposed to designating subprograms. (Remem-
ber, objects are either variables or constants.) There is no notion of dynamically allocating
procedures and functions in Ada so access-to-subprogram types are not relevant here. But
objects can be of protected types (or task types), and protected objects can "contain" en-
tries and protected subprograms, so there's a lot of expressive power available. You just
don't dynamically allocate procedures or functions as such.
First, a little background on access types, to supplement what we said earlier.
By default, the implementation chooses a standard storage pool for each named access-
to-object type. The storage allocated by an allocator (i.e., new) for such a type comes from
the associated pool.
Several access types can share the same pool. By default, the implementation might
choose to have a single global storage pool, used by all such access types. This global
pool might consist merely of calls to operating system routines (e.g., malloc), or it might
be a vendor-defined pool instead. Alternatively, the implementation might choose to create
a new pool for each access-to-object type, reclaiming the pool's memory when the access
type goes out of scope (if ever). Other schemes are possible.
Finally, users may define new pool types, and may override the choice of pool for an access-
to-object type by specifying Storage_Pool for the type. In this case, allocation (via new)
takes memory from the user-defined pool and deallocation puts it back into that pool, trans-
parently.
With that said, here's how to specify the storage to be used for an access-to-object type.
There are two ways to do it.
If you specify Storage_Pool for an access type, you indicate a specific pool object to be
used (user-defined or vendor-defined). The pool object determines how much storage is
gnat.html#debugging-control
generic
type Source(<>) is limited private;
type Target(<>) is limited private;
function Ada.Unchecked_Conversion (S : Source) return Target
with Pure, Nonblocking, Convention => Intrinsic;
The function, once instantiated and eventually invoked, returns the caller's value passed
to S (of type Source) as if it is a value of type Target. That value can then be used in any
way consistent with the Target type.
The two generic parameters, Source and Target, are defined in a manner that makes
them very permissive in terms of the types they will accept when instantiated. To under-
stand how, you need to understand a little bit of Ada's terminology and design for generic
unit parameters. (If you are already familiar with generic formal types and how they are
matched, feel free to skip this material.)
First, the terminology. The type parameters defined by a generic unit are known as "generic
formal types," or "generic formals" for short. Types Source and Target are the generic
formals in the unit above. When instantiating such a generic, clients must specify a type
for each generic formal type. The types specified by the client are known as "generic actual
types," or "generic actuals" for short. You can remember that by the fact that the actuals
are the types "actually" given to the generic unit to work with when instantiated. (You may
laugh, but that mnemonic works.)
Now we're ready to discuss the language design concept. The idea is that the syntax of a
generic formal type indicates what kind of generic actual is required for a legal instantiation.
This is known as the "Contract Model" because we can think of the formal parameters as
expressing a contract between the generic unit's implementation and the client code that
instantiates the generic. The contract is enforced by the compiler, in that it will reject any
instantiation that attempts to specify some actual type that does not match the formal's
requirements.
For example, if the generic computes some value for any floating point type, that floating-
point type would be declared as a generic formal type, and would be defined so that only
some floating-point type could be used for the corresponding actual type:
generic
type Real is digits <>;
The formal parameter syntax reflects the syntax of a floating-point type declaration, ex-
cept that the <> (the "box") indicates that the generic does not care how many digits are
available. The generic actual will be some floating point type and it will specify the number
of decimal digits.
If instead we try to match that formal with some actual that is anything other than a floating-
point type the compiler will reject the instantiation. Therefore, within the generic body,
the implementation code can be written with the assurance that the characteristics and
capabilities required of a floating point type will be available. That's the Contract Model
in full: the requirements are a matter of the generic unit's purpose and implementation,
so the formal parameters reflect those requirements and the compiler ensures they will be
met.
Some generic units, though, do not require specifically numeric actual types. These gener-
ics can use less specific syntax for their formal types, and as a result, more kinds of actual
types are permitted in the instantiations. Remember the Contract Model and this will make
sense. The contract between the generic and the clients is, in this case, more permissive:
it does not require a numeric type in order to implement whatever it does.
For illustration, suppose we want a generic procedure that will exchange two values of some
type. What operations does the generic unit require in the implementation in order to swap
two values? There are two: assignment, as you might expect, but also the ability to declare
objects of the type (the "temporary" used to hold one of the values during the swap steps).
As long as the body can do that, any type will suffice, so the generic formals are written
to be that permissive. What is the syntax that expresses that permissiveness, you ask? To
answer that, first consider simple, non-generic private types from the user's point of view.
For example:
package P is
type Foo is private;
procedure Do_Something (This : Foo);
private
type Foo is ... -- whatever
end P;
There are two "views" associated with the package: one for the "visible" part of the package
spec (declaration), known as the "partial" view, and one for the "private" part of the package
spec and the package body, known as the "full" view. The differences between the two
views are a function of compile-time visibility.
The partial view is what clients (i.e., users) of the package have: the ability to do things that
a type name provides, such as declarations of objects, as well as some basic operations such
as assignment, some functions for equality and inequality, some conversions, and whatever
subprograms work on the type (the procedure Do_Something above). Practically speaking,
that's about all that the partial view provides. That's quite a lot, in fact, and corresponds
to the classic definition of an "abstract data type."
The code within the package private part and package body has the full view. This code has
compile-time visibility to the full definition for type Foo, so there are additional capabilities
available to this code. For example, if the full definition for Foo is as an array type, indexing
will be available with the private part and body. If Foo is fully defined as some numeric
type, arithmetic operations will be possible within the package, and so on.
Therefore, the full view provides capabilities for type Foo that users of the type can-
not access via the partial view. Only the implementation for type Foo and procedure
Do_Something have the potential to access them.
Now, back to the generic formal parameter. If the generic unit doesn't care what the actual
type is, and just needs to be able do assignment and object declaration, a "generic formal
private type" expresses exactly that:
generic
type Item is private;
procedure Exchange( Left, Right : in out Item );
Inside generic procedure Exchange, the view of type Item is as if Item were some private
type declared in a package, with only the partial view available. But the operations provided
by a partial view are sufficient to implement the body of Exchange: only assignment and
object declaration are required. Any additional capabilities that the generic actual type may
have — array indexing, arithmetic operators, whatever — are immaterial because they are
not required. That's the Contract Model: only the specified view's required capabilities are
important. Anything else the type can also do is not relevant.
But consider limited types. Those types don't allow assignment, by definition. Therefore,
an instantiation that specified a limited actual type for the generic formal type Item above
would be rejected by the compiler. The contract specifies the ability to do assignment so a
limited type would violate the contract.
Finally, as mentioned, our Exchange generic needs to declare the "temporary" object
Old_Left. A partial view of a private type allows that. But not all types are sufficient,
by their name alone, to declare objects. Unconstrained array types, such as type String,
are a familiar example: they require the bounds to be specified when declaring objects;
the name String alone is insufficient. Therefore, such types would also violate the con-
tract and, therefore, would be rejected by the compiler when attempting to instate generic
procedure Exchange.
Suppose, however, that we have some other generic unit whose implementation does not
need to declare objects of the formal type. In that case, a generic actual type that did not
support object declaration (by the name alone) would be acceptable for an instantiation.
The generic formal syntax for expressing that contract uses these tokens: (<>) in addition
to the other syntax mentioned earlier:
generic
type Foo(<>) is private;
In the above, the generic formal type Foo expresses the fact that it can allow unconstrained
types — known as "indefinite types" — when instantiated because it will not attempt to use
that type name to declare objects. Of course, the compiler will also allow constrained
types (e.g., Integer, Boolean, etc.) in instantiations because it doesn't matter one way
or the other inside the generic implementation. The Contract Model says that additional
capabilities, declaring objects in this case, are allowed but not required. (There is a way
to declare objects of indefinite types, but not using the type name alone. The unchecked
facilities don't need to declare objects so we will not show how to do it.)
Now that you understand the Contract Model (perhaps more than you cared), we are ready
to examine the generic formal type parameters for Ada.Unchecked_Conversion. Here's
the declaration again:
generic
type Source(<>) is limited private;
type Target(<>) is limited private;
function Ada.Unchecked_Conversion (S : Source) return Target
with Pure, Nonblocking, Convention => Intrinsic;
The two generic formal types, Source, and Target, are the types used for the incoming
value and the returned value, respectively. Both formals are "indefinite, limited private
types" in the jargon, but now you know what that means. Inside the implementation of the
generic function, neither Source nor Target will be used to declare objects (the (<>) syn-
tax). Likewise, neither type will be used in an assignment statement (the "limited" reserved
word). And finally, no particular kind of type is required for Source or Target (the private
reserved word). That's a fairly restricted usage within the generic implementation, but as a
result the contract can be very permissive: the generic can be instantiated with almost any
type. It doesn't matter if the actual is limited or not, private or not, and indefinite or not.
The generic implementation doesn't need those capabilities to implement a conversion so
they are not part of the contract expressed by the generic formal types.
What sort of type would be disallowed? Abstract types, and incomplete types. However, it
is impossible to declare objects of those types, for good reasons, so unchecked conversion
is never needed for them.
Note that the result value is returned by-reference whenever possible, in which case it is
just a view of the Source bits in the formal parameter S and not a copy. For a Source type
that is not a by-copy type, the result of an unchecked conversion will typically be returned
by-reference (so that the result and the parameter S share the same storage); for a by-copy
Source type, a copy is made.
The compiler can restrict instantiations but implementers are advised by the language stan-
dard to avoid them unless they are required by the target environment. For example, an
instantiation for types for which unchecked conversion can't possibly make sense might be
disallowed.
Clients can apply language- and vendor-defined restrictions as well, via pragma Restric-
tions. In particular, the language defines the No_Dependence restriction, meaning that no
client's context clause can specify the unit specified. As a result no client can instantiate
the generic for unchecked conversion:
• S'Size = Target'Size
• S'Alignment is a multiple of Target'Alignment, or Target'Alignment is 0 (meaning
no alignment required whatsoever)
• Target is not an unconstrained composite type
• S and Target both have a contiguous representation
• The representation of S is a representation of an object of the target subtype
We will examine these requirements in turn, but realize that they are not a matter of legal-
ity. Compilers can allow instantiations that violate these requirements. Rather, they are
requirements for conversions to have the defined effect.
The first requirement is that the size (in bits) for the parameter S, of type Source, is the
same as the size of the Target type. That's reasonable if you consider it. What would it
mean to convert, for example, a 32-bit value to an 8-bit value? Which 8 bits should be
used?
As a result, one of the few reasons for setting the size of a type (as opposed to the size of
an object) is for the sake of well-defined unchecked conversions. We might make the size
larger than it would need to be because we want to convert a value of that type to what
would otherwise be a larger Target type.
Because converting between types that are not the same size is so open to interpretation,
most compilers will issue a warning when the sizes are not the same. Some will even reject
the instantiation. GNAT will issue a warning for these cases when the warnings are enabled,
but will allow the instantiation. We're supposed to know what we are doing, after all. The
warning is enabled via the specific -gnatwz switch or the more general -gnatwa switch.
GNAT tries to be permissive. For example, in the case of discrete types, a shorter source
is first zero or sign extended as necessary, and a shorter target is simply truncated on the
left. See the GNAT RM for the other details.
The next requirement concerns alignment. As we mentioned earlier, modern architectures
tend to have strict alignment requirements. We can meaningfully convert to a type with a
stricter alignment, or to a type with no alignment requirement, but converting in the other
direction would require a copy.
Next, recall that objects of unconstrained types, such as unconstrained array types or dis-
criminated record types, must have their constraints specified when the objects are de-
clared. We cannot just declare a String object, for example, we must also specify the
lower and upper bounds. Those bounds are stored in memory, logically as part of the
String object, since each object could have different bounds (that's the point, after all).
What, then, would it mean to convert some value of a type that has no bounds to a type
that requires bounds? The third requirement says that it is not meaningful to do so.
The next requirement is that the argument for S, and the conversion target type Target,
have a contiguous representation in memory. In other words, each storage unit must be
immediately adjacent, physically, to the next logical storage unit in the value. Such a
representation for any given type is not required by the language, although on typical
modern architectures it is common. (The type System.Storage_Elements.Storage_Array
is an exception, in that a contiguous representation is guaranteed.) An instance of Ada.
Unchecked_Conversion just takes the bits of S and treats them as if they are bits for a
value of type Target (more or less), and does not handle issues of segmentation.
The last requirement merely states that the bits of the argument S, when treated as a
value of type Target, must actually be a bit-pattern representing a value of type Target
(strictly, the subtype). For example, with signed integers, any bit pattern (of the right size)
represents a valid value for those types. In contrast, consider an enumeration type. By
default, the underlying representational values are the same as the position values, i.e.,
starting at zero and increasing by one. But users can override that representation: they
can start with any value and, although the values must increase, they need not increase
by one:
If we covert an unsigned integer (of the right size) to a Toggle_Switch value, what would
it mean if the Source value was neither 0 nor 4?
We've said that the instantiations are likely allowed, hence callable functions are created.
If the above requirements are not met, what happens?
What happens depends on the Target type, that is, the result type for the conversion.
Specifically, it depends on whether the target type is a "scalar" type. As we mentioned
earlier, a scalar type is either a "discrete" type or a "real" type, which are themselves
further defined, as the figure below indicates. Any other type is a non-scalar type, e.g.,
record types, access types, task types, and so on.
When the requirements for meaningful instantiations are not respected and the Target
type is a scalar type, the result returned from the call is implementation defined and is
potentially an invalid representation. For example, type Toggle_Switch is an enumeration
type, hence it is a scalar type. Therefore, if we covert an unsigned integer (of the right size)
to a Toggle_Switch value, and the Source value is neither 0 nor 4, the resulting value is an
invalid representation. That's the same as an object of type Toggle_Switch that is never
assigned a value. The random junk in the bits may or may not be a valid Toggle_Switch
value. That's not a good situation, clearly, but it is well-defined: if it is detected, either
Constraint_Error or Program_Error is raised. If the situation is not detected, execution
continues using the invalid representation. In that case it may or may not be detected,
near the call or later. For example:
with Ada.Unchecked_Conversion;
with Ada.Text_IO; use Ada.Text_IO;
with Interfaces; use Interfaces;
procedure Demo is
In the execution of the code above, the invalid representation value in T1 is not detected,
except that it is copied into T2, where it is eventually detected when 'Image is applied to T2.
The invalid representation is not detected in the assignment statement or the comparison
because we want the optimizer to be able to avoid emitting a check prior to every use of
the value. Otherwise the generated code would be too slow. (The language explicitly allows
this optimization.)
The evaluation of an object having an invalid representation value due to unchecked con-
version is a so-called "bounded error" because the results at run-time are predictable and
limited to one of those three possibilities: the two possible exceptions, or continued exe-
cution.
Continued execution might even work as hoped, but such code is not portable and should
be avoided. A new vendor's compiler, or even a new version of a given vendor's compiler,
might detect the situation and raise an exception. That happens, and it ends up costing
developer time to make the required application code changes.
The possibilities get much worse when the result type is not a scalar type. In this case,
the effect of the call — not the value returned by the call — is implementation defined.
As a result, the possible run-time behavior is unpredictable and, consequently, from the
language rules point of view anything is possible. Such execution is said to be "erroneous."
Why the difference based on scalar versus non-scalar types? Scalar types have a simple
representation: their bits directly represent their values. Non-scalar types don't always
have a simple representation that can be verified by examining their bits.
For example, we can have record types with discriminants that control the size of the corre-
sponding objects because the record type contains an array component that uses the dis-
criminant to set the upper bound. These record types might have multiple discriminants,
and multiple dependent components. As a result, an implementation could have hidden,
internal record components. These internal components might be used to store the start-
ing address of the dependent components, for example, or might use pointers to provide
a level of indirection. If an unchecked conversion did not provide correct values for these
internal components, the effect of referencing the record object would be unpredictable.
Even a comparatively simple record type with one such dependent component is sufficient
to illustrate the problem. There are no internal, hidden components involved:
with Ada.Unchecked_Conversion;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System; -- for Storage_Unit
with System.Storage_Elements; use System.Storage_Elements;
Buffer : OneK_Bounded_Buffer;
Sequence : Storage_Array (1 .. Buffer'Size / Storage_Unit);
begin
Buffer := As_OneK_Bounded_Buffer (Sequence);
Put_Line ("Buffer capacity is" & Buffer.Capacity'Image);
Show_Capacity (Buffer);
Put_Line ("Done");
end Demo_Erroneous;
In the above, the type Bounded_Buffer has an array component Content that depends
on the discriminant Capacity for the number of array components. This is an extremely
common idiom. However, unchecked conversion is only meaningful, as defined earlier,
when converting to constrained target types. Bounded_Buffer is not constrained, so we
define a constrained subtype (OneK_Bounded_Buffer) for the sake of the conversion.
The specific Buffer object is 8320 bits (1024 * 8, plus 2 * 64), as is the Sequence object,
so the sizes are the same.
The alignment of OneK_Bounded_Buffer is 8, and Storage_Array's alignment is 1, so the
Target type is a multiple of the Source type, as required.
Both types have a contiguous representation, and the sequence of bytes can be a valid
representation for the record type, although it certainly might not be valid. For example,
if we change the discriminant from what the subtype specifies, we would have an invalid
representation for that subtype.
So we can reasonably invoke an unchecked conversion between the array of bytes and
the record type. However, as you can see in the code and as the compiler warns, we
never assigned a value to the Sequence array object. The unchecked conversion from that
Sequence of bytes includes the discriminant value, so it is very possible that we will get a
discriminant value that is not 1K.
We can test that possibility by running the program. In the first call to Put_Line, the pro-
gram prints the Capacity discriminant for the Buffer object. The compiler knew it was
1024, so it doesn't get the discriminant component from memory, it just directly prints
1024. However, we can force the compiler to query the discriminant in memory. We can
pass Buffer to procedure Show_Capacity, which takes any Bounded_Buffer, and there
query (print) the Capacity component under that different view. That works because the
view inside the procedure Show_Capacity is as of Bounded_Buffer, in which the discrimi-
nant value is unknown at compile-time.
In the above examples, we are responsible for ensuring that the enumeration representation
encoding and the record discriminant value are correct when converted from some other
type. That's not too hard to recognize because we can literally see in the source code that
there is something to be maintained by the conversions. However, there might be hidden
implementation artifacts that we cannot see in the source code but that must be maintained
nevertheless.
For example, the compiler's implementation for some record type might use dynamic mem-
ory allocations instead of directly representing some components. That would not appear
in the source code. As a simpler example of invisible implementation issues, consider again
our earlier record type:
As we discussed earlier, between the bytes that are allocated to the record components
are some other bytes that are not used at all. As usual, the compiler must implement the
language-defined equality operator for the record type. One way to implement that function
would be to generate code that checks the equality for each component individually, ignor-
ing any unused bytes. But suppose you have a large record type with many components.
The code for checking record level equality will be extensive and inefficient. An alternative
implementation for the compiler would be to use a "block compare" machine instruction
to check the equality of the entire record at once, rather than component-by-component.
That will be considerably more efficient because the block-compare instruction just com-
pares the bits from one starting address to another ending address. But in that case the
"unused" bytes are not skipped so the values within those bytes become significant. Com-
parison of those unused bytes will only work if their values are defined and assigned in each
record object. Compilers that may use a block-comparison approach will, therefore, always
set those unused bytes to a known value (typically zero). That is part of the valid repre-
sentation for values of the type, and consequently must be maintained by our unchecked
conversions. This being a non-scalar target type, failure to do so results in erroneous exe-
cution, i.e., undefined behavior. "There be dragons" as ancient maps of the unknown world
once said.
As you can see, you should use unchecked conversions with considerable care and thought.
Moreover, because unchecked programming is such a low-level activity, and has vendor-
defined implementation issues, it is not only less portable than high-level coding, it is also
less portable than other low-level programming. You will be well served if you limit the
use of unchecked conversions overall. If your application code is performing unchecked
conversions all over the code, something is very likely wrong, or at least very questionable.
A well-designed Ada program should not need ubiquitous unchecked conversions.
That said, of course sometimes unchecked conversions are reasonable. But even then, it is
better to isolate and hide their use via compile-time visibility controls. For example, instead
of having clients invoke unchecked conversion instances many times, have a procedure
that is invoked many times, and let the procedure body do the conversion. That way, the
clients see a high-level specification of functionality, and, if the conversion needs to be
changed later, there is only that one conversion usage (the procedure body) to change.
This approach is really just another example of isolating and hiding code that might need
to change in the future.
Our earlier demo program assigned an incorrect value via unchecked conversion into an
object of an enumeration type that had non-standard representation values. The value
assigned was not one of those representation values so the object had an invalid represen-
tation. Certain uses of an invalid representation value will be erroneous, and we saw that
the effect of erroneous execution was unpredictable and unbounded.
That example was somewhat artificial, for the sake of illustration. But we might get an
invalid value in a real-world application. For example, we could get an invalid value from
a sensor. Hardware sensors are frequently unreliable and noisy. We might get an invalid
value from a call to an imported function implemented in some other language. Whenever
an assignment is aborted, the target of the assignment might not be fully assigned, leading
to so-called "abnormal" values. Other causes are also possible. The problem is not unusual
in low-level programming.
How do we avoid the resulting bounded errors and erroneous execution?
In addition to assignment statements, we can safely apply the Valid attribute to the ob-
ject. This language-defined attribute returns a Boolean value indicating whether or not the
object's value is a valid representation for the object's subtype. (More details in a moment.)
There is no portable alternative to check an object's validity. Here's an example:
with Ada.Unchecked_Conversion;
with Ada.Text_IO; use Ada.Text_IO;
with Interfaces; use Interfaces;
with System;
procedure Demo_Validity_Check is
T1 : Toggle_Switch;
Result : Toggle_Switch;
Sensor : Unsigned_8;
-- for Sensor'Address use System'To_Address (...);
begin
Result := As_Toggle_Switch (Sensor);
return (if Result'Valid then Result else Default);
end Sensor_Reading;
begin
T1 := Sensor_Reading (Default => Off); -- arbitrary
Put_Line (T1'Image);
end Demo_Validity_Check;
In the above, Sensor_Reading is the high-level, functional API provided to clients. The
function hides the use of the unchecked conversion, and also hides the memory-mapped
hardware interface named Sensor. We've commented out the address clause since we
don't really have a memory mapped device available. You can experiment with this program
by changing the code to assign a value to Sensor (e.g., when it is declared). It is an unsigned
8-bit quantity so any value in the corresponding range would be allowed.
In addition to checking for a valid representation, thus preventing the bounded error, Valid
also checks that the object is not abnormal, so erroneous execution can be prevented too.
(It also checks that any subtype predicate defined for the Target type is also satisfied, but
that's a lesson for another day.)
However, the Valid attribute can be applied only to scalar objects. There is no language-
defined attribute for checking objects of composite types. That's because it would be very
hard to implement for some types, if not impossible. For example, given a typical run-time
model, it is impossible to check the validity of an access value component. Therefore, you
must individually check the validity of scalar record or array components.
At least, you would have to check them individually in standard Ada. GNAT defines another
Boolean attribute, named Valid_Scalars, to check them all for us. This attribute returns
True if the evaluation of Valid returns True for every scalar subcomponent of the enclosing
composite type. It also returns True when there are no scalar subcomponents. See the
GNAT RM for more information.
THIRTYSIX
MULTI-LANGUAGE DEVELOPMENT
Software projects often involve more than one programming language. Typically that's
because there is existing code that already does something we need done and, for that
specific code, it doesn't make economic sense to redevelop it in some other language.
Consider the rotor blade model in a high-fidelity helicopter simulation. Nobody touches
the code for that model except for a few specialists, because the code is extraordinarily
complex. (This complexity is unavoidable because a rotor blade's dynamic behavior is so
complex. You can't even model it as one physical piece because the tip is traveling so
much faster than the other end.) Complex and expensive models like that are a simulator
company's crown jewels; their cost is meant to be amortized over as many projects as
possible. Nobody would imagine redeveloping it simply because a new project is to be
written in a different language.
Therefore, Ada includes extensive facilities to "import" foreign entities into Ada code, and to
"export" Ada entities to code in foreign languages. The facilities are so useful that Ada has
been used purely as "glue code" to allow code written in two other programming languages
to be used together.
You've already seen an introduction to Ada and C code working together in the "Interfacing"
section of the Ada introductory course (page 175). If you have not seen that material, be
sure to see it first. We will cover some further details not already discussed there, and then
go into the details of the facilities not covered elsewhere, but we assume you're familiar
with it.
The Ada foreign language interfacing facilities include both "general" and "language-
specific" capabilities. The "general" facilities are known as such because they are not tied
to any specific language. These pragmas and aspects work with any of the supported for-
eign languages. In contrast, the "language-specific" interfacing facilities are collections of
Ada declarations that provide Ada analogues for specific foreign language types and sub-
programs. For example, as you saw in that "Interfacing" section, there is a package with a
number of declarations for C types, such as int, float, and double, as well as C "strings",
with subprograms to convert back and forth between them and Ada's string type. Other
languages are also supported, both by the Ada Standard and by vendor additions. You will
frequently use both the "general" and the "language-specific" facilities together.
All these interfacing capabilities are defined in Annex B of the language standard. Note that
Annex B is not a "Specialized Needs" annex, unlike some of the other annexes. The Special-
ized Needs annexes are wholly optional, whereas all Ada implementations must implement
Annex B. However, some parts of Annex B are optional, so more precisely we should say
that every implementation must support all the required features of Annex B. That comes
down mainly to the package Interfaces (more on that package in a moment). However, if
an implementation does implement any optional part of Annex B, it must be implemented
as described by the standard, or with less functionality. An implementation cannot use the
same name for some facility (aspect, etc.) but with different semantics. That's true of the
Specialized Needs annexes too: not every part need be implemented, but any part that is
implemented must conform to the standard. In practice, for Annex B, all implementations
provide the required parts, but not all provide support for all the "language-specific" foreign
1153
Learning Ada
languages' interfaces. The vendors make a business decision for the optional parts, just as
they do regarding the Specialized Needs annexes.
In the "Interfacing" section of the Ada introductory course you saw that Ada defines aspects
and pragmas for working with foreign languages. These aspects and pragmas are function-
ally interchangeable, and we will use whichever one of the two that is most convenient
in our discussion. The pragmas are officially "obsolescent," but that merely means that a
newer approach is available, in this case the corresponding aspects. You can use either one
without concern for future support because language constructs that are obsolescent are
not removed from the language. Any compiler that supports such constructs will almost
certainly support them forever, for the sake of not invalidating existing customers' code.
The pragmas have been in the language since Ada 95 so there's a lot of existing code using
them. Changing the compiler isn't cost-free, after all, so why spend the money to poten-
tially lose a customer? Likewise, a brand new compiler will also probably support them, for
the sake of potentially gaining a customer.
The general interfacing facility consists of these aspects and pragmas, specifically Import,
Export, and Convention. As you saw in the Ada Introduction course, Import brings a
foreign entity into Ada code, Export does the opposite, and Convention supplies additional
information and directives to the compiler. We will go into the details of each.
Regardless of whether the Ada code is importing or exporting some entity, there will be
an Ada declaration for that entity. That declaration tells the compiler how the entity can
be used, as usual. The interfacing aspects and pragmas are then applied to these Ada
declarations.
If we are exporting, then the entity is implemented in Ada. For a subprogram that means
there will also be a subprogram body matching the declaration, and the compiler will enforce
that requirement as usual. In contrast, if we are importing a subprogram, then it is not
implemented in Ada, and therefore there will be no corresponding subprogram body for the
Ada declaration. The compiler would not allow it if we tried. In that case the Import is the
subprogram's completion.
Subprograms often have a separate declaration. Sometimes that's required, for example
when we want to include a subprogram as part of a package's API, but at other times it is op-
tional. Remember that a subprogram body acts as a corresponding declaration when there
is no separate declaration defined. Thus, either way, we have a subprogram declaration
available for the interfacing aspects and/or pragmas.
For data that are imported or exported, we'll have the declaration of the object in Ada to
which we can apply the necessary interfacing aspects/pragmas. But we will also have the
types for these objects, and as you will see, the types can be part of interfacing too.
As you saw in the "Interfacing" section of the Ada introductory course (page 175), when
importing and exporting you'll also specify the "convention" for the entity in question. The
pragmas for importing and exporting include a parameter for this purpose. When using the
aspects, you'll specify the Convention aspect too.
For types, though, you will specify the Convention aspect/pragma alone, without Import or
Export. In this case the convention specifies the layout for objects of that type, presumably
a layout different than the Ada compiler would normally use. You would need to specify this
other layout either because you're going to later declare and export an object of the type,
or because you are going to declare an object of the type and pass it as a argument to an
imported subprogram.
For example, Ada specifies that multi-dimensional arrays are represented in memory in
row-major order. In contrast, the Fortran standard specifies column-major order. If we want
to define a type in Ada that can be used for passing parameters to Fortran routines, we
need to specify that convention for the type. For example:
In the example above, Ada is known as a convention identifier, as is Fortran in the earlier
example. Convention identifiers are defined by the Ada language standard, but also by Ada
vendors.
The Ada standard defines two convention identifiers: Ada (the default), and Intrinsic. In
addition, Annex B defines convention identifiers C, COBOL, and Fortran. Support for these
Annex B conventions is optional.
GNAT supports the standard and Annex B conventions, as well as the following: Assembler,
"C_PLUS_PLUS" (or CPP), Stdcall, WIN32, and a few others. C_PLUS_PLUS is the convention
identifier required by the standard when C++ is supported. (Convention identifiers are
actual identifiers, not strings, so they must obey the syntax rules for identifiers. "C++"
would not be a valid identifier.) See the GNAT User Guide for those other GNAT-specific
conventions.
Stdcall and WIN32 actually do specify a particular calling convention, but for those con-
vention identifiers that are language names, how do we get from the name to a calling
convention?
The ultimate requirement for any calling convention is compatibility with the Ada compiler
we are using. Specifically, the Ada compiler must recognize what the calling convention
specifies, and support importing and exporting subprograms with that convention applied.
For the Ada convention that's simple. There is no standard calling convention for Ada. Con-
vention Ada simply means the calling convention applied by the Ada compiler we happen
to be using. (We'll talk about Intrinsic shortly.)
So far, so good. But how to we get from those other language names to corresponding
calling conventions? There is no standard calling convention for, say, C, any more than
there is a standard calling convention for Ada.
In fact we don't get to the calling convention, at least not directly. What the language name
in the convention identifier actually tells us is that, when that convention is supported,
there is a compiler for that foreign language that uses a calling convention known to, and
supported by, the Ada compiler we are using. The Ada compiler vendor defines which
languages it supports, after all. For example, when supported, convention C means that
there is a compatible C compiler known to the Ada compiler vendor. For GNAT you can
guess which C compiler that might be.
It's actually pretty straightforward once you have the big picture. If the convention is sup-
ported, the Ada compiler in use knows of a compiler for that language with which it can
work. Annex B just defines some convention identifiers for the sake of portability.
But suppose a given Ada compiler supports more than one vendor for a given program-
ming language? In that case the Ada compiler would define and support multiple conven-
tion identifiers for the same programming language. Presumably these identifiers would
be differentiated by the compiler vendors' names. Thus we might have available conven-
tions GNU_Fortran and Intel_Fortran if both were supported. The Fortran convention
identifier would then indicate the default vendor's compiler.
The Intrinsic calling convention represents subprograms that are "built in" to the com-
piler. When such a subprogram is called the compiler doesn't actually generate the code
for an out-of-line call. Instead, the compiler emits the assembly code — often just a single
instruction — corresponding to the intrinsic subprogram's name. There will be a separate
declaration for the subprogram, but no actual subprogram body containing a sequence of
statements. The compiler just knows what to emit in place of the call.
For example:
function Shift_Left
(Value : Unsigned_16;
Amount : Natural)
return Unsigned_16
with ..., Convention => Intrinsic;
The effect is much like a subprogram call that is always in-lined, except that there's no
body for the subprogram. In this example the compiler simply issues a shift-left instruction
in assembly language.
You'll see the Intrinsic convention applied to many language-defined subprograms. For
example:
generic
type Source(<>) is limited private;
type Target(<>) is limited private;
function Ada.Unchecked_Conversion(S : Source) return Target
with ..., Convention => Intrinsic;
function Sync_Val_Compare_And_Swap_Bool_8
(Destination : access Unsigned_8;
Comparand : Unsigned_8;
New_Value : Unsigned_8)
return Boolean
with Convention => Intrinsic,
...
We would specify additional aspects beyond that of Convention but these have not yet
been discussed. That's what the ellipses indicate in the various examples above.
You've already seen these aspects in the Ada Introduction course, but for completeness:
Import brings a foreign entity into Ada code, and Export makes an Ada entity available
to foreign code. In practice, these entities consist of objects and subprograms, but the
language doesn't impose many restrictions. It is up to the vendor to decide what makes
sense for their specific target.
The aspects Import and Export are so-called Boolean aspects because their value is either
True or False. For example:
For any Boolean-valued aspect the default is True so you only need to give the value ex-
plicitly if that value is False. There would be no point in doing that in these two cases, of
course. Hence we just give the aspect name:
Recall that objects of some types are initialized automatically during the objects' elabora-
tion, unless they are explicitly initialized as part of their declarations. Access types are like
that, for example. Objects of these types are default initialized to null as part of ensuring
that their values are always meaningful (absent unchecked conversion).
Obj : Reference;
In the above the value of Obj is null, just as if we had explicitly set it that way.
But that initialization is a problem if we are importing an object of an access type. Presum-
ably the value is set by the foreign code, so automatic initialization to null would overwrite
the incoming value. Therefore, the language guarantees that implicit initialization won't be
applied to imported objects.
Now the value of Obj is whatever the foreign code sets it to, and is not, in other words,
overwritten during elaboration of the declaration.
For an entity with a True Import or Export aspect, we can also specify a so-called external
name or link name. These names are specified via aspects External_Name and Link_Name
respectively.
An external name is a string value indicating the name for some entity as known by foreign
language code. For an entity that Ada code imports, this is the name that the foreign code
declares it to be. For an entity that Ada code exports, this is the name that the foreign code
is told to use. This string value is exactly the name to be used, so if you misspell the name
the link will fail. For example:
function Sync_Val_Compare_And_Swap_Bool_8
(Destination : access Unsigned_8;
Comparand : Unsigned_8;
New_Value : Unsigned_8)
return Boolean
with
Import,
Convention => Intrinsic,
External_Name => "__sync_bool_compare_and_swap_1";
The External_Name and Link_Name values are strings because the foreign unit names don't
necessary follow the Ada rules for identifiers (the leading underscores in this case). Note
that the ending digit in the name above is different from the declared Ada name.
Usually, the name of the imported or exported entity is precisely known and hence exactly
specified by External_Name. Sometimes, however, a compilation system may have a linker
"preprocessor" that augments the name actually used by the linkage step. For example,
an implementation might always prepend "_" and then pass the result to the system linker.
In that case we don't want to specify the exact name. Instead, we want to provide the
"starting point" for the name modification. That's the purpose of the aspect Link_Name.
If you don't specify either External_Name or Link_Name the compilation system will choose
one in some implementation-defined manner. Typically this would be the entity's defining
name in the Ada declaration, or some simple transformation thereof. But usually we know
the name exactly and so we use External_Name to give it.
As you can see, it really wouldn't make sense to specify both External_Name and Link_Name
since the semantics of the two conflict. But if both are specified for some reason, the
External_Name value is ignored.
Note that Link_Name cannot be specified for Intrinsic subprograms because there is no
actual unit being linked into the executable, because intrinsics are built-in. In this case you
must specify the External_Name.
Finally, because you will see a lot the pragma usage we should go into enough detail so
that you know what you're looking at when you see them.
Pragma Import and pragma Export work almost like a subprogram call. Parameters cannot
be omitted unless named notation is used. Reordering the parameters is not permitted,
however, unlike subprogram calls.
The BNF syntax is as follows. We show Import, but Export has identical parameters:
pragma Import(
[Convention =>] convention_identifier,
[Entity =>] local_name
[, [External_Name =>] external_name_string_expression]
[, [Link_Name =>] link_name_string_expression]);
As you can see, the parameters correspond to the individual aspects Convention, Exter-
nal_Name, and Link_Name. When using aspects you don't need to say which Ada entity
you're applying the aspects to, because the aspects are part of the entity declaration syn-
tax. In contrast, the pragma is distinct from the declaration so we must specify what's being
imported or exported via the Entity parameter. That's the declared Ada name, in other
words. Note that both the External_Name and Link_Name parameters are optional.
Here's that same built-in function, using the pragma to import it:
function Sync_Val_Compare_And_Swap_Bool_8
(Destination : access Unsigned_8;
Comparand : Unsigned_8;
New_Value : Unsigned_8)
return Boolean;
The first pragma parameter is for the convention. The next parameter, the Entity, is the
Ada unit's declared name. The last parameter is the external name. The compiler either
knows what we are referencing by that external name or it will reject the pragma. As we
mentioned before, the string value for the name is not required to match the Ada unit name.
You will see later that there are other convention identifiers as well, but we will wait for the
Specific Interfacing section (page 1161) to introduce those.
Package Interfaces must be provided by all Ada implementations. The package is in-
tended to provide types that reflect the actual numeric types provided by the target hard-
ware. Of course, the standard has no way to know what hardware is involved, therefore
the actual content is implementation-defined. But even so, it is possible to standardize the
names for these types, and that is what the language standard does.
Specifically, the standard defines the format for the names for the hardware's signed and
modular (unsigned) integer types, and for the floating-point types.
The signed integers have names of the form Integer_n, where n is the number of bits used
by the machine-supported type. The type for an eight-bit signed integer would be named
Integer_8, for example, and then Integer_16 and so on for the larger types, for as many
as the target machine supports.
Likewise, for the unsigned integers, the names are of the form Unsigned_n, with the same
meaning for n. The colloquial eight-bit "byte" would be named Unsigned_8, with Un-
signed_16 for the 16-bit version, and so on, again for as many as the machine supports.
For floating-point types it is harder to talk about a format that is sufficiently common to
standardize. The IEEE floating-point standard is well known and widely used, however, so
if the machine does support the IEEE format that name can be used. Such types would be
named IEEE_Float_n, again with the same meaning for n. Thus we might see declarations
for types IEEE_Float_32 and IEEE_Float_64 and so on, for all the machine supported
floating-point types.
In addition to these type declarations, for the unsigned integers only, there will be declara-
tions for shift and rotate operations provided as intrinsic functions.
The resulting package declaration might look something like this:
package Interfaces is
...
end Interfaces;
As you can see, when you need to write code in terms of the hardware's numeric types,
this package is a great resource. There's no need to declare your own UInt32 type, for
example, although of course you could, trivially:
But if you do, realize that you won't get the shift and rotate operations for your type. Those
are only defined for the types in package Interfaces. If you do need to declare such a
type, and you do want the additional shift/rotate operations, use inheritance:
The approach using inheritance is preferable because it is portable, all other things being
equal.
One reason to make up your own unsigned type is that you need one that does not in fact
reflect the target hardware's numeric types. For example, a hardware device register might
have gaps of bits that are currently not used by the device. Those gaps are frequently not
the size of a type declared in package Interfaces. We might need an Unsigned_3 type,
for example. That's a reasonable thing to do.
In addition to the aspects and pragmas for importing and exporting entities that work with
any language, Ada also defines standard language-specific facilities for interfacing with a
set of foreign languages. The standard defines which languages, but vendors can (and do)
expand the set.
Specifically, the "language-specific" interfacing facilities are collections of Ada declarations
that provide Ada analogues for specific foreign language types and subprograms. Package
Interfaces is the root package for a hierarchy of packages that organize these declarations
by language, with one or more child packages per language.
Note that the declarations within package Interfaces are, by definition, compile-time vis-
ible to any child package in the subsystem. Thus whenever one of the language-specific
packages needs to mention the machine types they are automatically available.
The standard defines specific support for foreign languages C, COBOL, and Fortran. Thus
there are one or more child packages rooted at Interfaces that have those language
names as their child package names: Interfaces.C, Interfaces.COBOL, and Interfaces.
Fortran.
The material below will focus on C and, to a lesser extent, Fortran, ignoring altogether the
support for COBOL. That's not because COBOL is unimportant. There is a lot of COBOL
business software out there in use. Rather, we skip COBOL because it is not relevant to
embedded systems. Similarly, although Fortran is extensively used, especially in high-
performance computing, it is not used extensively in embedded systems. We will provide
some information about the Fortran support but will not dwell on it.
Even though we do not consider C to be appropriate for large development projects, neither
technically not economically, it has its place in small, low-criticality embedded systems.
Ada developers can profit from existing device drivers and mature libraries coded in C, for
example. Hence interfacing to it is important.
What about C++? Interfacing to C++ is tricky compared to C, because of the vendor-
defined name-mangling, automatic invocations of constructors and destructors, exceptions,
and so on. Generally, interfacing with C++ code can be facilitated by preventing much of
those difficulties using the extern "C" {... } linkage-specification. Doing so then makes
the bracketed C++ code look like C, so the C interfacing facilities then can be used.
The child package Interfaces.C supports interfacing with units written in the C program-
ming language. Support is in the form of Ada constants and types, and some subprograms.
The constants correspond to C's limits.h header file, and the Ada types correspond to
types for C's int, short, unsigned_short, unsigned_long, unsigned_char, size_t, and
so on. There is also support for converting Ada's type String to/from char_array, and
similarly for type Wide_String, etc.
It's a large package so we will elide parts. The idea is to give you a feel for what's there. If
you want the details, see either the Ada reference manual or bring up the source code in
GNAT Studio.
package Interfaces.C is
CHAR_BIT : constant := 8;
SCHAR_MIN : constant := -128;
SCHAR_MAX : constant := 127;
UCHAR_MAX : constant := 255;
-- Signed and Unsigned Integers. Note that in GNAT, we have ensured that
-- the standard predefined Ada types correspond to the standard C types
...
-- Floating-Point
----------------------------
-- Characters and Strings --
----------------------------
...
end Interfaces.C;
The primary purpose of these types is for use in the formal parameters of Ada subprograms
imported from C or exported to C. The various conversion functions can be called from
within Ada to manipulate the actual parameters.
When writing the Ada subprogram declaration corresponding to a C function, an Ada pro-
cedure directly corresponds to a void function. An Ada procedure also corresponds to a C
function if the return value is always to be ignored. Otherwise, the Ada declaration should
be a function.
As we said, the types declared in this package can be used as the formal parameter types.
That is the intended and recommended approach. However, some Ada types naturally
correspond to C types, and you might see them used instead of those from Interfaces.C.
Type int is the C native integer type for the target, for example, as is type Integer in Ada.
Likewise, C's type float and type Ada's Float are likely compatible. GNAT goes to some
lengths to maintain compatibility with C, since the two gcc compilers share so much internal
technology. Other vendors might not do so. Best practice is use the types in Interfaces.C
for your parameters.
Of course, the types in Interfaces.C are not sufficient for all uses. You will often need to
use user-defined types for the formal parameters, such as enumeration types and record
types.
Ada enumeration types are compatible with C's enums but note that C requires enum values
to be the size of an int, whereas Ada does not. The Ada compiler uses whatever sized
machine type will support the specified number of enumeral values. It might therefore be
smaller than an int but it might also be larger. (Declaring more enumeration values than
would fit in an integer is unlikely except in tool-generated code, but it is possible.) For
example:
If we printed the object size for Small_Enum we'd get 8 (on a typical machine with GNAT).
Therefore, applying the aspect Convention to the Ada enumeration type declaration is a
good idea:
procedure Demo is
begin
Put_Line (Address'Object_Size'Image);
Put_Line (Name'Object_Size'Image);
end Demo;
Some Ada types simply have no corresponding type in C, such as record extensions, task
types, and protected types. You'll have to pass those as an "opaque" type, usually as an
address. It isn't clear that a C function would know what to do with values of these types, but
the general notion of passing an opaque type as an address is useful and not uncommon.
Of course, that approach forgoes all type safety, so avoid it when possible.
In addition to the types for the formal parameters, you'll also need to know how parameters
are passed to and from C functions. That affects the parameter profiles on both sides, Ada
and C. The text in Annex B for Interfaces.C specifies how parameters are to be passed
back and forth between Ada and C so that your subprogram declarations can be portable.
That's the approach for each supported programming language, i.e., in the discussion of
the corresponding child package under Interfaces.
The rules are expressed in terms of scalar types, "elementary" types, array types, and
record types. Remember that scalar types are composed of the discrete types and the
real types, so we're talking about the signed and modular integers, enumerations, floating-
point, and the two kinds of fixed-point types. The "elementary" types consist of the scalars
and access types. The rules are fairly intuitive, but throw in Ada's access parameters and
parameter modes and some subtleties arise. We won't cover all the various rules but will
explore some of the subtleties.
First, the easy cases: mode in scalar parameters, such as int, as simply passed by copy.
Scalar parameters are passed by copy anyway in Ada so the mechanism aligns with C in
a straightforward manner. A record type T is passed by reference, so on the C side we'd
see t* where t is a C struct corresponding to T. A constrained array type in Ada with a
component type T would correspond to a C formal parameter t* where t corresponds to T.
An Ada access parameter access T corresponds on the C side to t* where t corresponds
to T. And finally, a private type is passed according to the full definition of the type; the fact
that it is private is just a matter of controlling the client view, being private doesn't affect
how it is passed. There are other simple cases, such as access-to-subprogram types, but
we can leave that to the Annex.
Now to the more complicated cases. First, some C ABIs (application binary interfaces) pass
small structs by copy instead of by reference. That can make sense, in particular when the
struct is small, say the size of an address or smaller. In that case there's no performance
benefit to be had by passing a reference. When that situation applies, there is another
convention we have not yet mentioned: C_Pass_By_Copy. As a result the record parameter
will be passed by copy instead of the default, by reference (i.e., T rather than *T), as long
as the mode is in. For example:
type R2 is record
V : int;
end record
with Convention => C_Pass_By_Copy;
struct R2 {
int V;
};
On the C side we expect that p is passed by copy and indeed that is how we find it. That
said, passing record values to structs by reference is the more common programmer choice.
Like arrays, records are typically larger than an address. The point here is that the Ada code
can be configured easily to match the C code.
Next, consider passing array values, both to and from C. When passing an array value to C,
remember that Ada array types have bounds. Those bounds are either specified at compile
time when they are declared, or, for unconstrained array types, specified elsewhere, at
run-time.
Array types are not first-class types in C, and C has no notion of unconstrained array types,
or even of upper bounds. Therefore, passing an unconstrained array type value is interest-
ing. One approach is to avoid them. Instead, declare a sufficiently large constrained array
as a subtype of the unconstrained array type, and then just pass the actual upper bound
you want, along with the array object itself.
With that, we can just pass the value by reference as usual on the C side:
But that's assuming we know how many array components are sufficient from the C code's
point of view. In the example above we'll pass a value up to 100 to the Size parameter and
hope that is sufficient.
Really, it would work to use the unconstrained array type as the formal parameter type
instead:
The C function parameter profile wouldn't change. But why does this work? With values
of unconstrained array types, the bounds are stored with the value. Typically they are
stored just ahead of the first component, but it is implementation-defined. So why doesn't
the above accidentally pass the bounds instead of the first array component itself? It works
because we are guaranteed by the Ada language that passing an array will pass (the address
of) the components, not the bounds, even for Ada unconstrained array types.
Now for the other direction: passing an array from C to Ada. Here the lack of bounds
information on the C side really makes a difference. We can't just pass the array by itself
because that would not include the bounds, unlike an Ada call to an Ada routine. In this case
the approach is the similar to the first alternative described above, in which we declare a
very large array and then pass the bounds explicitly:
int x [100];
The fundamental idea is to declare an Ada type big enough to handle anything conceivably
needed on the C side. Subtype Natural means 0 .. Integer'Last so List is quite large
indeed. Just be sure never to declare an object of that type. You'll probably run out of
storage on an embedded target.
Earlier we said that it is the Ada type that determines how parameters are passed, and that
scalars and elementary types are always passed by copy. For mode in that's simple, the
copy to the C formal parameter is done and that's all there is to it. But suppose the mode
is instead out or in out? In that case the presumably updated value must be returned
to the caller, but C doesn't do that by copy. Here the compiler will come to the rescue
and make it work, transparently. Specifically, we just declare the Ada subprogram's formal
parameter type as usual, but on the C formal we use a reference. We're talking about scalar
and elementary types so let's use int arbitrarily. We make the mode in out but out would
also serve:
Now the compiler does its magic: it generates code to make a copy of the actual parameter,
but it makes that copy into a hidden temporary object. Then, when calling the C routine,
it passes the address of the hidden object, which corresponds to the reference expected
on the C side. The C code updates the value of the temporary object via the reference,
and then, on return, the compiler copies the value back from the temporary to the actual
parameter. Problem solved, if a bit circuitous.
There are other aspects to interfacing with C, such as variadic functions that take a varying
number of arguments, but you can find these elsewhere in the learn courses.
Next, we examine the child packages under Interfaces.C. These packages are not used
as much as the parent Interfaces.C package so we will provide an overview. You can look
up the contents within GNAT Studio or the Ada language standard.
Package Interfaces.C declares types and subprograms allowing an Ada program to allo-
cate, reference, update, and free C-style strings. In particular, the private type chars_ptr
corresponds to a common use of char * in C programs, and an object of this type can be
passed to imported subprograms for which char * is the type of the argument of the C
function. A subset of the package content is as follows:
package Interfaces.C.Strings is
...
end Interfaces.C.Strings;
Note that allocation might be via malloc, or via Ada’s allocator new. In either case, the
returned value is guaranteed to be compatible with char*. Deallocation must be via the
supplied procedure Free.
An amusing point is that you can overwrite the end of the char array just like you can in C,
via procedure Update. The Check parameter indicates whether overwriting past the end is
checked. The default is True, unlike in C, but you could pass an explicit False if you felt
the need to do something questionable.
Like Interfaces.C, package Interfaces.Fortran defines Ada types to be used when work-
ing with subprograms using the Fortran calling convention. These types have representa-
tions that are identical to the default representations of the Fortran intrinsic types Integer,
Real, Double Precision, Complex, Logical, and Character in some supported Fortran imple-
mentation. And like the C package, the ways that parameters of various types are passed
are also specified.
We leave the details to you to look up in the language standard, if you find them needed in
an embedded application.
When working close to the hardware, especially when interacting with a device, it is not
uncommon for the hardware to require a very specific set of assembly language instructions
to be generated. There are two ways to achieve this: the right way and the wrong way.
The wrong way is to experiment with the source code and compiler switches until you get
the exact assembly code you need generated (assuming it is possible at all). But what
happens when the next compiler release arrives with a new optimization? And abandon all
hope if you go to a new compiler vendor. This approach is both labor-intensive and very
brittle.
The right way is to express the precise assembly code sequence explicitly within the Ada
source code. (That's true to any high level language, not just Ada.) Or you can call an
intrinsic function, if there is one that does exactly what you need. We will focus on inserting
it directly, in what is known as "machine code insertion", or "inline assembler."
As an example of the need for this capability, consider the GPIO (General Purpose I/O) port
on an STM32 Arm microcontroller. Each port contains 16 individual I/O pins, each of which
can be configured as an independent discrete input or output, or as a control line for a
device, with pull-up or pull-down registers, with different clock speeds, and so on. Different
on-chip devices use various collections of pins in ways specific to the devices, and require
exclusive assignment of the pins. However, any given pin can be used by several different
devices. For example, pin 11 on port A ("PA11") can be used by USART #1 as the clear-
to-send ("CTS") line, or the CAN #1 bus Rx line, or Channel 4 of Timer 1, among others.
Therefore, one of the responsibilities of the system designer is to allocate pins to devices,
ensuring that they are allocated uniquely. It is difficult to debug the case in which a pin is
accidentally configured for one device and then reconfigured for use with another device
(assuming the first device remains in use). To help ensure exclusive allocations, every
GPIO port on this Arm implementation has a way of locking the configuration of each I/O
pin. That way, some other part of the software can't successfully change the configuration
accidentally, for use with some other device. Even if the same configuration was to be
used for another device, the lock prevents the accidental update so we find out about the
unintentional sharing.
To lock a pin on a port requires a special sequence of reads and writes to a GPIO register
for that port. A specific bit pattern is required during the reads and writes. The sequence
and bit pattern is such that accidentally locking the pin is highly unlikely.
Once we see how to express assembly language sequences in general we will see how to
get the necessary sequence to lock a port/pin pair. Unfortunately, although you can express
exactly the code sequence required, such a sequence of assembly language instructions
is clearly target hardware-specific. That means portability is inherently limited. Moreover,
the syntax for expressing it varies with the vendor, even for the same target hardware.
Being able to insert it at the Ada source level doesn't help with either portability issue.
You should understand that the use-case for machine code insertion is for small, short se-
quences. Otherwise you would write the code in assembly language directly, in a separate
file. That might obtain a degree of vendor independence, at least for the given target, but
not necessarily. The use of inline assembler is intended for cases in which a separate file
containing assembly language is not simpler.
With those caveats in place, let's first examine how to do it in general and then how to
express it with GNAT specifically.
The right way to express an arbitrary sequence of one or more assembly language state-
ments is to use so-called "code statements." A code statement is an Ada statement, but
it is also a qualified expression of a type defined in package System.Machine_Code. The
content of that package, and the details of code statements, are implementation-defined.
Although that affects portability there really is no alternative because we are talking about
machine instruction sets, which vary considerably and cannot be standardized at this level.
Package System.Machine_Code contains types whose values provide a way of expressing
assembly instructions. For example, let's say that there is a "HLT" instruction that halts the
processor for some target. There is no other parameter required, just that op-code. Let's
also say that one of the types in System.Machine_Code is for these "short" instructions
consisting only of an op-code. The syntax for the type declaration would then allow the
following code statement:
Each of Short_Instruction, Command, and HLT are defined by the vendor in this hypothet-
ical version of package System.Machine_Code. You can see why we say that it is both a
statement (note the semicolon) and a qualified expression (note the apostrophe).
Code statements must appear in a subprogram body, after the begin. Only code state-
ments are allowed in such a body, only use-clauses can be in the declarative part, and no
exception handlers are allowed. The complete example would be as follows:
With that, to halt the processor the Ada code can simply call procedure Halt. When the
optimizer is enabled there will be no code emitted to make the call, we'd simply see the
halt instruction emitted directly in-line.
Package System.Machine_Code provides access to machine instructions but as we men-
tioned, the content is vendor-defined. In addition, the package itself is optional, but is
required if Annex C, the Systems Programming Annex, is implemented by the vendor. In
practice most all vendors provide this annex.
In GNAT, the content of System.Machine_Code looks something like this:
...
function Asm
(Template : String;
Outputs : Asm_Output_Operand := No_Output_Operands;
Inputs : Asm_Input_Operand := No_Input_Operands;
Clobber : String := "";
Volatile : Boolean := False) return Asm_Insn;
With this package content, the expression in a code statement is of type Asm_Insn, short
for "assembly instruction." Multiple overloaded functions named Asm return values of that
type.
The Template parameter in a string containing one or more assembly language instruc-
tions. These instructions are specific to the target machine. The parameter Outputs pro-
vides mappings from registers to source-level entities that are updated by the assembly
statement(s). Inputs provides mappings from source-level entities to registers for inputs.
Volatile, when True, tells the compiler not to optimize the call away, and Clobber tells the
compiler which registers, or memory, if any, are altered by the instructions in Template.
("Clobber" is colloquial English for "destroy.") That last is important because the compiler
was likely already using some of those registers so the compiler will need to restore them
after the call.
We could say, for example, the following, taking all the defaults except for Volatile:
As you can imagine the full details are extensive, beyond the scope of this introduction.
See the GNAT User Guide ("Inline Assembler") for all the gory details.
Now, back to our GPIO port/bin locking example. The port type is declared as follows:
We've elided all but the LCKR component representing the "lock register" within each port.
We'd have a record representation clause to ensure the required layout but that's not im-
portant here. Word is an unsigned (modular) 32-bit integer type. One of the hardware
requirements for accessing the lock register is that the entire register has to be read or
written whenever any bits within it are accessed. The compiler must not, for example,
write one of the bytes within the register in order to set or clear a bit within that part of
the register. Therefore we mark the register as Atomic. If the compiler cannot honor that
aspect the compilation will fail, so we would know there is a problem.
Per the ST Micro Reference Manual, the lock control bit is referred to as LCKK and is bit #16,
i.e., the first in the upper half of the LCKR register word.
That bit is also known as the "Lock Key" (hence the abbreviation) because it is used to
control the locking of port/pin configurations.
There are 16 GPIO pins per port, represented by the lower 16 bits of the register. Each one
of these 16 bits corresponds to one of the 16 GPIO pins on a port. If any given bit reads as
a 1 then the corresponding pin is locked.
Graphically that looks like this:
type GPIO_Pin is
(Pin_0, Pin_1, Pin_2, Pin_3, Pin_4, Pin_5, Pin_6, Pin_7,
Pin_8, Pin_9, Pin_10, Pin_11, Pin_12, Pin_13, Pin_14, Pin_15);
Note that we had to override the default enumeration representation so that each pin —
each enumeral value — would occupy a single dedicated bit in the bit-mask.
With that in place, let's lock a pin. A specific sequence is required to set a pin's lock bit.
The sequence writes and reads values from the port's LCKR register. Remember that this
32-bit register has 16 bits for the pin mask (0 .. 15), with bit #16 used as the "lock control
bit".
1. write a 1 to the lock control bit with a 1 in the pin bit mask for the pin to be locked
2. write a 0 to the lock control bit with a 1 in the pin bit mask for the pin to be locked
3. do step 1 again
4. read the entire LCKR register
5. read the entire LCKR register again (optional)
Throughout the sequence the same value for the lower 16 bits of the word must be main-
tained (i.e., the pin mask), including when clearing the LCCK bit in the upper half.
If we wrote this in Ada it would look like this:
Pin'Enum_Rep gives us the underlying value for the enumeration value. We cannot use
'Pos because that attribute provides the logical position number within the enumerated
values, and as such always increases consecutively. We need the underlying representation
value that we specified explicitly.
The Ada procedure works, but only if the optimizer is enabled (which also precludes debug-
ging). But even so, there is no guarantee that the required assembly language instruction
sequence would be generated, especially one that maintains that required bit mask value
on each access. A machine-code insertion is appropriate for all the reasons presented ear-
lier:
We've combined the instructions into one Asm expression. As a result, we can use ASCII
line-feed and horizontal tab characters to format the listing produced by the compiler so
that each instruction is on a separate line and aligned with the previous instruction, as if we
had written the sequence in assembly language directly. That enhances readability later,
during examination of the compiler output to verify the required sequence was emitted.
In the above, "%0" is the first input, containing the address of the Port parameter. "%1" is
the other input, the value of the Pin parameter. We're using register r3 explicitly, as the
"temporary" variable, so we tell the compiler that it has been "clobbered."
If we examine the assembly language output from compiling the file, we find the body of
procedure Lock is as hoped:
The first two statements load register 2 (r2) and register 1 (r1) with the subprogram param-
eters, i.e., the port and pin, respectively. Register 2 gets the starting address of the port
record, in particular. (Offset #28 is the location of the LCKR register. The port is passed by
reference so that address is actually that of the hardware device.)
We will have separately declared procedure Lock with inlining enabled, so whenever we
call the procedure we will get the exact assembly language sequence required to lock the
indicated pin on the given port, without any additional code for a procedure call.
Note that we get the calling convention right automatically, because the subprogram is not
a foreign entity written in some other language (such as assembly language). It's an Ada
subprogram with special content so the Ada convention applies as usual.
When multiple programming languages are involved, the main procedure might not be
implemented in Ada. Maybe the bulk of the program is written in C, for example, and this
C code calls some Ada routines that have been exported (with the C convention).
That means the Ada builder does not create the executable image's entry point. In fact the
Ada main procedure is never the entry point for the final executable image, it's just where
the application code begins, like the C main function. There are setup and initialization steps
that must happen before any program can execute on a target, and the entry point code
is responsible for this functionality. For example, on a bare machine target, the hardware
must be initialized, the trap vectors installed, the segments initialized, and so on. On a
target running an operating system, the OS is responsible for that initialization but there
will be OS-specific initialization steps too. For example, if command-line arguments are
supported these may be gathered. All this initialization code is generated by the builder,
regardless of the language, followed by a call to the main routine.
Some of the initialization is specific to Ada programming, and must occur before any calls
occur to the exported Ada routines. In particular, the entry point code emitted by the Ada
builder initializes the Ada run-time system and calls all the elaboration routines for the
library units in the application code. Only then does the emitted code invoke the Ada main.
If the Ada builder is not going to create the executable it has no chance to emit the code to
do that prior initialization. A foreign language builder will not emit such code, so we have
a problem.
You could learn enough about how the foreign builder works, and how your Ada builder
works, to create a work-around. You could learn what the Ada builder would emit, in other
words, and ensure those routines are called manually, either directly or by augmenting the
builder scripts (assuming that's possible). But the work-around would be labor-intensive
and not robust to changes by the tool vendors. It would be an ugly hack, in other words.
That work-around would not be portable either. The Ada standard can't address hardware-
or OS-specific initialization, but it can standardize the name for a routine to do the Ada-
specific initialization. Specifically, procedure adainit initializes the Ada application code
and the Ada run-time library. Similarly, one might need to shut down the Ada code when
no further calls will be made to the exported Ada routines. Procedure adafinal performs
this shut-down functionality. Neither procedure has parameters.
The main function in the other language is intended to import these routines and manually
call them each exactly once. adainit must be called prior to any calls to the Ada code,
and adafinal is to be called after all the calls to the Ada code.
For example:
#include "stdio.h"
In the above, we have an Ada routine to compute a checksum, called by a C main function.
Therefore, we use "extern" to tell the C compiler that the "checksum" function is defined
elsewhere, i.e., in the Ada routine. Likewise, we tell the compiler that functions adainit
and adafinal are defined elsewhere. The call to adainit is made before the call to any
Ada code, thus all the elaboration code is guaranteed to happen before checksum needs it.
Once the Ada code is not needed, the call to adafinal can be made.
Both adainit and adafinal have no effect after the first invocation. That means you
cannot structure your foreign code to iteratively call the two routines whenever you want
to invoke some Ada code. In practice you just call them once in the main and be done with
it.
THIRTYSEVEN
Interacting with hardware devices is one of the more frequent activities in embedded sys-
tems programming. It is also one of the most enjoyable because you can make something
happen in the physical world. There's a reason that making an LED blink is the "hello
world" of embedded programming. Not only is it easy to do, it is surprisingly satisfying. I
suspect that even the developers of "Full Authority Digital Engine Controllers" (FADEC) —
the computers that are in complete, total control of commercial airline engines — have fond
memories of making an LED blink early in their careers. And of course a blinking LED is a
good way to indicate application status, especially if off-board I/O is limited, which is often
the case.
Working at the device register level can be error prone and relatively slow, in terms of
source-lines-of-code (SLOC) produced. That's partly because the hardware is in some cases
complicated, and partly because of the way the software is written. Using bit masks for
setting and clearing bits is not a readable approach, comparatively speaking. There's just
not enough information transmitted to the reader. It might be clear enough when written,
but will you see it that way months later? Readability is important because programs are
read many more times than they are written. Also, an unreadable program is more difficult
to maintain, and maintenance is where most money is spent in long-lived applications.
Comments can help, until they are out of date. Then they are an active hindrance.
For example, what do you think the following code does? This is real code, where temp and
temp2 are unsigned 32-bit integers:
That's unfair to ask, absent any context. The code configures a general purpose I/O (GPIO)
pin on an Arm microcontroller for one of the "alternate functions". GPIOx is a pointer to a
GPIO port, GPIO_PinSource is a GPIO pin number, and GPIO_AF is the alternate function
number. But let's say you knew that. Is the code correct? The longer it takes to know, the
less productive you are.
The fact that the code above is in C is beside the point. If we wrote it the same way in Ada
it would be equally opaque, if not more so. There are simpler approaches. Judicious use of
record and array types is one. We'll say more about that later, but the underlying idea is to
let the compiler do as much work for us as possible. For example, the data structures used
in the code above require explicit shifting whenever they are accessed. If we can avoid that
at the source code level — by having the compiler do it for us — we will have simplified
the code considerably. Furthermore, letting the compiler do the work for us makes the code
more maintainable (which is where the money is). For example, if the code does the shifting
explicitly and the data structures are changed, we'll have to change the number of bits to
shift left or right. Constants will help there, but we still have to remember to change them;
1175
Learning Ada
the compiler won't complain if we forget. In contrast, if we let the compiler do this shifting
for us, the amounts to shift will be changed automatically.
Some devices are very simple. In these cases the application may interact directly with the
device without unduly affecting productivity. For example, there was a board that had a
user-accessible rotary switch with sixteen distinct positions. Users could set the switch to
whatever the application code required, e.g., to indicate some configuration information.
The entire software interface to this device consisted of a single read-only 8-bit byte in
memory. That's all there was to it: you read the memory and thus got the numeric setting
of the switch.
More complex devices, however, usually rely on software abstraction to deal with the com-
plexity. Just as abstraction is a fundamental way to combat complexity in software, ab-
straction also can be used to combat the complexity of driving sophisticated hardware.
The abstraction is presented to users by a software "device driver" that exists as a layer
between the application code and the hardware device. The layer hides the gory details of
the hardware manipulation behind subprograms, types, and parameters.
We say that the device driver layer is an abstraction because, at the least, the names of
the procedures and functions indicate what they do, so at the call site you can tell what is
being done. That's the point of abstraction: it allows us to focus on what, rather than how.
Consider that GPIO pin configuration code block again. Instead of writing that block every
time we need to configure the alternate function for a pin, suppose we called a function:
The GPIO_PinAFConfig function is part of the GPIO device driver provided by the STM32
Standard Peripherals Library (SPL). Even though that's not the best function name conceiv-
able, calls to the function will be far more readable than the code of the body, and we only
have to make sure the function implementation is correct once. And assuming the device
drivers' subprograms can be inlined, the subprogram call imposes no performance penalty.
Note the first parameter to the call above: USARTx_TX_GPIO_PORT. There are multiple GPIO
ports on an Arm implementation; the vendor decides how many. In this case one of them has
been connected to a USART (Universal Synchronous Asynchronous Receiver Transmitter),
an external device for sending and receiving serial data. When there are multiple devices,
good software engineering suggests that the device driver present a given device as one of
a type. That's what an "abstract data type" (ADT) provides for software and so the device
driver applies the same design. An ADT is essentially a class, in class-oriented languages. In
Ada, an ADT is represented as a private type declared in a package, along with subprograms
that take the type as a parameter.
The Ada Drivers Library (ADL) provided by AdaCore and the Ada community uses this design
to supply Ada drivers for the timers, I2C, A/D and D/A converters, and other devices common
to microcontrollers. Multiple devices are presented as instances of abstract data types. A
variety of development platforms from various vendors are supported, including the STM32
series boards. The library is available on GitHub for both non-proprietary and commercial
use here: https://github.com/AdaCore/Ada_Drivers_Library. We are going to use some of
these drivers as illustrations in the following sections.
Some devices are connected to the processor on a dedicated bus that is separate from the
memory bus. The Intel processors, for example, used to have (and may still have) instruc-
tions for sending and receiving data on this bus. These are the "in" and "out" instructions,
and their data-length specific variants.
The original version of Ada defined a package named Low_Level_IO for such architectures,
but there were very few implementations (maybe just one, known to support the Intel
processors). As a result, the package was actually removed from the language standard.
Implementations could still support the package, it just wouldn't be a standard package.
That's different from constructs that are marked as "obsolescent" by the standard, e.g., the
pragmas replaced by aspects, among other things. Obsolescent constructs are still part of
the standard.
If a given target machine has such I/O instructions for the device bus, these can be invoked
in Ada via machine-code insertions. For example:
Applications could use these subprograms to set the frequency of the Intel PC tone gener-
ator, for example, and to turn it on and off. (You can't do that any more in application code
because modern operating systems don't give applications direct access to the hardware,
at least not by default.)
Although the Low_Level_IO package is no longer part of the language, you can write this
sort of thing yourself, or vendors can do it. That's possible because the Systems Program-
ming Annex, when implemented, guarantees fully effective use of machine-code inserts.
That means you can express anything the compiler could emit. The guarantee is important
because otherwise the compiler might "get in the way." For example, absent the guaran-
tee, the compiler would be allowed to insert additional assembly language statements in
between yours. That can be a real problem, depending on what your statements do. For
instance, if your MCI assembly statements do something and then check a resulting condi-
tion code, such as the overflow flag, those interleaved compiler-injected statements might
clear that condition code before your code can check it. Fortunately, the annex guarantees
that sort of thing cannot happen.
In another earlier chapter (page 1117), we said that we could query the address of some
object, and we also showed how to use that result to specify the address of some other
object. We used that capability to create an "overlay," in which two objects are used to
refer to the same memory locations. As we indicated in that discussion, you would not
use the same type for each object — the point, after all, is to provide a view of the shared
underlying memory cells that is not already available otherwise. Each distinct type would
provide a distinct view of the memory values, that is, a set of operations providing some
required functionality.
For example, here's an overlay composed of a 32-bit signed integer object and a 32-bit
array object:
X : aliased Integer_32;
Y : Bits32 with Address => X'Address;
Because one view is as an integer and the other as an array, we can access that memory
using the two different views' operations. Using the view as an array object (Y) we can
access individual bits of the memory shared with X. Using the view as an integer (X), we
can do arithmetic on the contents of that memory. (We could have used an unsigned integer
instead of the signed type, and thereby gained the bit-oriented operations, but that's not
the point.)
Very often, though, there is only one Ada object that we place at some specific address.
That's because the Ada object is meant to be the software interface to some memory-
mapped hardware device. In this scenario we don't have two overlaid Ada objects, we just
have one. The other "object" is the hardware device mapped to that starting address. Since
they are at the same memory location(s), accessing the Ada object accesses the hardware
device.
For a real-world but nonetheless simple example, recall that example of a rotary switch on
the front of our embedded computer that we mentioned in the introduction. This switch
allows humans to provide some very simple input to the software running on the computer.
We declare the object and also specify the address, but not by querying some entity. We
already know the address from the hardware documentation. But we cannot simply use
an integer address literal from that documentation because type System.Address is al-
most always a private type. We need a way to compose an Address value from an integer
value. The package System.Storage_Elements defines an integer representation for Ad-
dress values, among other useful things, and a way to convert those integer values to
Address values. The function To_Address does that conversion.
As a result, in the Ada code, reading the value of the variable Rotary_Switch reads the
number on the actual hardware switch.
Note that if you specify the wrong address, it is hard to say what happens. Likewise, it is an
error for an address clause to disobey the object's alignment. The error cannot be detected
at compile time, in general, because the address is not necessarily known at compile time.
There's no requirement for a run-time check for the sake of efficiency, since efficiency
seems paramount here. Consequently, this misuse of address clauses is just like any other
misuse of address clauses — execution of the code is erroneous, meaning all bets are off.
You need to know what you're doing.
What about writing to the variable? Is that meaningful? In this particular example, no. It
is effectively read-only memory. But for some other device it very well could be mean-
ingful, certainly. It depends on the hardware. But in this case, assigning a value to the
Rotary_Switch variable would have no effect, which could be confusing to programmers.
It looks like a variable, after all. We wouldn't declare it as a constant because the human
user could rotate the switch, resulting in a different value read. Therefore, we would hide
the Ada variable behind a function, precluding the entire issue. Clients of the function can
then use it for whatever purpose they require, e.g., as the unique identifier for a computer
in a rack.
Let's talk more about the type we use to represent a memory-mapped device. As we said,
that type defines the view we have for the object, and hence the operations we have avail-
able for accessing the underlying mapped device.
We choose the type for the representative Ada variable based on the interface of the hard-
ware mapped to the memory. If the interface is a single monolithic register, for example,
then an integer (signed or unsigned) of the necessary size will suffice. But suppose the
interface is several bytes wide, and some of the bytes have different purposes from the
others? In that case, a record type is the obvious solution, with distinct record components
dedicated to the different parts of the hardware interface. We could use individual bits too,
of course, if that's what the hardware does. Ada is particularly good at this fine-degree
of representation because record components of any types can be specified in the layout,
down to the bit level, within the record.
In addition, we might want to apply more than one type, at any one time, to a given memory-
mapped device. Doing so allows the client code some flexibility, or it might facilitate an
internal implementation. For example, the STM32 boards from ST Microelectronics include
a 96-bit device unique identifier on each board. The identifier starts at a fixed memory
location. In this example we provide two different views — types — for the value. One
type provides the identifier as a String containing twelve characters, whereas another type
provides the value as an array of three 32-bit unsigned words (i.e., 12 bytes). The two types
are applied by two overloaded functions that are distinguished by their return type:
package STM32.Device_Id is
end STM32.Device_Id;
The subtype Device_Id_Image is the view of the 96-bits as an array of twelve 8-bit char-
acters. (Using type String here isn't essential. We could have defined an array of bytes
instead of Character.) Similarly, subtype Device_Id_Tuple is the view of the 96-bits as
an array of three 32-bit unsigned integers. Clients can then choose how they want to view
the unique id by choosing which function to call.
In the package body we implement the functions as two ways to access the same shared
memory:
with System;
end STM32.Device_Id;
The problem, other than the magic numbers (some named constants would have helped),
is that the code is doing nearly all the work instead of off-loading it to the compiler. Partly
that's because in C we cannot declare a numeric type representing a 4-bit quantity, so
everything is done in terms of machine units, in this case 32-bit unsigned integers.
Why do we need 4-bit values? At the hardware level, each memory-mapped GPIO port
has a sequence of 16 4-bit quantities, one for each of the 16 pins on the port. Those 4-bit
quantities specify the "alternate functions" that the pin can take on, if needed. The alternate
functions allow a given pin to do more than act as a single discrete I/O pin. For example, a
pin could be connected to the incoming lines of a USART. We use the configuration routine to
apply the specific 4-bit code representing the alternate function required for our application.
These 16 4-bit alternate function fields are contiguous in the register (hence memory) so
we can represent them as an array with a total size of 64-bits (i.e., 16 times 4). In the C
version this array has two components of type uint32_t so it must compute where the
corresponding 4-bit value for the pin is located within those two words. In contrast, the Ada
version of the array has components of the 4-bit type, rather than two 32-bit components,
and simply uses the pin number as the index. The resulting Ada procedure body is extremely
simple:
procedure Configure_Alternate_Function
(Port : in out GPIO_Port;
Pin : GPIO_Pin;
AF : GPIO_Alternate_Function_Code)
is
begin
Port.AFR (Pin) := AF;
end Configure_Alternate_Function;
In the Ada version, AFR is a component within the GPIO_Port record type, much like in the
C code's struct. However, Ada allows us to declare a much more descriptive set of types,
and it is these types that allows the developer to off-load the work to the compiler.
First, in Ada we can declare a 4-bit numeric type:
type Bits_4 is mod 2**4 with Size => 4;
The Bits_4 type was already globally defined elsewhere so we just derive our 4-bit "al-
ternate function code" type from it. Doing so allows the compiler to enforce simple strong
typing so that the two value spaces are not accidentally mixed. This approach also increases
understanding for the reader:
type GPIO_Alternate_Function_Code is new Bits_4;
-- We cannot use an enumeration type because there are duplicate binary
-- values
Note that we can use the GPIO Pin parameter directly as the index into the array type,
obviating any need to massage the Pin value in the procedure. That's possible because
the type GPIO_Pin is an enumeration type:
type GPIO_Pin is
(Pin_0, Pin_1, Pin_2, Pin_3, Pin_4, Pin_5, Pin_6, Pin_7,
Pin_8, Pin_9, Pin_10, Pin_11, Pin_12, Pin_13, Pin_14, Pin_15);
In the hardware, the GPIO_Pin values don't start at zero and monotonically increase. In-
stead, the values are bit patterns, where one bit within each value is used. The enumeration
representation clause allows us to express that representation.
Type Alternate_Function_Fields is then used to declare the AFR record component in
the GPIO_Port record type:
These declarations define a record type that matches the content and layout of the STM32
GPIO Port memory-mapped device.
Let's compare the two procedure implementations again. Here they are, for convenience:
procedure Configure_Alternate_Function
(Port : in out GPIO_Port;
Pin : GPIO_Pin;
AF : GPIO_Alternate_Function_Code)
is
begin
Port.AFR (Pin) := AF;
end Configure_Alternate_Function;
Which one is correct? Both. But clearly, the Ada version is far simpler, so much so that
it is immediately obvious that it is correct. Not so for the coding approach used in the C
version, comparatively speaking. It is true that the Ada version required a couple more
type declarations, but those make the procedure body far simpler. That resulting simplicity
is a reflection of the balance between data structures and executable statements that we
should always try to achieve. Ada just makes that easier to achieve than in some other
languages.
Of course, the underlying hardware likely has no machine-supported 4-bit unsigned type so
larger hardware numeric types are used in the generated code. Hence there are shifts and
masking being done in the Ada version as well, but they do not appear in the source code.
The developer has let the compiler do that work. An additional benefit of this approach is
that the compiler will change the shifting and masking code for us if we change the explicit
type declarations.
Why is simplicity so important? Simplicity directly increases understandability, which di-
rectly affects correctness and maintainability, which greatly affects the economic cost of
the software. In large, long-lived projects, maintenance is by far the largest economic cost
driver. In high-integrity applications, correctness is essential. Therefore, doing anything
reasonable to keep the code as simple as possible is usually worth the effort. In some
projects the non-functional requirements, especially performance, can dictate less simple
code, but that won't apply to all of the code. Where possible, simplicity rules.
One more point about the GPIO ports. There are as many of these ports as the Arm mi-
crocontroller vendor decides to implement. And as we said, they are memory-mapped, at
addresses specified by the vendor. If the memory used by all the ports is contiguous, we
can conveniently use an array of the GPIO_Port record type to represent all the ports im-
plemented. We would just set the array object's address at the address specified for the
first port object in memory. Then, normal array indexing will provide access to any given
port in the memory-mapped hardware.
This array approach requires each array component — the GPIO_Port record type — to be
the right size so that all the array components start on addresses corresponding to the start
of the next port in hardware.
That starting address correspondence for the array components is obtained automatically
as long as the record type includes all the memory used by any individual device. In that
case the next array component will indeed start at an address matching the next device in
hardware. Note that this assumes the first array component matches the address of the
first hardware device in memory. The first array component is at the same address as the
whole array object itself (a fact that is guaranteed by the language), so the array address
must be set to whatever the vendor documentation specified for the first port.
However, in some cases the vendor will leave gaps of unused memory for complicated
memory-mapped objects like these ports. They do so for the sake of future expansion of
the implementation, e.g., to add new features or capacity. The gaps are thus between
consecutive hardware devices.
These gaps are presumably (hopefully!) included in the memory layout documented for
the device, but it won't be highlighted particularly. You should check, therefore, that the
documented starting addresses of the second and subsequent array components are what
you will get with a simple array object having components of that record type.
For example, the datasheet for the STM32F407 Arm implementation indicates that the
GPIO ports start at address 16#4002_0000#. That's where GPIO_A begins. The next port,
GPIO_B, starts at address 16#4002_0400#, or a byte offset of 1024 in decimal. In the
STM32F4 Reference Manual, however, the GPIO port register layout indicates a size for any
one port that is much less than 1024 bytes. As you saw earlier in the corresponding record
type declaration, on the STM32F4 each port only requires 40 (decimal) bytes. Hence there's
a gap of unused memory between the ports, including after the last port, of 984 bytes (7872
bits).
To represent the gap, an "extra", unused record component was added, with the necessary
location and size specified within the record type, so that the unused memory is included in
the representation. As a result, each array component will start at the right address (again,
as long as the first one does). Telling the compiler, and future maintainers, that this extra
component is not meant to be referenced by the software would not hurt. You can use the
pragma or aspect Unreferenced for that purpose. Here's the code again, for convenience:
The type for the gap, Unaccessed_Gap, must represent 984 bytes so we declared an array
like so:
We also set the size of the entire record type to 16#400# bytes since that is the total of
the required bytes plus the gap, as per the documentation. As such, this is a "confirming"
size clause because the reserved gap component increases the required size to that value
(which is the point). We don't really need to do both, i.e., declare the reserved gap com-
ponent and also set the record type size to the larger value. We could have done either
one alone. One could argue that setting the size alone would have been simpler, in that
it would obviate the type declaration and corresponding record component declaration.
Being doubly explicit seemed a good idea at the time.
In the overlay example there were two distinct Ada objects, of two different types, sharing
one (starting) address. The overlay provides two views of the memory at that address
because there are two types involved. In this idiom the address is known when the code is
written, either because it is a literal value specified in some hardware spec, or it is simply
the address of the other object (in which case the actual address value is neither known
nor relevant).
When there are several views required, declaring multiple overlaid variables at the same
address absolutely can work, but can be less convenient than an alternative idiom. The
alternative is to convert an address value to a value of an access type. Dereferencing the
resulting access value provides a view of the memory corresponding to the designated type,
starting at the converted address value.
For example, perhaps a networking component is given a buffer — an array of bytes —
representing a received message. A subprogram is called with the buffer as a parameter,
or the parameter can be the address of the buffer. If the subprogram must interpret this
array via different views, this alternative approach works well. We could have an access
type designating a message preamble, for example, and convert the first byte's address into
such an access value. Dereferencing the conversion gives the preamble value. Likewise,
the subprogram might need to compute a checksum over some of the bytes, so a different
view, one of an array of a certain set size, could be used. Again, we could do that with
overlaid objects but the alternative can be more convenient.
Here's a simple concrete example to illustrate the approach. Suppose we want to have a
utility to swap the two bytes at any arbitrary address. Here's the declaration:
Callers pass the address of an object intended to have its (first) two bytes swapped:
Swap2 (Z'Address);
generic
type Object (<>) is limited private;
package System.Address_To_Access_Conversions is
end System.Address_To_Access_Conversions;
Object is the generic formal type parameter, i.e., the type we want our converted addresses
to designate via the type Object_Pointer. In the byte-swapping example, the type Word
was passed to Object in the instantiation.
The access type used by the functions is Object_Pointer, declared along with the func-
tions. Object_Pointer designates values of the type used for the generic actual parameter,
in this case Word.
Note the pragma Convention applied to each function, indicating that there is no actual
function call involved; the compiler emits the code directly, if any code is actually re-
quired. Otherwise the compiler just treats the incoming Address bits as a value of type
Object_Pointer.
The instantiation specifies type Word as the generic actual type parameter, so now we have
a set of functions for that type, in particular To_Pointer.
Let's look at the code again, this time with the additional declarations:
Word_Ops is the generic instance, followed immediately by a use clause so that we can refer
to the visible content of the package instance conveniently.
In the renaming expression, To_Pointer (Location) converts the incoming address in
Location to a pointer designating the Word at that address. The .all dereferences the
resulting access value to get the designated Word value. Hence X refers to that two-byte
value in memory.
We could almost certainly achieve the same affect by replacing the call to the function
in To_Pointer with a call to an instance of Ada.Unchecked_Conversion. The conversion
would still be between an access type and a value of type System.Address, but the access
type would require declaration by the user. In both cases there would be an instantiation
of a language-defined facility, so there's not much saving in lines of source code, other
than the access type declaration. Because System.Address_To_Access_Conversions is
explicitly intended for this purpose, good style suggests its use in preference to unchecked
conversion, but both approaches are common in production code.
In either case, the conversion is not required to work, although in practice it will, most of
the time. Representing an access value as an address value is quite common because it
matches the typical underlying hardware's memory model. But even so, a single address
is not necessarily sufficient to represent an access value for any given designated type. In
that case problems arise, and they are difficult to debug.
For example, in GNAT, access values designating values of unconstrained array types, such
as String, are represented as two addresses, known as "fat pointers". One address points
to the bounds for the specific array object, since they can vary. The other address desig-
nates the characters. Therefore, conversions of a single address to an access value requir-
ing fat pointers will not work using unchecked conversions. (There is a way, however, to
tell GNAT to use a single address value, but it is an explicit step in the code. Once done,
though, unchecked conversions would then work correctly.)
You can alternatively use generic package System.Address_To_Access_Conversions.
That generic is defined for the purpose of converting addresses to access values, and vice
versa. But note that the implementation of the generic's routines must account for the
representation their compiler uses for unbounded types like String.
Part of "letting the compiler do the work for you" is not doing address arithmetic in the
source code if you can avoid it. Instead, for instance, use the normal "dot notation" to
reference components, and let the compiler compute the offsets to those components. The
approach to implementing procedure Configure_Alternate_Function for a GPIO_Port is
a good example.
That said, sometimes address arithmetic is the most direct expression of what you're trying
to implement. For example, when implementing your own memory allocator, you'll need
to do address arithmetic.
Earlier in this section we mentioned the package System.Storage_Elements, for the sake
of the function that converts integer values to address values. The package also defines
functions that provide address arithmetic. These functions work in terms of type System.
Address and the package-defined type Storage_Offset. The type Storage_Offset is an
integer type with an implementation-defined range. As a result you can have positive and
negative offsets, as needed. Addition and subtraction of offsets to/from addresses is sup-
ported, as well as the mod operator.
Combined with package System (for type System.Address), the functions and types in this
package provide the kinds of address arithmetic other languages provide. Nevertheless,
you should prefer having the compiler do these computations for you, if possible.
Here's an example illustrating the facilities. The procedure defines an array of record values,
then traverses the array, printing the array components as it goes. (This is not the way to
really implement such code. It's just an illustration for address arithmetic.)
procedure Demo_Address_Arithmetic is
type R is record
X : Integer;
Y : Integer;
end record;
Offset : Storage_Offset;
begin
Objects := ((0,0), (1,1), (2,2), (3,3), (4,4),
(5,5), (6,6), (7,7), (8,8), (9,9));
Offset := 0;
Seriously, this is just for the purpose of illustration. It would be much better to just index
into the array directly.
THIRTYEIGHT
In another chapter (page 1153), we mentioned that the best way to get a specific set of
machine instructions emitted from the compiler is to write them ourselves, in the Ada source
code, using machine-code insertions (MCI). The rationale was that the code generator will
make reasonable assumptions, including the assumption that performance is of uppermost
importance, but that these assumptions can conflict with device requirements.
For example, the code generator might not issue the specific sequence of machine code
instructions required by the hardware. The GPIO pin "lock" sequence in that referenced
chapter is a good example. Similarly, the optimizer might remove what would otherwise
be "redundant" read/writes to a memory-mapped variable.
The code generator might issue instructions to read a small field in a memory-mapped
record object using byte-sized accesses, when instead the device requires whole-word or
half-word access instructions.
The code generator might decide to load a variable from memory into a register, accessing
the register when the value is required. Typically that approach will yield far better perfor-
mance than going to memory every time the value is read or updated. But suppose the
variable is for a memory-mapped device? In that case we really need the generated code
to go to memory every time.
As you can see, there are times when we cannot let the code generator make the usual
assumptions. Therefore, Ada provides aspects and pragmas that developers can use to
inform the compiler of facts that affect code generation in this regard.
These facilities are defined in the Systems Programming Annex, C.6, specifically. The title of
that sub-clause is "Shared Variables" because the objects (memory) can be shared between
tasks as well as between hardware devices and the host computer. We ignore the context
of variables shared between tasks, focusing instead of shared memory-mapped devices, as
this course is about embedded systems.
When describing these facilities we will use aspects, but remember that the corresponding
pragmas are defined as well, except for one. (We'll mention it later.) For the other aspects,
the pragmas existed first and, although obsolescent, remain part of the language and sup-
ported. There's no need to change your existing source code using the pragmas to use the
aspects instead, unless you need to change it for some other reason.
As this is an introduction, we will not go into absolutely all the details, but will instead give
a sense of what the language provides, and why.
1191
Learning Ada
package P is
type R is record
B0 : Boolean;
B1 : Boolean;
B2 : Boolean;
(continues on next page)
end P;
For a typical target machine the compiler will reject that code, complaining that the Size for
R' must be at least 48 bits, i.e., 8 bits per component. That's because the smallest quantity
this machine can independently address is an 8-bit byte.
But if we don't really need the individual bits to be independently accessed — and let's hope
no hardware designer would define such a device — then we have more flexibility. We could,
for example, require that objects of the entire record type be independently accessible:
package Q is
type R is record
B0 : Boolean;
B1 : Boolean;
B2 : Boolean;
B3 : Boolean;
B4 : Boolean;
B5 : Boolean;
end record with
Size => 8,
Independent;
end Q;
This the compiler should accept, assuming a machine that can access bytes in memory
individually, without having to read some number of other bytes.
But for another twist, suppose we need one of the components to be aliased, so that we
can construct access values designating it via the Access attribute? For example, given
the record type R above, and some object Foo of that type, suppose we want to say Foo.
B0'Access? We'd need to mark the component as aliased:
package QQ is
type R is record
(continues on next page)
end QQ;
The compiler will once again reject the code, complaining that the size of B0 must be a
multiple of a Storage_Unit, in other words, the size of something independently accessible
in memory on this machine.
Why? The issue here is that aliased objects, including components of composite types,
must be represented in such a way that creating the designating access ("pointer") value
is possible. The component B0, if allocated only one bit, would not allow an access value
to be created due to the usual machine accessibility limitation we've been discussing.
Similarly, a record component that is of some by-reference type, such as any tagged type,
introduces the same issues as an aliased component. That's because the underlying imple-
mentation of by-reference parameter passing is much like a 'Access attribute reference.
As important as the effect of this aspect is, you probably won't see it specified. There are
other aspects that are more typically required. However, the semantics of Independent
are part of the semantics of some of these other aspects. Applying them applies Inde-
pendent too, in effect. So even though you don't typically apply it directly, you need to
understand the independent access semantics. We discuss these other, more commonly
applied aspects next.
These representation aspects may be specified for an object declaration, a component dec-
laration, a full type declaration, or a generic formal (complete) type declaration. If any of
these aspects are specified True for a type, then the corresponding aspect is True for all
objects of the type.
Earlier we said that the compiler (specifically the optimizer) might decide to load a variable
from memory into a register, accessing the register when the value is required or updated.
Similarly, the compiler might reorder instructions, and remove instructions corresponding
to redundant assignments in the source code. Ordinarily we'd want those optimizations,
but in the context of embedded memory-mapped devices they can be problematic.
The hardware might indeed require the source code to read or write to the device in a way
that the optimizer would consider redundant, and in order to interact with the device we
need every read and write to go to the actual memory for the mapped device, rather than
a register. As developers we have knowledge about the context that the compiler lacks.
The compiler is aware of the fact that the Ada object is memory-mapped because of the
address clause placing the object at a specific address. But the compiler does not know we
are interacting with an external hardware device. Perhaps, instead, the object is mapped to
a specific location because some software written in another language expects to access it
there. In that case redundant reads or writes of the same object really would be redundant.
The fact that we are interacting with a hardware device makes a difference.
In terms of the language rules, we need reading from, and writing to, such devices to be
part of what the language refers to as the "external effects" of the software. These effects
are what the code must actually produce. Anything else — the internal effects — could be
removed by the optimizer.
For example, suppose you have a program that writes a value to some variable and also
writes the string literal "42" to a file. That's is absolutely all that the program contains.
procedure Demo is
Output : File_Type;
Silly : Integer;
begin
Silly := 0;
Create (Output, Out_File, "output.txt");
Put (Output, "42");
Close (Output);
end Demo;
The value of the variable Silly is not used in any way so there is no point in even declaring
the variable, much less generating code to implement the assignment. The update to the
variable has only an internal effect. With warnings enabled we'll receive notice from the
compiler, but they're just warnings.
However, writing to the file is an external effect because the file persists beyond the end
of the program's execution. The optimizer (when enabled) would be free to remove any
access to the variable Silly, but not the write to the file.
We can make the compiler recognize that a software object is part of an external effect
by applying the aspect Volatile. (Aspect Atomic is pertinent too. More in a moment.)
As a result, the compiler will generate memory load or store instructions for every read or
update to the object that occurs in the source code. Furthermore, it cannot generate any
additional loads or stores to that variable, and it cannot reorder loads or stores from their
order in the source code. "What You See Is What You Get" in other words.
procedure Demo is
Output : File_Type;
Silly : Integer with Volatile;
begin
Silly := 0;
Create (Output, Out_File, "output.txt");
Put (Output, "42");
Close (Output);
end Demo;
If we compile the above, we won't get the warning we got earlier because the compiler is
now required to generate the assignment for Silly.
The variable Silly is not even a memory-mapped object, but remember that we said these
aspects are important to the tasking context too, for shared variables. We're ignoring that
context in this course.
There is another reason to mark a variable as Volatile. Sometimes you want to have
exactly the load and store instructions generated that match those of the Ada code,
even though the volatile object is not a memory-mapped object. For example, elsewhere
(page 1153) we said that the best way to achieve exact assembly instruction sequences is
the use of machine-code inserts (MCI). That's true, but for the moment let's say we want to
write it in Ada without the MCIs. Our earlier example was the memory-mapped GPIO ports
on Arm microcontrollers produced by ST Microelectronics. Specifically, these ports have a
"lock" per GPIO pin that allows the developer to configure the pin and then lock it so that no
other configuration can accidentally change the configuration of that pin. Doing so requires
an exact sequence of loads and stores. If we wrote this in Ada it would look like this:
procedure Lock
(Port : in out GPIO_Port;
Pin : GPIO_Pin)
is
Temp : Word with Volatile;
begin
-- set the lock control bit and the pin
-- bit, clear the others
Temp := LCCK or Pin'Enum_Rep;
Temp is marked volatile for the sake of getting exactly the load and stores that we express
in the source code, corresponding to the hardware locking protocol. It's true that Port is a
memory-mapped object, so it too would be volatile, but we also need Temp to be volatile.
This high-level coding approach will work, and is simple enough that MCIs might not be
needed. However, what really argues against it is that the correct sequence of emitted
code requires the optimizer to remove all the other cruft that the code generator would
otherwise include. (The gcc code generator used by the GNAT compiler generates initially
poor code, by design, relying on the optimizer to clean it up.) In other words, we've told
the optimizer not to change or add loads and stores for Temp, but without the optimizer
enabled the code generator generates other code that gets in the way. That's OK in itself,
as far as procedure Lock is concerned, but if the optimizer is sufficiently enabled we cannot
debug the rest of the code. Using MCIs avoids these issues. The point, though, is that not
all volatile objects are memory mapped.
So far we've been illustrating volatility with scalar objects, such as Lock.Temp above. What
about objects of array and record types? (There are other "composite" types in Ada but
they are not pertinent here.)
When aspect Volatile is applied to a record type or an object of such a type, all the record
components are automatically volatile too.
For an array type (but not a record type), a related aspect Volatile_Components declares
that the components of the array type — but not the array type itself — are volatile. How-
ever, if the Volatile aspect is specified, then the Volatile_Components aspect is auto-
matically applied too, and vice versa. Thus components of array types are covered auto-
matically.
If an object (of an array type or record type) is marked volatile then so are all of its sub-
components, even if the type itself is not marked volatile.
Therefore aspects Volatile and Volatile_Components are nearly equivalent. In fact,
Volatile_Components is superfluous. The language provides the Volatile_Components
aspect only to give symmetry with the Atomic_Components and Independent_Components
aspects. You can simply apply Volatile and be done with it.
Finally, note that applying aspect Volatile does not implicitly apply Independent, although
you can specify it explicitly if need be.
Consider the GPIO pin configuration lock we've mentioned a few times now, that freezes
the configuration of a given pin on a given GPIO port. The register, named LCKR for "lock
register", occupies 32-bits, but only uses 17 total bits (currently). The low-order 16 bits,
[0:15], represent the 16 GPIO pins on the given port. Bit #16 is the lock bit. That bit is the
first bit in the upper half of the entire word. To freeze the configuration of a given pin in
[0:15], the lock bit must be set at the same time as the bit to be frozen. In other words, the
lower half and the upper half of the 32-bit word representing the register must be written
together, at the same time. That way, accidental (un)freezing is unlikely to occur, because
the most efficient, hence typical way for the generated code to access individual bits is for
the compiler to load or store just the single byte that contains the bit or bits in question.
This indivisibility effect can be specified via aspect Atomic. As a result, all reads and up-
dates of such an object as a whole are indivisible. In practice that means that the entire
object is accessed with one load or store instruction. For a 16-bit object, all 16-bits are
loaded and stored at once. For a 32-bit object, all 32-bits at once, and so on. The up-
per limit is the size of the largest machine scalar that the processor can manipulate with
one instruction, as defined by the target processor. The typical lower bound is 8, for a
byte-addressable machine.
Therefore, within the record type representing a GPIO port, we include the lock register
component and apply the aspect Atomic:
Hence loads and stores to the LCKR component will be done atomically, otherwise the com-
piler will let us know that it is impossible. That's all we need to do for the lock register to
be read and updated atomically.
You should understand that only accesses to the whole, entire object are atomic. In the case
of the lock register, the entire object is a record component, but that causes no problems
here.
There is, however, something we must keep in mind when manipulating the values of atomic
objects. For the lock register we're using a scalar type to represent the register, an unsigned
32-bit integer. There are no sub-components because scalar types don't have components,
by definition. We simply use the bit-level operations to set and clear the individual bits.
But we cannot set the bits — the lock bit and the bit for the I/O pin to freeze — one at a
time because the locking protocol requires all the bits to be written at the same time, and
only the entire 32-bit load and stores are atomic. Likewise, if instead of a scalar we used
a record type or an array type to represent the bits in the lock register, we could not write
individual record or array components one at a time, for the same reason we could not
write individual bits using the unsigned scalar. The Atomic aspect only applies to loads and
stores of the entire register.
Therefore, to update or read individual parts of an atomic object we must use a coding idiom
in which we explicitly read or write the entire object to get to the parts. For example, to read
an individual record component, we'd first read the entire record object into a temporary
variable, and then access the component of that temporary variable. Likewise, to update
one or more individual components, we'd first read the record object into a temporary
variable, update the component or components within that temporary, and then write the
temporary back to the mapped device object. This is known as the "read-modify-write"
idiom. You'll see this idiom often, regardless of the programming language, because the
hardware requirement is not unusual. Fortunately Ada defines another aspect that makes
the compiler do this for us. We'll describe it in the next section.
Finally, there are issues to consider regarding the other aspects described in this section.
If you think about atomic behavior in the context of machine instructions, loading and stor-
ing from/to memory atomically can only be performed for quantities that are independently
addressable. Consequently, all atomic objects are considered to be specified as indepen-
dently addressable too. Aspect specifications and representation items cannot change that
fact. You can expect the compiler to reject any aspect or representation choice that would
prevent this from being true.
Likewise, atomic accesses only make sense on actual memory locations, not registers.
Therefore all atomic objects are volatile objects too, automatically.
However, unlike volatile objects, the components of an atomic object are not automatically
atomic themselves. You'd have to mark these types or objects explicitly, using aspect
Atomic_Components. Unlike Volatile_Components, aspect Atomic_Components is thus
useful.
As is usual with Ada programming, you can rely on the compiler to inform you of problems.
The compiler will reject an attempt to specify Atomic or Atomic_Components for an object
or type if the implementation cannot support the indivisible and independent reads and
updates required.
Many devices have single-bit flags in the hardware that are not allocated to distinct bytes.
They're packed into bytes and words shared with other flags. It isn't just individual bits
either. Multi-bit fields that are smaller than a byte, e.g., two 4-bit quantities packed into a
byte, are common. We saw that with the GPIO alternate functions codes earlier.
Ordinarily in Ada we represent such composite hardware interfaces using a record type.
(Sometimes an array type makes more sense. That doesn't change anything here.) Com-
pared to using bit-patterns, and the resulting bit shifting and masking in the source code, a
record type representation and the resulting "dot notation" for accessing components is far
more readable. It is also more robust because the compiler does all the work of retrieving
these individual bits and bit-fields for us, doing any shifting and masking required in the
generated code. The loads and stores are done by the compiler in whatever manner the
compiler thinks most efficient.
When the hardware device requires atomic accesses to the memory mapped to such flags,
we cannot let the compiler generate whatever width load and store accesses it thinks best.
If full-word access is required, for example, then only loads and stores for full words can
work. Yet aspect Atomic only guarantees that the entire object, in this case the record
object, is loaded and stored indivisibly, via one instruction. The aspect doesn't apply to
reads and updates to individual record components.
In the section on Atomic above, we mentioned that proper access to individual components
of atomic types/objects can be achieved by a "read-modify-write" idiom. In this idiom, to
read a component you first read into a temporary the entire enclosing atomic object. Then
you read the individual component from that temporary variable. Likewise, to update an in-
dividual component, you start with the same approach but then update the component(s)
within the temporary, then store the entire temporary back into the mapped atomic ob-
ject. Applying aspect Atomic to the enclosing object ensures that reading and writing the
temporary will be atomic, as required.
Using bit masks and bit patterns to access logical components as an alternative to a record
type doesn't change the requirement for the idiom.
Consider the STM32F4 DMA device. The device contains a 32-bit stream configuration
register that requires 32-bit reads and writes. We can map that register to an Ada record
type like so:
The "confirming" size clause ensures we have declared the type correctly such that it will
fit into 32-bits. There will also be a record representation clause to ensure the record com-
ponents are located internally as required by the hardware. We don't show that part.
The aspect Atomic is applied to the entire record type, ensuring that the memory mapped
to the hardware register is loaded and stored only as 32-bit quantities. In this example it
isn't that we want the loads and stores to be indivisible. Rather, we want the generated
machine instructions that load and store the object to use 32-bit word instructions, even
if we are only reading or updating a component of the object. That's what the hardware
requires for all accesses.
Next we'd use that type declaration to declare one of the components of an enclosing record
type representing one entire DMA "stream":
Hence any individual DMA stream record object has a component named CR that represents
the corresponding configuration register.
The DMA controllers have multiple streams per unit so we'd declare an array of DMA_Stream
components. This array would then be part of another record type representing a DMA
controller. Objects of the DMA_Controller type would be mapped to memory, thus mapping
the stream configuration registers to memory.
Now, given all that, suppose we want to enable a stream on a given DMA controller. Using
the read-modify-write idiom we would do it like so:
procedure Enable
(Unit : in out DMA_Controller;
Stream : DMA_Stream_Selector)
is
Temp : Stream_Config_Register;
-- these registers require 32-bit accesses, hence the temporary
begin
Temp := Unit.Streams (Stream).CR; -- read entire CR register
Temp.Stream_Enabled := True;
Unit.Streams (Stream).CR := Temp; -- write entire CR register
end Enable;
That works, and of course the procedural interface presented to clients hides the details,
as it should.
To be fair, the bit-pattern approach can express the idiom concisely, as long as you're care-
ful. Here's the C code to enable and disable a selected stream:
#define DMA_SxCR_EN ((uint32_t)0x00000001)
The code reads and writes the entire CR register each time it is referenced so the require-
ment is met.
Nevertheless, the idiom is error-prone. We might forget to use it at all, or we might get it
wrong in one of the very many places where we need to access individual components.
Fortunately, Ada provides a way to have the compiler implement the idiom for us, in the
generated code. Aspect Full_Access_Only specifies that all reads of, or writes to, a com-
ponent are performed by reading and/or writing all of the nearest enclosing full access
object. Hence we add this aspect to the declaration of Stream_Config_Register like so:
type Stream_Config_Register is record
-- ...
Direction : DMA_Data_Transfer_Direction;
P_Flow_Controller : Boolean;
TCI_Enabled : Boolean; -- transfer complete interrupt
HTI_Enabled : Boolean; -- half-transfer complete
TEI_Enabled : Boolean; -- transfer error interrupt
DMEI_Enabled : Boolean; -- direct mode error interrupt
Stream_Enabled : Boolean;
end record
with Atomic, Full_Access_Only, Size => 32;
This code works because the compiler implements the read-modify-write idiom for us in the
generated code.
The aspect Full_Access_Only is new in Ada 2022, and is based on an implementation-
defined aspect that GNAT first defined named Volatile_Full_Access. You'll see that GNAT
aspect throughout the Arm device drivers in the Ada Drivers Library, available here: https:
//github.com/AdaCore/Ada_Drivers_Library. Those drivers were the motivation for the GNAT
aspect.
Unlike the other aspects above, there is no pragma corresponding to the aspect
Full_Access_Only defined by Ada 2022. (There is such a pragma for the GNAT-specific
version named Volatile_Full_Access, as well as an aspect.)
THIRTYNINE
HANDLING INTERRUPTS
39.1 Background
Embedded systems developers offload functionality from the application processor onto
external devices whenever possible. These external devices may be on the same "chip"
as the central processor (e.g., within a System-on-Chip) or they may just be on the same
board, but the point here is that they are not the processor executing the application. Of-
floading work to these other devices enables us to get more functionality implemented in a
target platform that is usually very limited in resources. If the processor has to implement
everything we might miss deadlines or perhaps not fit into the available code space. And,
of course, some specialized functionality may simply require an external device, such as a
sensor.
For a simple example, a motor encoder is a device attached to a motor shaft that can be
used to count the number of full or partial rotations that the shaft has completed. When the
shaft is rotating quickly, the application would need to interact with the encoder frequently
to get an up-to-date count, representing a non-trivial load on the application processor.
There are ways to reduce that load, which we discuss shortly, but by far the simplest and
most efficient approach is to do it all in hardware: use a timer device driven directly by
the encoder. The timer is connected to the encoder such that the encoder signals act like
an external clock driving the timer's internal counter. All the application processor must
do to get the encoder count is query the timer's counter. The timer is almost certainly
memory-mapped, so querying the timer amounts to a memory access.
In some cases, we even offload communication with these external devices onto other
270
external devices. For example, the I2C (Inter-Integrated Circuit) protocol is a popular
two-wire serial protocol for communicating between low-level hardware devices. Individual
bits of the data are sent by driving the data line high and low in time with the clock signal
on the other line. The protocol has been around for a long time and many embedded
devices use it to communicate. We could have the application drive the data line for each
individual bit in the protocol. Known as "bit-banging," that would be a significant load on the
processor when the overall traffic volume is non-trivial. Fortunately, there are dedicated
devices — I2C transceivers — that will implement the protocol for us. To send application
data to another device using the I2C protocol, we just give the transceiver the data and
destination address. The rest is done in the transceiver hardware. Receiving data is of
course also possible. I2C transceivers are ubiquitous because the protocol is so common
among device implementations. A USART271 / UART272 is a similar example.
Having offloaded some of the work, the application must have some way to interact with
the device in order to know what is happening. Maybe the application has requested the
external device perform some service — an analog-to-digital conversion, say — and must
know when that function has completed. Maybe a communications device is receiving
270 https://en.wikipedia.org/wiki/I%C2%B2C
271 https://en.wikipedia.org/wiki/Universal_synchronous_and_asynchronous_receiver-transmitter
272 https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
1203
Learning Ada
incoming data for the application to process. Or maybe that communications device has
completed sending outgoing data and is ready for more to send.
Ultimately, interaction with the external device will be either synchronous or asynchronous,
and has system-level design implications.
For synchronous interaction, the application periodically queries the device, typically a sta-
tus flag or function on the device. Known as "polling," this approach is simple to implement
but wastes cycles when the external device has not yet completed the request. After all, the
point of offloading the work is to allow the application processor to execute other function-
ality. Polling negates that benefit. On the other hand, if the expected time to completion
is extremely short, polling can be sufficiently efficient to make sense.
Usually, there's enough time involved so that polling is undesirable. The external environ-
ment takes time to respond and change state. Maybe a sensor has been designed to wait
passively for something to happen in the external world, and only on the infrequent occur-
rence of that event should the application be notified. Perhaps a switch is to be toggled in
certain circumstances, or an intruder detected. In this case, nothing happens for extended
intervals.
As a consequence of all this, there's a very good chance that the internal processor should
not poll these external devices.
Before we discuss the asynchronous alternative, there's another issue to consider. How-
ever the notification from the external device is implemented, a very quick response from
the internal processor may be required. Think back to that serial port with a USART again.
The USART is responsible for composing the arriving characters (or bytes) from their indi-
vidual incoming bits on the receiving line. When all the bits for a single character have
arrived, what happens next depends on the software design. In the simplest case, the in-
ternal processor copies the single character from the USART to an internal buffer and then
goes back to doing something else while the next full character arrives in the USART. The
response to the USART must be fairly quick because the next incoming character's bits are
arriving. The internal processor must get the current character before it is overwritten by
the next arriving character, otherwise we'll lose data. So we can say that the response to
the notification from the external device must often be very quick.
Now, ideally in the USART case, we would further offload the work from the internal pro-
cessor. Instead of having the processor copy each arriving character from the USART into
an application buffer, we would have another external hardware device — a direct memory
access (DMA)273 device — copy each arriving character from the USART to the buffer. A
DMA device copies data from one location to another, in this case from the address of the
USART's one-character memory-mapped register to the address of the application buffer in
memory. The copy is performed by the DMA hardware so it is extremely fast and costs the
main processor no cycles. But even with this approach, we need to notify the application
that a complete message is ready for processing. We might need to do that quickly so that
enough time remains for the application to process the message content prior to the arrival
of the next message.
Therefore, the general requirement is for an external device to be able to asynchronously
notify the internal processor, and for the notification to be implemented in such a way that
the beginning of the response can be sufficiently and predictably quick.
Fortunately, computers already have such a mechanism: interrupts. The details vary con-
siderably with the hardware architecture, but the overall idea is independent of the ISA274 :
an external event can trigger a response from the processor by becoming "active." The cur-
rent state of the application is temporarily stored, and then an interrupt response routine,
known as an "interrupt handler" is executed. Upon completion of the handler, the original
state of the application is restored and the application continues execution. The time be-
tween the interrupt becoming active and the start of the responding handler execution is
known as the "interrupt latency."
273 https://en.wikipedia.org/wiki/Direct_memory_access
274 https://en.wikipedia.org/wiki/Instruction_set_architecture
Hardware interrupts typically have priorities assigned, depending on the hardware. These
priorities are applied when multiple interrupts are triggered at the same time, to define
the order in which the interrupts are presented and the handlers invoked. The canonical
model is that only higher-priority interrupts can preempt handlers executing in response to
interrupts with lower or equal priority.
Ada defines a model for hardware interrupts and interrupt handling that closely adheres
to the conceptual model described above. If you have experience with interrupt handling,
you will recognize them in the Ada model. One very important point to make about the
Ada facilities is that they are highly portable, so they don't require extensive changes when
moving to an new target computer. Part of that portability is due to the language-defined
model.
Before we go into the Ada facility details, there's a final point. Sometimes we do want
the application to wait for the external device. When would that be the case? To answer
that, we need to introduce another term. The act of saving and restoring the state of the
interrupted application software is known as "interrupt context switching." If the time for the
device to complete the application request is approximately that of the context switching,
the application might as well wait for the device after issuing the request.
Another reason to consider polling is that the architectural complexity of interrupt handling
is greater than that of polling. If your system has some number of devices to control and
polling them would be fast enough for the application to meet requirements, it is simpler
to do so. But that will likely only work for a few devices, or at least a few that have short
response time requirements.
The application code can wait for the device by simply entering a loop, exiting only when
some external device status flag indicates completion of the function. The loop itself, in
its simplest form, would contain only the test for exiting. As mentioned earlier, polling in a
tight loop like this only makes sense for very fast device interactions. That's not the usual
situation though, so polling should not be your default design assumption. Besides, active
polling consumes power. On an embedded platform, conserving power is often important.
That loop polling the device will never exit if the device can fail to signal completion. Or
maybe it might take too long in some odd case. If you don't want to be potentially stuck
in the loop indefinitely, chewing up cycles and power, you can add an upper bound on the
number of attempts, i.e., loop iterations. For example:
In the above, Data_Status is a function that returns a record object containing Boolean
flags. The if-statement queries one of those flags. Thus the loop either detects the de-
sired device status or raises an exception after the maximum number of attempts have
been made. In this version, the maximum is a known upper bound so a local constant will
suffice. The maximum could be passed as a parameter instead, or declared in a global
"configuration" package containing such constants.
Presumably, the upper bound on the attempts is either specified by the device documen-
tation or empirically determined. Sometimes, however, the documentation will instead
specify a maximum possible response time, for instance 30 milliseconds. Any time beyond
In this approach, we compute the deadline as a point on the timeline by adding the value re-
turned from the Clock function (i.e., "now") to the time interval specified by the parameter.
Then, within the loop, we compare the value of the Clock to that deadline.
Finally, with another design approach we can reduce the processor cycles "wasted" when
the polled device is not yet ready. Specifically, in the polling loop, when the device has
not yet completed the requested function, we can temporarily relinquish the processor so
that other tasks within the application can execute. That isn't perfect because we're still
checking the device status even though we cannot exit the loop. And it requires other tasks
to exist in your design, although that's probably a good idea for other reasons (e.g., logical
threads having different, non-harmonic periods). This approach would look like this (an
incomplete example):
procedure Poll_With_Delay is
Next_Release : Time;
Period : constant Time_Span := Milliseconds (30); -- let's say
begin
Next_Release := Clock;
loop
exit when Status (...);
(continues on next page)
The code above will check the status of some device every 30 milliseconds (an arbitrary
period just for illustration) until the Status function result allows the loop to exit. If the
device "hangs" the loop is never exited, but as you saw there are ways to address that
possibility. When the code does not exit the loop, the next point on the timeline is computed
and the task executing the code then suspends, allowing the other tasks in the application
to execute. Eventually, the next release point is reached and so the task becomes ready to
execute again (and will, subject to priorities).
But how long should the polling task suspend when awaiting the device? We need to sus-
pend long enough for the other tasks to get something done, but not so long that the de-
vice isn't handled fast enough. Finding the right balance is often not simple, and is further
complicated by the "task switching" time. That's the time it takes to switch the execution
context from one task to another, in this case in response to the "delay until" statement
suspending the polling task. And it must be considered in both directions: when the delay
expires we'll eventually switch back to the polling task.
As you can see, polling is easily expressed but has potentially significant drawbacks and
architectural ramifications so it should be avoided as a default approach.
Now let's explore the Ada interrupt facilities.
The Ada language standard defines a model for hardware interrupts, as well as language-
defined mechanisms for handling interrupts consistent with that model. The model is de-
fined in Annex C, the "Systems Programming" annex, section 3 "Interrupt Support." The
following is the text of that section with only a few simplifications and elisions.
• Interrupts are said to occur. An occurrence of an interrupt is separable into generation
and delivery.
– Generation of an interrupt is the event in the underlying hardware or system that
makes the interrupt available to the program.
– Delivery is the action that invokes part of the program as response to the interrupt
occurrence.
• Between generation and delivery, the interrupt occurrence is pending.
• Some or all interrupts may be blocked. When an interrupt is blocked, all occurrences
of that interrupt are prevented from being delivered.
• Certain interrupts are reserved. A reserved interrupt is either an interrupt for which
user-defined handlers are not supported, or one which already has an attached handler
by some other RTL-defined means. The set of reserved interrupts is determined by the
hardware and run-time library (RTL).
• Program units can be connected to non-reserved interrupts. While connected, the
program unit is said to be attached to that interrupt. The execution of that program
unit, the interrupt handler, is invoked upon delivery of the interrupt occurrence.
• While a handler is attached to an interrupt, it is called once for each delivered occur-
rence of that interrupt.
• The corresponding interrupt is blocked while the handler executes. While an inter-
rupt is blocked, all occurrences of that interrupt are prevented from being delivered.
Whether such occurrences remain pending or are lost is determined by the hardware
and the RTL.
• Each interrupt has a default treatment which determines the system's response to
an occurrence of that interrupt when no user-defined handler is attached. The set of
possible default treatments is defined by the RTL.
• An exception propagated from a handler that is invoked by an interrupt has no effect.
In particular, it is not propagated out of the handler, in the same way that exceptions
do not propagate outside of task bodies.
• If the Ceiling_Locking policy is in effect, the interrupt handler executes with the
active priority that is the ceiling priority of the corresponding protected object. ("Pro-
tected object" is abbreviated as "PO" for convenience).
• If the hardware or the underlying system holds pending interrupt occurrences, the RTL
must provide for later delivery of these occurrences to the program.
(The above is not everything in the model but we can ignore the rest in this introduction.)
Because interrupt occurrences are generated by the hardware and delivered by the under-
lying system software (run-time library or real-time operating system), the application code
is mainly responsible for responding to occurrences. Of course, the application must first
configure the relevant external devices so that they generate the expected interrupts.
The actual response is application-specific but is also hardware-specific. The latter often
(but not always) requires clearing the interrupt status within the generating device so that
the same occurrence is not delivered again.
Furthermore, the standard model requires the underlying software to block further occur-
rences while the handler executes, and only allow preemption by higher-priory interrupt
occurrences (if any). The application handlers are not responsible for these semantics ei-
ther. As you will see, the choice of program unit used for expressing handlers makes this
all very convenient for the developer.
As a consequence, in terms of the response, the application developer must write the spe-
cific handlers and attach those handlers to the corresponding interrupts. Attaching the
handlers is implemented in the underlying system software, and it is this same underlying
software that delivers the occurrences.
We will now explore the Ada facilities in detail. At the end of this chapter we will explore
some common idioms using these mechanisms, especially with regard to the handlers'
interaction with the rest of the application.
Interrupt handling is, by definition, asynchronous: some event occurs that causes the pro-
cessor to suspend the application, respond to the event, and then resume application exe-
cution.
Because these events are asynchronous, the actions performed by the interrupt handler and
the application are subject to the same sorts of race conditions as multiple tasks acting on
shared data.
For example, a "reader" task may be in the act of reading (copying) the value of some shared
variable, only to be preempted by a "writer" task that updates the value of the variable. In
that case, when the "reader" task resumes execution, it will finish the read operation but
will, as a result, have a value that is partly from the old value and partly from the new value.
The effect is unpredictable. An interrupt handler can have the same effect on shared data
as the preempting "writer" task that interrupts the "reader" task. This problem is possible
for shared data of any type that is not atomically read or written. You can think of large
record objects if that helps, but it even applies to some scalars.
That scenario applies even if no explicit tasks are declared in the application. That's be-
cause an implicit "environment task" is executing the main subprogram. In that case, the
main subprogram is the entire application, but more typically some non-null application
code is actively executing in one or more tasks.
But it's not just a matter of tasks. We said that interrupts usually have priorities. Typically
that means a higher-priority interrupt will preempt the execution of the handler for a lower-
priority interrupt. It's the same issue.
Furthermore, the fact that an interrupt has occurred needs to be communicated to the
application, for example to say that updated data are available, perhaps a sensor reading
or characters from a serial port. As we said above, we usually don't want to poll for that fact,
so the application must be able to suspend until the event has occurred. Often we'll have a
dedicated task within the application that suspends, rather than the entire application, but
that's an application detail.
Ada's protected objects address all these asynchronous issues. Shared data declared within
a protected object can be accessed only via protected procedures or protected entries, both
of which execute with mutually exclusive access. Hence no race conditions are possible.
Here is an extremely simple, but realistic, example of a PO. This is not an interrupt handler
example — we'll get to that — but it does show a shared variable and a protected procedure
that executes with mutually exclusive access no matter how many tasks concurrently call
it. The PO provides unique serial numbers.
protected Serial_Number is
procedure Get_Next (Number : out Positive);
private
Value : Positive := 1;
end Serial_Number;
end Serial_Number;
Imagine there are multiple assembly lines creating devices of various sorts. Each de-
vice gets a unique serial number. These assembly lines run concurrently, so the calls to
Get_Next occur concurrently. Without mutually exclusive access to the Value variable,
multiple devices could get the same serial number.
Protected entries can suspend a caller until some condition is true; in this case, the fact
that an interrupt has occurred and been handled. (As we will see, a protected entry is not
the only way to synchronize with an accessing task, but it is the most robust and general.)
Here's an example of a PO with a protected entry:
protected type Persistent_Signal is
entry Wait;
procedure Send;
private
Signal_Arrived : Boolean := False;
end Persistent_Signal;
procedure Send is
begin
Signal_Arrived := True;
end Send;
end Persistent_Signal;
This is a PO providing a "Persistent Signal" abstraction. It allows a task to wait for a "signal"
from another task. The signal is not lost if the receiving task is not already waiting, hence
the term "persistent." Specifically, if Signal_Arrived is False, a caller to Wait will be
suspended until Signal_Arrived becomes True. A caller to Send sets Signal_Arrived to
True. If a caller to Wait was already present, suspended, it will be allowed to continue
execution. If no caller was waiting, eventually some caller will arrive, find Signal_Arrived
True, and will be allowed to continue. In either case, the Signal_Arrived flag will be
set back to False before the Wait caller is released. Protected objects can have a priority
assigned, similar to tasks, so they are integrated into the global priority semantics including
interrupt priorities.
Therefore, in Ada an interrupt handler is a protected procedure declared within some pro-
tected object (PO). A given PO may handle more than one interrupt, and if so, may use one
or more protected procedures to do so.
Interrupts can be attached to a protected procedure handler using a mechanism we'll dis-
cuss shortly. When the corresponding interrupt occurs, the attached handler is invoked.
Any exceptions propagated by the handler's execution are ignored and do not go past the
procedure.
While the protected procedure handler executes, the corresponding interrupt is blocked. As
a consequence, another occurrence of that same interrupt will not preempt the handler's
execution. However, if the hardware does not allow interrupts to be blocked, no blocking
occurs and a subsequent occurrence would preempt the current execution of the handler.
In that case, your handlers must be written with that possibility in mind. Most targets do
block interrupts so we will assume that behavior in the following descriptions.
The standard mutually exclusive access provided to the execution of protected procedures
and entries is enforced whether the "call" originates in hardware, via an interrupt, or in
the application software, via some task. While any protected action in the PO executes,
the corresponding interrupt is blocked, such that another occurrence will not preempt the
execution of that actions' procedure or entry body execution in the PO.
On some processors blocked interrupts are lost, they do not persist. However, if the hard-
ware can deliver an interrupt that had been blocked, the Systems Programming Annex re-
quires the handler to be invoked again later, subject to the PO semantics described above.
The default treatment for a given interrupt depends on the RTL implementation. The default
may be to jump immediately to system-defined handler that merely loops forever, thereby
"hanging" the system and preventing any further execution of the application. On a bare-
board target that would be a very common approach. Alternatively the default could be to
ignore the interrupt entirely.
As mentioned earlier, some interrupts may be reserved, meaning that the application can-
not install a replacement handler. For instance, most bare-board systems include a clock
that is driven by a dedicated interrupt. The application cannot (or at least should not)
override the interrupt handler for that interrupt. The determination of which interrupts
are reserved is RTL-defined. Attempting to attach a user-defined handler for a reserved
interrupt raises Program_Error, and the existing treatment is unchanged.
Ada defines a standard package that provides a primary type for identifying individual in-
terrupts, as well as subprograms that take a parameter of that type in order to manage the
system's interrupts and handlers. The package is named Ada.Interrupts, appropriately.
The primary type in that package is named Interrupt_Id and is an compiler-defined dis-
crete type, meaning that it is either an integer type (signed or not) or an enumeration type.
That representation is guaranteed so you can be sure that Interrupt_Id can be used, for
example, as the index for an array type.
Package Ada.Interrupts provides functions to query whether a given interrupt is reserved,
or if an interrupt has a handler attached. Procedures are defined to allow the application to
attach and detach handlers, among other things. These procedures allow the application
to dynamically manage interrupts. For example, when a new external device is added,
perhaps as a "hot spare" replacing a damaged device, or when a new external device is
simply connected to the target, the application can arrange to handle the new interrupts
without having to recompile the application or restart application execution.
However, typically you will not use these procedures or functions to manage interrupts. In
part that's because the architecture is usually static, i.e., the handlers are set up once and
then never changed. In that case you won't need to query whether a given exception is
reserved at run-time, or to check whether a handler is attached. You'd know that already,
as part of the system architecture choices. For the same reasons, another mechanism
for attaching handlers is more commonly used, and will be explained in that section. The
package's type Interrupt_Id, however, will be used extensively.
A child package Ada.Interrupts.Names defines a target-dependent set of constants pro-
viding meaningful names for the Interrupt_Id values the target supports. Both the num-
ber of constants and their names are defined by the compiler, reflecting the variations in
hardware available. This package and the enclosed constants are used all the time. For the
sake of illustration, here is part of the package declaration for a Cortex M4F microcontroller
supported by GNAT:
package Ada.Interrupts.Names is
Sys_Tick_Interrupt : constant Interrupt_ID := 1;
...
EXTI0_Interrupt : constant Interrupt_ID := 8;
....
DMA1_Stream0_Interrupt : constant Interrupt_ID := 13;
...
HASH_RNG_Interrupt : constant Interrupt_ID := 80;
...
end Ada.Interrupts.Names;
Notice HASH_RNG_Interrupt, the name for Interrupt_Id value 80 on this target. That is
the interrupt that the on-chip random number generator hardware uses to signal that a new
value is available. We will use this interrupt in an example at the end of this chapter.
The representation chosen by the compiler for Interrupt_Id is very likely an integer, as in
the above package, so the child package provides readable names for the numeric values.
If Interrupt_Id is represented as an enumeration type the enumeral values are probably
sufficiently readable, but the child package must be provided by the vendor nonetheless.
As we mentioned above, the Ada standard provides two ways to attach handlers to inter-
rupts. One is procedural, described earlier. The other mechanism is automatic, achieved
during elaboration of the protected object enclosing the handler procedure. The behavior
is not unlike the activation of tasks: declared tasks are activated automatically as a result
of their elaboration, whereas dynamically allocated tasks are activated as a result of their
allocations.
We will focus exclusively on the automatic, elaboration-driven attachment model because
that is the more common usage, and as a result, that is what GNAT supports on bare-board
targets. It is also the mechanism that the standard Ravenscar and Jorvik profiles require.
Our examples are consistent with those targets.
In the elaboration-based attachment model, we specify the interrupt to be attached to a
given protected procedure within a protected object. This interrupt specification occurs
within the enclosing protected object declaration. (Details in a moment.) When the en-
closing PO is elaborated, the run-time library installs that procedure as the handler for that
interrupt. A given PO may contain one or more interrupt handler procedures, as well as any
other protected subprograms and entries.
In particular, we can associate an interrupt with a protected procedure by applying the
aspect Attach_Handler to that procedure as part of its declaration, with the Interrupt_Id
value as the aspect parameter. The association can also be achieved via a pragma with the
same name as the aspect. Strictly speaking, the pragma Attach_Handler is obsolescent,
but that just means that there is a newer way to make the association (i.e., the aspect). The
pragma is not illegal and will remain supported. Because the pragma existed in a version
of Ada prior to aspects you will see a lot of existing code using the pragma. You should
become familiar with it. There's no language-driven reason to change the source code to
use the aspect. New code should arguably use the aspect, but there's no technical reason
to prefer one over the other.
Here is an example of a protected object with one protected procedure interrupt handler.
It uses the Attach_Handler aspect to tie a random number generator interrupt to the
RNG_Controller.Interrupt_Handler procedure:
protected RNG_Controller is
...
entry Get_Random (Value : out UInt32);
private
Last_Sample : UInt32 := 0;
Buffer : Ring_Buffer;
Data_Available : Boolean := False;
end RNG_Controller;
That's all that the developer must do to install the handler. The compiler and run-time
library do the rest, automatically.
The local variables are declared in the private part, as required by the language, because
they are shared data meant to be protected from race conditions. Therefore, the only
compile-time access possible is via visible subprograms and entries declare in the visible
part. Those subprograms and entries execute with mutually exclusive access so no race
conditions are possible, as guaranteed by the language.
Note that procedure Interrupt_Handler is declared in the private part of RNG_Controller,
rather than the visible part. That location is purely a matter of choice (unlike the variables),
but there is a good reason to hide it: application software can call an interrupt handler
procedure too. If you don't ever intend for that to happen, have the compiler enforce your
intent. An alert code reader will then recognize that clients cannot call that procedure. If,
on the other hand, the handler is declared in the visible part, the reader must examine more
of the code to determine whether there are any callers in the application code. Granted, a
software call to an interrupt handler is rare, but not illegal, so you should state your intent
in the code in an enforceable manner.
Be aware that the Ada compiler is allowed to place restrictions on protected procedure
handlers. The compiler can restrict the content of the procedure body, for example, or it
might forbid calls to the handler from the application software. The rationale is to allow
direct invocation by the hardware, to minimize interrupt latency to the extent possible.
For completeness, here's the same RNG_Controller protected object using the pragma
instead of the aspect to attach the interrupt to the handler procedure:
protected RNG_Controller is
...
entry Get_Random (Value : out UInt32);
private
Last_Sample : UInt32 := 0;
Buffer : Ring_Buffer;
Data_Available : Boolean := False;
procedure Interrupt_Handler;
pragma Attach_Handler (Interrupt_Handler,
Ada.Interrupts.Names.HASH_RNG_Interrupt;
end RNG_Controller;
As you can see, there isn't much difference. The aspect is somewhat more succinct. (The
choice of where to declare the procedure remains the same.)
In this attachment model, protected declarations containing interrupt handlers must be de-
clared at the library level. That means they must be declared in library packages. (Protected
objects cannot be library units themselves, just as tasks cannot. They must be declared
within some other unit.) Here is the full declaration for the RNG_Controller PO declared
within a package — in this case within a package body:
with Ada.Interrupts.Names;
with Bounded_Ring_Buffers;
protected RNG_Controller is
...
entry Get_Random (Value : out UInt32);
private
Last_Sample : UInt32 := 0;
Samples : Ring_Buffer (Upper_Bound => 9); -- arbitrary
Data_Available : Boolean := False;
end RNG_Controller;
(continues on next page)
...
end STM32.RNG.Interrupts;
But note that we're talking about protected declarations, a technical term that en-
compasses not only protected types but also anonymously-typed protected objects. In
the RNG_Controller example, the PO does not have an explicit type declared; it is
anonymously-typed. (Task objects can also be anonymously-typed.) You don't have to
use a two-step process of first declaring the type and then an object of the type. If you only
need one, no explicit type is required.
Although interrupt handler protected types must be declared at library level, the Ada model
allows you to have an object of the type declared elsewhere, not necessarily at library level.
However, note that the Ravenscar and Jorvik profiles require protected interrupt handler
objects — anonymously-typed or not — to be declared at the library level too, for the sake
of analysis. The profiles also require the elaboration-based attachment mechanism we
have shown. For the sake of the widest applicability, and because with GNAT the most
likely use-case involves either Ravenscar or Jorvik, we are following those restrictions in
our examples.
Many (but not all) processors assign priorities to interrupts, with blocking and preemption
among priorities of different levels, much like preemptive priority-based task semantics.
Consequently, the priority semantics for interrupt handlers are as if a hardware "task,"
executing at an interrupt level priority, calls the protected procedure handler.
Interrupt handlers in Ada are protected procedures, which do not have priorities individually,
but the enclosing protected object can be assigned a priority that will apply to the handler(s)
when executing.
Therefore, protected objects can have priorities assigned using values of subtype System.
Interrupt_Priority, which are high enough to require the blocking of one or more inter-
rupts. The specific values among the priority subtypes are not standardized but the intent
is that interrupt priorities are higher (more urgent) than non-interrupt priorities, as if they
are declared like so in package System:
For example, here are the subtype declarations in the GNAT compiler for an Arm Cortex M4
target:
Although the ranges are compiler-defined, when the Systems Programming Annex is imple-
mented the range of System.Interrupt_Priority must include at least one value. Ven-
dors are not required to have a distinct priority value in Interrupt_Priority for each
hardware interrupt possible on a given target. On a bare-metal target, they probably will
have a one-to-one correspondence, but might not in a target with an RTOS or host OS.
A PO containing an interrupt handler procedure must be given a priority within the Inter-
rupt_Priority subtype's range. To do so, we apply the aspect Interrupt_Priority to
the PO. Perhaps confusingly, the aspect and the value's required subtype have the same
name.
package Gyro_Interrupts is
end Gyro_Interrupts;
The code above uses the highest (most urgent) interrupt priority value but some other
value could be used instead, as long as it is in the Interrupt_Priority subtype's range.
Constraint_Error is raised otherwise.
There is also an alternative pragma, now obsolescent, with the same name as the aspect
and subtype. Here is an example:
package Gyro_Interrupts is
protected Handler is
pragma Interrupt_Priority (245);
private
procedure IRQ_Handler;
pragma Attach_Handler (IRQ_Handler, EXTI2_Interrupt);
end Handler;
end Gyro_Interrupts;
In the above we set the interrupt priority to 245, presumably a value conformant with this
specific target. You should be familiar with this pragma too, because there is some much
existing code using it. New code should use the aspect, ideally.
If we don't specify the priority for some protected object containing an interrupt handler
(using either the pragma or the aspect), the initial priority of protected objects of that type
is compiler-defined, but within the range of the subtype Interrupt_Priority. Generally
speaking, you should specify the priorities per those of the interrupts handled, assuming
they have distinct values, so that you can reason concretely about the relative blocking
behavior at run-time.
Note that the parameter specifying the priority is optional for the Interrupt_Priority
pragma. When none is given, the effect is as if the value Interrupt_Priority'Last was
specified.
package Gyro_Interrupts is
end Gyro_Interrupts;
In this section we explore some of the common idioms used when writing interrupt handlers
in Ada.
Suppose we have more than one instance of a kind of device. For example, multiple DMA
controllers are often available on a System-on-Chip such as an Arm microcontroller. We
can simplify our code by defining a device driver type, with one object of the type per
supported hardware device. This is the same abstract data type (ADT) approach we'd take
for software objects in application code, and in general for device drivers when multiple
hardware instances are available.
We can also apply the ADT approach to interrupt handlers when we have multiple devices
of a given kind that can generate interrupts. In this case, the type will be fully implemented
as a protected type containing at least one interrupt handling procedure, with or without
additional protected procedures or entries.
As is the case with abstract data types in general, we can tailor each object with discrim-
inants defined with the type, in order to "parameterize" the type and thus allow distinct
objects to have different characteristics. For example, we might define a bounded buffer
ADT with a discriminant specifying the upper bound, so that distinct objects of the sin-
gle type could have different bounds. In the case of hardware device instances, one of
these parameters will often specify the device being driven, but we can also specify other
device-specific characteristics. In particular, for interrupt handler types both the interrupt
to handle and the interrupt priority can be discriminants. That's possible because the as-
pects/pragmas do not require their values to be specified via literals, unlike what was done
in the RNG_Controller example above.
For example, here is the declaration for an interrupt handler ADT named
DMA_Interrupt_Controller. This type manages the interrupts for a given DMA de-
vice, known as a DMA_Controller. Type DMA_Controller is itself an abstract data type,
declared elsewhere.
protected type DMA_Interrupt_Controller
(Controller : not null access DMA_Controller;
Stream : DMA_Stream_Selector;
IRQ : Ada.Interrupts.Interrupt_Id;
IRQ_Priority : System.Interrupt_Priority)
with
Interrupt_Priority => IRQ_Priority
is
procedure Start_Transfer
(Source : Address;
Destination : Address;
Data_Count : UInt16);
procedure Clear_Transfer_State;
private
end DMA_Interrupt_Controller;
In the above, the Controller discriminant provides an access value designating the spe-
cific DMA_Controller device instance to be managed. Each DMA device supports multiple
independent conversion "streams" so the Stream discriminant specifies that characteristic.
The IRQ and IRQ_Priority discriminants specify the handler values for that specific device
and stream. These discriminant values are then used in the Interrupt_Priority pragma
and the Attach_Handler aspect in the private part. ("IRQ" is a command handler name
across programming languages, and is an abbreviation for "interrupt request.")
Here then are the declarations for two instances of the interrupt handler type:
DMA2_Stream0 : DMA_Interrupt_Controller
(Controller => DMA_2'Access,
Stream => Stream_0,
IRQ => DMA2_Stream0_Interrupt,
IRQ_Priority => Interrupt_Priority'Last);
DMA2_Stream5 : DMA_Interrupt_Controller
(Controller => DMA_2'Access,
Stream => Stream_5,
IRQ => DMA2_Stream5_Interrupt,
IRQ_Priority => Interrupt_Priority'Last);
In the above, both objects DMA2_Stream0 and DMA2_Stream5 are associated with the same
object named DMA2, an instance of the DMA_Controller type. The difference in the objects
is the stream that generates the interrupts they handle. One object handles Stream_0
interrupts and the other handles those from Stream_5. Package Ada.Interrupts.Names
for this target (for GNAT) declares distinct names for the streams and devices generating
the interrupts, hence DMA2_Stream0_Interrupt and DMA2_Stream5_Interrupt.
On both objects the priority is the highest interrupt priority (and hence the highest overall),
Interrupt_Priority'Last. That will work, but of course all interrupts will be blocked
during the execution of the handler, as well as the execution of any other subprogram or
entry in the same PO. That means that the clock interrupt is blocked for that interval, for
example. We use that interrupt value in our demonstrations for expedience, but in a real
application you'd almost certainly use a lower value specific to the interrupt handled.
We could reduce the number of discriminants, and also make the code more robust, by
taking advantage of the requirement that type Interrupt_Id be a discrete type. As such,
it can be used as the index type into arrays. Here is a driver example with only the Inter-
rupt_Id discriminant required:
Now we use the one IRQ discriminant both to assign the priorities for distinct objects and
Interrupt handlers are intended to be very brief, in part because they prevent lower priority
interrupts and application tasks from executing.
However, complete interrupt processing may require more than just the short protected
procedure handler’s activity. Therefore, two levels of handling are common: the protected
procedure interrupt handler and a task. The handler does the least possible and then signals
the task to do the rest.
Of course, sometimes the handler does everything required and just needs to signal the
application. In that case, the awakened task does no further "interrupt processing" but
simply uses the result.
Regardless, the same issues apply: 1) How do application tasks synchronize with the han-
dlers? Assuming the task is not polling the event, at some point the task must stop what
it was doing and suspend, waiting for the handler to signal it. 2) Once synchronized, how
can the handlers pass data to the tasks?
Using protected objects for interrupt handling provides an efficient mechanism that ele-
gantly addresses both issues. In addition, when data communication is not required, an-
other standard language mechanism is available. These give rise to two design idioms. We
will explore both.
In the first idiom, the protected object contains a protected entry as well as the interrupt
handler procedure. The task suspends on the entry when ready for the handler results,
controlled by the barrier condition as usual. The protected handler procedure responds to
interrupts, managing data (if any) as required. When ready, based on what the handler
does, the handler sets the entry barrier to True. That allows the suspended task to execute
the entry body. The entry body can do whatever is required, possibly just copying the
local protected data to the entry parameters. Of course, the entry may be used purely for
synchronizing with the handler, i.e., suspending and resuming the task, in which case there
would be no parameters passed.
The image below depicts this design.
procedure Start_Transfer
(Source : Address;
Destination : Address;
Data_Count : UInt16);
procedure Clear_Transfer_State;
private
end DMA_Interrupt_Controller;
The client application code (task) calls procedure Start_Transfer to initiate the DMA trans-
action, then presumably goes off to accomplish something else, and eventually calls the
Wait_For_Completion entry. That call blocks the task if the device has not yet completed
the DMA transfer. The interrupt handler procedure, cleverly named Interrupt_Handler,
handles the interrupts, one of which indicates that the transfer has completed. Device er-
rors also generate interrupts so the handler detects them and acts accordingly. Eventually,
the handler sets the barrier to True and the task can get the status via the entry parameter.
procedure Start_Transfer
(Source : Address;
Destination : Address;
Data_Count : UInt16)
is
begin
No_Transfer_In_Progess := False;
Had_Buffer_Error := False;
Clear_All_Status (Controller.all, Stream);
Start_Transfer_with_Interrupts
(Controller.all,
Stream,
Source,
...,
Enabled_Interrupts =>
(Half_Transfer_Complete_Interrupt => False,
others => True));
end Start_Transfer;
entry Wait_For_Completion
(Status : out DMA_Error_Code)
when
No_Transfer_In_Progress
is
begin
Status := Last_Status;
end Wait_For_Completion;
In the above, the entry barrier consists of the Boolean variable No_Transfer_In_Progress.
Procedure Start_Transfer first sets that variable to False so that a caller to
Wait_For_Completion will suspend until the transaction completes one way or the other.
Eventually, the handler sets No_Transfer_In_Progress to True.
procedure Interrupt_Handler is
subtype Checked_Status_Flag is DMA_Status_Flag with
Static_Predicate => Checked_Status_Flag /= Half_Transfer_Complete_Indicated;
begin
for Flag in Checked_Status_Flag loop
if Status (Controller.all, Stream, Flag) then
case Flag is
when FIFO_Error_Indicated =>
Last_Status := DMA_FIFO_Error;
Had_Buffer_Error := True;
No_Transfer_In_Progess := not Enabled (Controller.all, Stream);
when Direct_Mode_Error_Indicated =>
Last_Status := DMA_Direct_Mode_Error;
No_Transfer_In_Progess := not Enabled (Controller.all, Stream);
when Transfer_Error_Indicated =>
Last_Status := DMA_Transfer_Error;
No_Transfer_In_Progess := True;
when Transfer_Complete_Indicated =>
Last_Status := DMA_No_Error;
No_Transfer_In_Progess := True;
end case;
Clear_Status (Controller.all, Stream, Flag);
end if;
end loop;
(continues on next page)
This device driver doesn't bother with interrupts indicating that transfers are half-way
complete so that specific status flag is ignored. In response to an interrupt, the handler
checks each status flag to determine what happened. Note the resulting assignments for
both the protected variables Last_Status and No_Transfer_In_Progess. The variable
No_Transfer_In_Progess controls the entry, and Last_Status is passed to the caller via
the entry formal parameter. When the interrupt handler exits, the resulting protected ac-
tion allows the now-enabled entry call to execute.
In the second design idiom, the handler again synchronizes with the application task, but
not using a protected entry.
The image below depicts this design.
In this approach, the task synchronizes with the handler using a Suspension_Object vari-
able. The type Suspension_Object is defined in the language standard package Ada.
Synchronous_Task_Control. Essentially, the type provides a thread-safe Boolean flag.
Callers can suspend themselves (hence the package name) until another task resumes
them by setting the flag to True. Here's the package declaration, somewhat elided:
package Ada.Synchronous_Task_Control is
private
...
end Ada.Synchronous_Task_Control;
Tasks call Suspend_Until_True to suspend themselves on some object of the type passed
as the parameter. The call suspends the caller until that object becomes True. If it is
already True, the caller continues immediately. Objects of type Suspension_Object are
automatically set to False initially, and become True via a call to Set_True. As part of the
return from a call to Suspend_Until_True, the flag is set back to False. As a result, you
probably only need those two subprograms.
The interrupt handler procedure responds to interrupts, eventually setting some visible
Suspension_Object to True so that the caller will be signaled and resume. Here's an ex-
ample showing both the protected object, with handler, and a Suspension_Object decla-
ration:
package Gyro_Interrupts is
Data_Available : Suspension_Object;
protected Handler is
pragma Interrupt_Priority;
private
procedure IRQ_Handler
with Attach_Handler => EXTI2_Interrupt;
end Handler;
end Gyro_Interrupts;
procedure IRQ_Handler is
begin
if External_Interrupt_Pending (EXTI_Line_2) then
Clear_External_Interrupt (EXTI_Line_2);
Set_True (Data_Available);
end if;
end IRQ_Handler;
end Handler;
end Gyro_Interrupts;
The handler simply clears the interrupt and resumes the caller task via a call to Set_True
on the variable declared in the package spec.
The lack of an entry means that no data can be passed to the task via entry parameters.
It is possible to pass data to the task but doing so would require an additional protected
procedure or function.
275 https://www.st.com/en/mems-and-sensors/l3gd20.html
The gyroscope hardware device interface is in package L3GD20. Here are the pertinent
parts:
package L3GD20 is
procedure Initialize
(This : in out Three_Axis_Gyroscope;
Port : Any_SPI_Port;
Chip_Select : Any_GPIO_Point);
...
...
...
procedure Get_Raw_Angle_Rates
(This : Three_Axis_Gyroscope;
Rates : out Angle_Rates);
...
end L3GD20;
With those packages available, we can write a simple main program to use the gyro. The
real demo displayed the readings on an LCD but we've elided all those irrelevant details:
with Gyro_Interrupts;
with Ada.Synchronous_Task_Control; use Ada.Synchronous_Task_Control;
with L3GD20; use L3GD20;
with STM32.Board;
...
procedure Demo_L3GD20 is
Axes : L3GD20.Angle_Rates;
...
...
begin
Configure_Gyro;
Configure_Gyro_Interrupt;
(continues on next page)
The demo is a main procedure, even though we've been describing the client application
code in terms of tasks. The main procedure is executed by the implicit "environment task"
so it all still works. Await_Raw_Angle_Rates suspends (if necessary) on Gyro_Interrupts.
Data_Available and then calls L3GD20.Get_Raw_Angle_Rates to get the rate values.
The operations provided by Suspension_Object are faster than protected entries, and
noticeably so. However, that performance difference is due to the fact that Suspen-
sion_Object provides so much less capability than entries. In particular, there is no notion
of protected actions, nor expressive entry barriers for condition synchronization, nor pa-
rameters to pass data while synchronized. Most importantly, there is no caller queue, so at
most one caller can be waiting at a time on any given Suspension_Object variable. You'll
get Program_Error if you try. Protected entries should be your first design choice. Note
that the Ravenscar restrictions can make use of Suspension_Object much more likely.
As you can see, the semantics of protected objects are a good fit for interrupt handling.
However, other forms of handlers are allowed to be supported. For example, the compiler
and RTL for a specific target may include support for interrupts generated by a device known
to be available with that target. For illustration, let's imagine the target always has a serial
port backed by a UART. In addition to handlers as protected procedure without parameters,
perhaps the compiler and RTL support interrupt handlers with a single parameter of type
Unsigned_8 (or larger) as supported by the UART.
Overall, the interrupt model defined and supported by Ada is quite close to the canonical
model presented by most programming languages, in part because it matches the model
presented by typical hardware.
FORTY
CONCLUSION
1227
Learning Ada
1229
Learning Ada
This course presents an overview of the new features of the latest Ada 2022 standard.
This document was written by Maxim Reznik and reviewed by Richard Kenner.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn web-
site277 . The directory structure in the zip file is based on the code block metadata. For
example, if you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
1. Unpack the zip file;
2. Go to target directory;
3. Start GNAT Studio on this directory;
4. Build (or compile) the project;
5. Run the application (if a main procedure is available in the project).
276 http://creativecommons.org/licenses/by-sa/4.0
277 https://learn.adacore.com/zip/learning-ada_code.zip
1231
Learning Ada
1232
CHAPTER
FORTYONE
INTRODUCTION
This is a collection of short code examples demonstrating new features of the Ada 2022
Standard278 as they are implemented in GNAT Ada compiler.
To use some of these features, you may need to use a compiler command line switch
or pragma. Compilers starting with GNAT Community Edition 2021279 or GCC 11280 use
pragma Ada_2022; or the -gnat2022 switch. Older compilers use pragma Ada_2020; or
-gnat2020. To use the square brackets syntax or 'Reduce expressions, you need pragma
Extensions_Allowed (On); or the -gnatX switch.
41.1 References
278 http://www.ada-auth.org/standards/22aarm/html/AA-TTL.html
279 https://blog.adacore.com/gnat-community-2021-is-here
280 https://gcc.gnu.org/gcc-11/
281 http://www.ada-auth.org/standards/22aarm/html/AA-TTL.html
282 https://blog.adacore.com/ada-202x-support-in-gnat
1233
Learning Ada
FORTYTWO
Since the publication of the Technical Corrigendum 1283 in February 2016, the 'Image at-
tribute can now be applied to a value. So instead of My_Type'Image (Value), you can just
write Value'Image, as long as the Value is a name284 . These two statements are equiva-
lent:
Ada.Text_IO.Put_Line (Ada.Text_IO.Page_Length'Image);
Ada.Text_IO.Put_Line
(Ada.Text_IO.Count'Image (Ada.Text_IO.Page_Length));
In Ada 2022, you can apply the 'Image attribute to any type, including records, arrays,
access types, and private types. Let's see how this works. We'll define array, record, and
access types and corresponding objects and then convert these objects to strings and print
them:
Listing 1: main.adb
1 pragma Ada_2022;
2
3 with Ada.Text_IO;
4
5 procedure Main is
6 type Vector is array (Positive range <>) of Integer;
7
1235
Learning Ada
20 begin
21 Ada.Text_IO.Put_Line (V1'Image);
22 Ada.Text_IO.Put_Line (Pos'Image);
23 Ada.Text_IO.New_Line;
24 Ada.Text_IO.Put_Line (V1_Ptr'Image);
25 end Main;
Project: Courses.Ada_2022_Whats_New.Image_Attribute
MD5: 47945f0f8a4ba37b838f87b7e5acaa49
Runtime output
[ 1, 2, 3]
(access 7ffdd1e99348)
$ gprbuild -q -P main.gpr
Build completed successfully.
$ ./main
[ 1, 2, 3]
(LINE => 10,
COLUMN => 3)
(access 7fff64b23988)
Note the square brackets in the array image output. In Ada 2022, array aggregates could
be written this way (page 1245)!
42.3 References
285 http://www.ada-auth.org/standards/22aarm/html/AA-4-10.html
286 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/ai12s/ai12-0020-1.txt
FORTYTHREE
In Ada 2022, you can redefine 'Image attribute for your type, though the syntax to do this
has been changed several times. Let's see how it works in GNAT Community 2021.
In our example, let's redefine the 'Image attribute for a location in source code. To do this,
we provide a new Put_Image aspect for the type:
Listing 1: main.adb
1 pragma Ada_2022;
2
3 with Ada.Text_IO;
4 with Ada.Strings.Text_Buffers;
5
6 procedure Main is
7
14 procedure My_Put_Image
15 (Output : in out Ada.Strings.Text_Buffers.Root_Buffer_Type'Class;
16 Value : Source_Location);
17
18 procedure My_Put_Image
19 (Output : in out Ada.Strings.Text_Buffers.Root_Buffer_Type'Class;
20 Value : Source_Location)
21 is
22 Line : constant String := Value.Line'Image;
23 Column : constant String := Value.Column'Image;
24 Result : constant String :=
25 Line (2 .. Line'Last) & ':' & Column (2 .. Column'Last);
26 begin
27 Output.Put (Result);
28 end My_Put_Image;
29
32 begin
(continues on next page)
1237
Learning Ada
Project: Courses.Ada_2022_Whats_New.Image_Redefine
MD5: a4a6df87eea66d0a2bcaac9c4cccbe4a
Runtime output
10:1
procedure Put
(Buffer : in out Root_Buffer_Type;
Item : in String) is abstract;
GNAT Community Edition 2020 and GCC 11 both provide a draft implementation that's
incompatible with the Ada 2022 specification. For those versions, My_Put_Image looks like:
procedure My_Put_Image
(Sink : in out Ada.Strings.Text_Output.Sink'Class;
Value : Source_Location)
is
Line : constant String := Value.Line'Image;
Column : constant String := Value.Column'Image;
Result : constant String :=
Line (2 .. Line'Last) & ':' & Column (2 .. Column'Last);
begin
Ada.Strings.Text_Output.Utils.Put_UTF_8 (Sink, Result);
end My_Put_Image;
43.3 References
FORTYFOUR
USER-DEFINED LITERALS
In Ada 2022, you can define string, integer, or real literals for your types. The compiler will
convert such literals to your type at run time using a function you provide. To do so, specify
one or more new aspects:
• Integer_Literal
• Real_Literal
• String_Literal
For our example, let's define all three for a simple type and see how they work. For sim-
plicity, we use a Wide_Wide_String component for the internal representation:
Listing 1: main.adb
1 pragma Ada_2022;
2
3 with Ada.Wide_Wide_Text_IO;
4 with Ada.Characters.Conversions;
5
6 procedure Main is
7
1241
Learning Ada
Runtime output
Test "string"
123
16#DEAD_BEEF#
2.99_792_458e+8
As you see, real and integer literals are converted to strings while preserving the formatting
in the source code, while string literals are decoded: From_String is passed the specified
string value. In all cases, the compiler translates these literals into function calls.
Now we can get the same result in Ada! But before we do, we need to define a custom +
operator:
Listing 2: main.adb
1 pragma Ada_2022;
2
3 with Ada.Wide_Wide_Text_IO;
4 with Ada.Characters.Conversions;
5
6 procedure Main is
7
32 begin
33 Print ("5" + 3);
34 end Main;
Project: Courses.Ada_2022_Whats_New.User_Defined_Literals_JS
MD5: 9f41f61b1f4bc03cbe245cd8e0288e4f
Runtime output
53
Jokes aside, this feature is very useful. For example it allows a "native-looking API" for big
integers (page 1265).
44.2 References
290 http://www.ada-auth.org/standards/22rm/html/RM-4-2-1.html
291 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0249-1.TXT
292 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0342-1.TXT
FORTYFIVE
In Ada 2022, you can use square brackets in array aggregates. Using square brackets
simplifies writing both empty aggregates and single-element aggregates. Consider this:
Listing 1: show_square_brackets.ads
1 pragma Ada_2022;
2 pragma Extensions_Allowed (On);
3
4 package Show_Square_Brackets is
5
14 end Show_Square_Brackets;
Project: Courses.Ada_2022_Whats_New.Square_Brackets
MD5: fb4638717d4a12c1dae8e646705ddf17
1245
Learning Ada
This association starts with for keyword, just like a quantified expression. It declares an
index parameter that you can use in the computation of a component.
Iterated component associations can nest and can be nested in another association (iter-
ated or not). Here we use this to define a square matrix:
Iterated component associations in this form provide both element indices and values, just
like named component associations:
Data : Integer_Array (1 .. 5) :=
[for J in 2 .. 3 => J, 5 => 5, others => 0];
Listing 2: show_iterated_component_association.adb
1 pragma Ada_2022;
2 pragma Extensions_Allowed (On); -- for square brackets
3
4 with Ada.Text_IO;
5
6 procedure Show_Iterated_Component_Association is
7
Project: Courses.Ada_2022_Whats_New.Iterated_Component_Association
MD5: 05f7fc94e3f4d79b7ca25de4d7dedf4f
Runtime output
[ 2, 4, 6, 8, 10]
[
[ 11, 12, 13],
[ 0, 2, 3, 0, 5]
[ 1, 2, 3, 4, 5]
45.3 References
293 http://www.ada-auth.org/standards/22aarm/html/AA-4-3-3.html
294 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0212-1.TXT
295 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0306-1.TXT
FORTYSIX
CONTAINER AGGREGATES
Ada 2022 introduces container aggregates, which can be used to easily create values for
vectors, lists, maps, and other aggregates. For containers such as maps, the aggregate
must use named assоciations to provide keys and values. For other containers it uses
positional assоciations. Only square brackets are allowed. Here's an example:
Listing 1: main.adb
1 pragma Ada_2022;
2
3 with Ada.Text_IO;
4 with Ada.Containers.Vectors;
5 with Ada.Containers.Ordered_Maps;
6
7 procedure Main is
8
Project: Courses.Ada_2022_Whats_New.Container_Aggregates_1
MD5: dd1dd78890d4bf6c78b79d56abba332d
Runtime output
[ 1, 2, 3]
1249
Learning Ada
At run time, the compiler creates an empty container and populates it with elements one by
one. If you define a new container type, you can specify a new Aggregate aspect to enable
container aggregates for your container and let the compiler know what subprograms to
use to construct the aggregate:
Listing 2: main.adb
1 pragma Ada_2022;
2
3 procedure Main is
4
5 package JSON is
6 type JSON_Value is private
7 with Integer_Literal => To_JSON_Value;
8
17 procedure Append
18 (Self : in out JSON_Array;
19 Value : JSON_Value) is null;
20
21 private
22 type JSON_Value is null record;
23 type JSON_Array is null record;
24
Project: Courses.Ada_2022_Whats_New.Container_Aggregates_2
MD5: 9cf1fefa4a725083c50794146d5cbde7
Listing 3: main.adb
1 pragma Ada_2022;
2
3 procedure Main is
4
5 package JSON is
6 type JSON_Value is private
(continues on next page)
17 procedure Insert
18 (Self : in out JSON_Object;
19 Key : Wide_Wide_String;
20 Value : JSON_Value) is null;
21
22 private
23 type JSON_Value is null record;
24 type JSON_Object is null record;
25
You can't specify both Add_Named and Add_Unnamed subprograms for the same type. This
prevents you from defining JSON_Value with both array and object aggregates present. But
we can define conversion functions for array and object and get code almost as dense as
the same code in native JSON. For example:
Listing 4: main.adb
1 pragma Ada_2022;
2
3 procedure Main is
4
5 package JSON is
6 type JSON_Value is private
7 with Integer_Literal => To_Value, String_Literal => To_Value;
8
1251
Learning Ada
18 procedure Insert
19 (Self : in out JSON_Object;
20 Key : Wide_Wide_String;
21 Value : JSON_Value) is null;
22
31 procedure Append
32 (Self : in out JSON_Array;
33 Value : JSON_Value) is null;
34
37 private
38 type JSON_Value is null record;
39 type JSON_Object is null record;
40 type JSON_Array is null record;
41
61 Offices : JSON.JSON_Array :=
62 [+["name" => "North American Office",
63 "phones" => -[1_877_787_4628,
64 1_866_787_4232,
65 1_212_620_7300],
66 "email" => "[email protected]"],
67 +["name" => "European Office",
68 "phones" => -[33_1_49_70_67_16,
69 33_1_49_70_05_52],
70 "email" => "[email protected]"]];
71 -----------------------------------------------------------------
72 begin
73 -- Equivalent old initialization code is too long to print it here
74 null;
75 end Main;
Project: Courses.Ada_2022_Whats_New.Container_Aggregates_4
MD5: 3e8d96bbcf77e2c63fb87dcf313b98f1
46.1 References
296 http://www.ada-auth.org/standards/22aarm/html/AA-4-3-5.html
297 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0212-1.TXT
FORTYSEVEN
DELTA AGGREGATES
Sometimes you need to create a copy of an object, but with a few modifications. Before Ada
2022, doing this involves a dummy object declaration or an aggregate with associations for
each property. The dummy object approach doesn't work in contract aspects or when there
are limited components. On the other hand, re-listing properties in an large aggregate can
be very tedious and error-prone. So, in Ada 2022, you can use a delta aggregate instead.
The more components you have, the more you will like the delta aggregate.
You can also use delta aggregates for arrays to change elements, but not bounds. Moreover,
it only works for one-dimensional arrays of non-limited components.
You can use parentheses for array aggregates, but you can't use square brackets for record
aggregates.
Here is the complete code snippet:
1255
Learning Ada
Listing 1: main.adb
1 pragma Ada_2022;
2
3 with Ada.Text_IO;
4
5 procedure Main is
6
47.3 References
298 http://www.ada-auth.org/standards/22aarm/html/AA-4-3-4.html
299 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0127-1.TXT
FORTYEIGHT
Ada 2022 introduces a new symbol, @, which can only appear on the right hand side of an
assignment statement. This symbol acts as the equivalent of the name on the left hand
side of that assignment statement. It was introduced to avoid code duplication: instead of
retyping a (potentially long) name, you can use @. This symbol denotes a constant, so you
can't pass it into [in] out arguments of a subprogram.
As an example, let's calculate some statistics for My_Data array:
Listing 1: statistics.ads
1 pragma Ada_2022;
2
3 package Statistics is
4
12 Statistic_For_My_Data : Statistic;
13
14 end Statistics;
Project: Courses.Ada_2022_Whats_New.Assignment_Tagged_Intro
MD5: 5cc813a4a22d3acc8418b0c1c6df3877
Listing 2: main.adb
1 pragma Ada_2022;
2 with Ada.Text_IO;
3
4 procedure Main is
5
1257
Learning Ada
14 Statistic_For_My_Data : Statistic;
15
16 begin
17 for Data of My_Data loop
18 Statistic_For_My_Data.Count := @ + 1;
19 Statistic_For_My_Data.Total := @ + Data;
20 end loop;
21
22 Ada.Text_IO.Put_Line (Statistic_For_My_Data'Image);
23 end Main;
Project: Courses.Ada_2022_Whats_New.Assignment_Tagged_2
MD5: 10dd019f4c09bc950895a93b3a88b778
Runtime output
(COUNT => 5,
TOTAL => 1.50000E+01)
Each right hand side is evaluated only once, no matter how many @ symbols it contains.
Let's verify this by introducing a function call that prints a line each time it's called:
Listing 3: main.adb
1 pragma Ada_2022;
2 with Ada.Text_IO;
3
4 procedure Main is
5
14 begin
15 My_Data (To_Index (1)) := @ ** 2 - 3.0 * @;
16 Ada.Text_IO.Put_Line (My_Data'Image);
17 end Main;
Project: Courses.Ada_2022_Whats_New.Assignment_Tagged_3
MD5: 98d6afbaea5c0f6cd2bebe6b39962ad3
Runtime output
To_Index is called.
This use of @ may look a bit cryptic, but it's the best solution that was found. Unlike other
languages (e.g., sum += x; in C), this approach lets you use @ an arbitrary number of times
within the right hand side of an assignment statement.
48.1 Alternatives
In C++, the previous statement could be written with a reference type (one line longer!):
auto& a = my_data[to_index(1)];
a = a * a - 3.0 * a;
declare
A renames My_Data (To_Index (1));
begin
A := A ** 2 - 3.0 * A;
end;
Here we use a new short form of the rename declaration, but this still looks too heavy, and
even worse, it can't be used for discriminant-dependent components.
48.2 References
300 http://www.ada-auth.org/standards/22aarm/html/AA-5-2-1.html
301 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0125-3.TXT
FORTYNINE
ENUMERATION REPRESENTATION
Enumeration types in Ada are represented as integers at the machine level. But there are
actually two mappings from enumeration to integer: a literal position and a representation
value.
Each enumeration literal has a corresponding position in the type declaration. We can easily
obtain it from the Type'Pos (Enum) attribute.
Listing 1: main.adb
1 with Ada.Text_IO;
2 with Ada.Integer_Text_IO;
3
4 procedure Main is
5 begin
6 Ada.Text_IO.Put ("Pos(False) =");
7 Ada.Integer_Text_IO.Put (Boolean'Pos (False));
8 Ada.Text_IO.New_Line;
9 Ada.Text_IO.Put ("Pos(True) =");
10 Ada.Integer_Text_IO.Put (Boolean'Pos (True));
11 end Main;
Project: Courses.Ada_2022_Whats_New.Enum_Val.Pos
MD5: de7c39f83f7df231dd648606579996a8
Runtime output
Pos(False) = 0
Pos(True) = 1
1261
Learning Ada
Listing 2: main.adb
1 with Ada.Text_IO;
2
3 procedure Main is
4 begin
5 Ada.Text_IO.Put_Line (Boolean'Val (0)'Image);
6 Ada.Text_IO.Put_Line (Boolean'Val (1)'Image);
7 end Main;
Project: Courses.Ada_2022_Whats_New.Enum_Val.Val
MD5: 43f712d25552970bccc4c0c84089d927
Runtime output
FALSE
TRUE
The representation value defines the internal code, used to store enumeration values in
memory or CPU registers. By default, enumeration representation values are the same as
the corresponding literal positions, but you can redefine them. Here, we created a copy of
Boolean type and assigned it a custom representation.
In Ada 2022, we can get an integer value of the representation with Type'Enum_Rep(Enum)
attribute:
Listing 3: main.adb
1 with Ada.Text_IO;
2 with Ada.Integer_Text_IO;
3
4 procedure Main is
5 type My_Boolean is new Boolean;
6 for My_Boolean use (False => 3, True => 6);
7 begin
8 Ada.Text_IO.Put ("Enum_Rep(False) =");
9 Ada.Integer_Text_IO.Put (My_Boolean'Enum_Rep (False));
10 Ada.Text_IO.New_Line;
11 Ada.Text_IO.Put ("Enum_Rep(True) =");
12 Ada.Integer_Text_IO.Put (My_Boolean'Enum_Rep (True));
13 end Main;
Project: Courses.Ada_2022_Whats_New.Enum_Val.Enum_Rep
MD5: 384ad9de7124c8131aa83ab71da58964
Runtime output
Enum_Rep(False) = 3
Enum_Rep(True) = 6
Listing 4: main.adb
1 with Ada.Text_IO;
2 with Ada.Integer_Text_IO;
3
4 procedure Main is
5 type My_Boolean is new Boolean;
6 for My_Boolean use (False => 3, True => 6);
7 begin
8 Ada.Text_IO.Put_Line (My_Boolean'Enum_Val (3)'Image);
9 Ada.Text_IO.Put_Line (My_Boolean'Enum_Val (6)'Image);
10
Runtime output
FALSE
TRUE
Pos(False) = 0
Pos(True) = 1
This doesn't initially look like an important feature, but let's see how we'd do the equivalent
with Ada 2012 and earlier versions. First, we need an integer type of matching size, then
we instantiate Ada.Unchecked_Conversion. Next, we call To_Int/From_Int to work with
representation values. And finally an extra type conversion is needed:
Listing 5: main.adb
1 with Ada.Text_IO;
2 with Ada.Integer_Text_IO;
3 with Ada.Unchecked_Conversion;
4
5 procedure Main is
6
18 begin
19 Ada.Text_IO.Put ("To_Int(False) =");
20 Ada.Integer_Text_IO.Put (Integer (To_Int (False)));
21 Ada.Text_IO.New_Line;
22 Ada.Text_IO.Put ("To_Int(True) =");
23 Ada.Integer_Text_IO.Put (Integer (To_Int (True)));
24 Ada.Text_IO.New_Line;
25 Ada.Text_IO.Put ("From_Int (3) =");
26 Ada.Text_IO.Put_Line (From_Int (3)'Image);
27 Ada.Text_IO.New_Line;
28 Ada.Text_IO.Put ("From_Int (6) =");
29 Ada.Text_IO.Put_Line (From_Int (6)'Image);
30 end Main;
Project: Courses.Ada_2022_Whats_New.Enum_Val.Conv
MD5: 7c7624ed024b26036389f77dbd6cb109
Runtime output
To_Int(False) = 3
To_Int(True) = 6
From_Int (3) =TRUE
Even with all that, this solution doesn't work for generic formal type (because T'Size must
be a static value)!
We should note that these new attributes may already be familiar to GNAT users because
they've been in the GNAT compiler for many years.
49.4 References
302 http://www.ada-auth.org/standards/22aarm/html/AA-13-4.html
303 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0237-1.TXT
FIFTY
BIG NUMBERS
115792089237316195423570985008687907853269984665640564039457584007913129639936
Note: Note that you shouldn't use Big_Numbers for cryptography because it's vulnerable
to timing side-channels attacks.
We can implement the RSA algorithm304 in a few lines of code. The main operation of RSA
is (md ) mod n. But you can't just write m ** d, because these are really big numbers and
the result won't fit into memory. However, if you keep intermediate result mod n during the
md calculation, it will work. Let's write this operation as a function:
Listing 1: power_mod.ads
1 pragma Ada_2022;
2
3 with Ada.Numerics.Big_Numbers.Big_Integers;
4 use Ada.Numerics.Big_Numbers.Big_Integers;
(continues on next page)
304 https://en.wikipedia.org/wiki/RSA_(cryptosystem)
1265
Learning Ada
6 -- Calculate M ** D mod N
7
Listing 2: power_mod.adb
1 function Power_Mod (M, D, N : Big_Integer) return Big_Integer is
2
6 Result : Big_Integer := 1;
7 Exp : Big_Integer := D;
8 Mult : Big_Integer := M mod N;
9 begin
10 while Exp /= 0 loop
11 -- Loop invariant is Power_Mod'Result = Result * Mult**Exp mod N
12 if Is_Odd (Exp) then
13 Result := (Result * Mult) mod N;
14 end if;
15
20 return Result;
21 end Power_Mod;
Project: Courses.Ada_2022_Whats_New.Big_Integers
MD5: 217c2aa3535952b68e2f088d262e6f60
Let's check this with the example from Wikipedia305 . In that example, the public key is (n
= 3233, e = 17) and the message is m = 65. The encrypted message is me mod n = 6517
mod 3233 = 2790 = c.
2790
To decrypt it with the public key (n = 3233, d = 413), we need to calculate cd mod n =
2790413 mod 3233:
65
Listing 3: main.adb
1 pragma Ada_2022;
2
3 with Ada.Text_IO;
(continues on next page)
305 https://en.wikipedia.org/wiki/RSA_(cryptosystem)
7 procedure Main is
8
9 -- Calculate M ** D mod N
10
16 Result : Big_Integer := 1;
17 Exp : Big_Integer := D;
18 Mult : Big_Integer := M mod N;
19 begin
20 while Exp /= 0 loop
21 -- Loop invariant is Power_Mod'Result = Result * Mult**Exp mod N
22 if Is_Odd (Exp) then
23 Result := (Result * Mult) mod N;
24 end if;
25
30 return Result;
31 end Power_Mod;
32
33 begin
34 Ada.Text_IO.Put_Line (Big_Integer'Image (2 ** 256));
35 -- Encrypt:
36 Ada.Text_IO.Put_Line (Power_Mod (M => 65, D => 17, N => 3233)'Image);
37 -- Decrypt:
38 Ada.Text_IO.Put_Line (Power_Mod (M => 2790, D => 413, N => 3233)'Image);
39 end Main;
Project: Courses.Ada_2022_Whats_New.Big_Numbers_Tiny_RSA
MD5: 6178da9d6998db6d51f31fd5c7cc5391
Runtime output
115792089237316195423570985008687907853269984665640564039457584007913129639936
2790
65
50.4 References
307 http://www.ada-auth.org/standards/22aarm/html/AA-A-5-6.html
308 http://www.ada-auth.org/standards/22aarm/html/AA-A-5-7.html
309 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0208-1.TXT
FIFTYONE
In C, variadic functions310 take a variable number of arguments and an ellipsis as the last
parameter of the declaration. A typical and well-known example is:
Usually, in Ada, we bind such a function with just the parameters we want to use:
procedure printf_double
(format : Interfaces.C.char_array;
value : Interfaces.C.double)
with Import,
Convention => C,
External_Name => "printf";
Unfortunately, doing it this way doesn't always work because some ABI311 s use different
calling conventions for variadic functions. For example, the AMD64 ABI312 specifies:
• %rax — with variable arguments passes information about the number of vector reg-
isters used;
• %xmm0–%xmm1 — used to pass and return floating point arguments.
This means, if we write (in C):
printf("%d", 5);
The compiler will place 0 into %rax, because we don't pass any float argument. But in Ada,
if we write:
procedure printf_int
(format : Interfaces.C.char_array;
value : Interfaces.C.int)
with Import,
Convention => C,
(continues on next page)
310 https://en.cppreference.com/w/c/variadic
311 https://en.wikipedia.org/wiki/Application_binary_interface
312 https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf
1269
Learning Ada
the compiler won't use the %rax register at all. (You can't include any float argument be-
cause there's no float parameter in the Ada wrapper function declaration.) As result, you
will get a crash, stack corruption, or other undefined behavior.
To fix this, Ada 2022 provides a new family of calling convention names — C_Variadic_N:
The convention C_Variadic_n is the calling convention for a variadic C function
taking n fixed parameters and then a variable number of additional parameters.
Therefore, the correct way to bind the printf function is:
procedure printf_int
(format : Interfaces.C.char_array;
value : Interfaces.C.int)
with Import,
Convention => C_Variadic_1,
External_Name => "printf";
Without this convention, problems cause by this mismatch can be very hard to debug. So,
this is a very useful extension to the Ada-to-C interfacing facility.
Here is the complete code snippet:
Listing 1: main.adb
1 with Interfaces.C;
2
3 procedure Main is
4
5 procedure printf_int
6 (format : Interfaces.C.char_array;
7 value : Interfaces.C.int)
8 with Import,
9 Convention => C_Variadic_1,
10 External_Name => "printf";
11
12 begin
13 printf_int (Interfaces.C.To_C ("d=%d"), 5);
14 end Main;
Project: Courses.Ada_2022_Whats_New.Variadic_Import
MD5: 94515f55a93f27e4f4ecec31256645d9
51.1 References
313 http://www.ada-auth.org/standards/22aarm/html/AA-B-3.html
314 http://www.ada-auth.org/cgi-bin/cvsweb.cgi/AI12s/AI12-0028-1.TXT
1273
Learning Ada
This document will present the Ada language using terminology and examples that are
familiar to developers that understand the C++ or Java languages.
This document was prepared by Quentin Ochem, with contributions and review from Richard
Kenner, Albert Lee, and Ben Brosgol.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn web-
site316 . The directory structure in the zip file is based on the code block metadata. For
example, if you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
1. Unpack the zip file;
2. Go to target directory;
3. Start GNAT Studio on this directory;
4. Build (or compile) the project;
5. Run the application (if a main procedure is available in the project).
315 http://creativecommons.org/licenses/by-sa/4.0
316 https://learn.adacore.com/zip/learning-ada_code.zip
1275
Learning Ada
1276
CHAPTER
FIFTYTWO
PREFACE
Nowadays it seems like talking about programming languages is a bit passé. The technical
wars of the past decade have subsided and today we see a variety of high-level and well-
established languages offering functionality that can meet the needs of any programmer.
Python, Java, C++, C#, and Visual Basic are recent examples. Indeed, these languages
make it easier to write code very quickly, are very flexible, offer features with highly dy-
namic behavior, and some even allow compilers to deduce the developer's probable intent.
Why, then, talk about yet another language? Well, by addressing the general programming
market, the aforementioned languages have become poorly suited for working within the
domain of high-integrity systems. In highly reliable, secure and safe applications such as
those found in and around airplanes, rockets, satellites, trains, and in any device whose
failure could jeopardize human life or critical assets, the programming languages used must
support the high standard of software engineering necessary to maintain the integrity of
the system.
The concept of verification — the practice of showing that the system behaves and per-
forms as intended — is key in such environments. Verification can be accomplished by
some combination of review, testing, static analysis, and formal proof techniques. The in-
creasing reliance on software and increasing complexity of today's systems has made this
task more difficult. Technologies and practices that might have been perfectly acceptable
ten or fifteen years ago are insufficient today. Thankfully, the state of the art in analysis
and proof tools and techniques has also advanced.
The latest revisions of the Ada language, Ada 2005 and Ada 2012, make enhanced software
integrity possible. From its inception in the 1980s, Ada was designed to meet the require-
ments of high-integrity systems, and continues to be well-suited for the implementation
of critical embedded or native applications. And it has been receiving increased attention
recently. Every language revision has enhanced expressiveness in many areas. Ada 2012,
in particular, has introduced new features for contract-based programming that are valu-
able to any project where verification is part of the engineering lifecycle. Along with these
language enhancements, Ada compiler and tool technology has also kept pace with gen-
eral computing developments over the past few years. Ada development environments
are available on a wide range of platforms and are being used for the most demanding
applications.
It is no secret that we at AdaCore are very enthusiastic about Ada, but we will not claim that
Ada is always the solution; Ada is no more a silver bullet than any other language. In some
domains other languages make sense because of the availability of particular libraries or
development frameworks. For example, C++ and Java are considered good choices for
desktop programs or applications where a shortened time to market is a major objective.
Other areas, such as website programming or system administration, tend to rely on differ-
ent formalisms such as scripting and interpreted languages. The key is to select the proper
technical approach, in terms of the language and tools, to meet the requirements. Ada's
strength is in areas where reliability is paramount.
Learning a new language shouldn't be complicated. Programming paradigms have not
evolved much since object oriented programming gained a foothold, and the same
1277
Learning Ada
paradigms are present one way or another in many widely used languages. This docu-
ment will thus give you an overview of the Ada language using analogies to C++ and Java
— these are the languages you're already likely to know. No prior knowledge of Ada is
assumed. If you are working on an Ada project now and need more background, if you
are interested in learning to program in Ada, or if you need to perform an assessment of
possible languages to be used for a new development, this guide is for you.
FIFTYTHREE
BASICS
Ada implements the vast majority of programming concepts that you're accustomed to
in C++ and Java: classes, inheritance, templates (generics), etc. Its syntax might seem
peculiar, though. It's not derived from the popular C style of notation with its ample use
of brackets; rather, it uses a more expository syntax coming from Pascal. In many ways,
Ada is a simpler language — its syntax favors making it easier to conceptualize and read
program code, rather than making it faster to write in a cleverly condensed manner. For
example, full words like begin and end are used in place of curly braces. Conditions are
written using if, then, elsif, else, and end if. Ada's assignment operator does not
double as an expression, smoothly eliminating any frustration that could be caused by =
being used where == should be.
All languages provide one or more ways to express comments. In Ada, two consecutive
hyphens -- mark the start of a comment that continues to the end of the line. This is
exactly the same as using // for comments in C++ and Java. There is no equivalent of /*
... /* block comments in Ada; use multiple -- lines instead.
Ada compilers are stricter with type and range checking than most C++ and Java program-
mers are used to. Most beginning Ada programmers encounter a variety of warnings and
error messages when coding more creatively, but this helps detect problems and vulnera-
bilities at compile time — early on in the development cycle. In addition, dynamic checks
(such as array bounds checks) provide verification that could not be done at compile time.
Dynamic checks are performed at run time, similar to what is done in Java.
Ada identifiers and reserved words are case insensitive. The identifiers VAR, var and VaR are
treated as the same; likewise begin, BEGIN, Begin, etc. Language-specific characters, such
as accents, Greek or Russian letters, and Asian alphabets, are acceptable to use. Identifiers
may include letters, digits, and underscores, but must always start with a letter. There are
73 reserved keywords in Ada that may not be used as identifiers, and these are:
1279
Learning Ada
Ada is designed to be portable. Ada compilers must follow a precisely defined interna-
tional (ISO) standard language specification with clearly documented areas of vendor free-
dom where the behavior depends on the implementation. It's possible, then, to write an
implementation-independent application in Ada and to make sure it will have the same
effect across platforms and compilers.
Ada is truly a general purpose, multiple paradigm language that allows the programmer to
employ or avoid features like run-time contract checking, tasking, object oriented program-
ming, and generics. Efficiently programmed Ada is employed in device drivers, interrupt
handlers, and other low-level functions. It may be found today in devices with tight limits
on processing speed, memory, and power consumption. But the language is also used for
programming larger interconnected systems running on workstations, servers, and super-
computers.
FIFTYFOUR
C++ programming style usually promotes the use of two distinct files: header files used
to define specifications (.h*, .hxx, .hpp), and implementation files which contain the ex-
ecutable code (.c, .cxx, .cpp). However, the distinction between specification and imple-
mentation is not enforced by the compiler and may need to be worked around in order to
implement, for example, inlining or templates.
Java compilers expect both the implementation and specification to be in the same .java file.
(Yes, design patterns allow using interfaces to separate specification from implementation
to a certain extent, but this is outside of the scope of this description.)
Ada is superficially similar to the C++ case: Ada compilation units are generally split into
two parts, the specification and the body. However, what goes into those files is more
predictable for both the compiler and for the programmer. With GNAT, compilation units
are stored in files with a .ads extension for specifications and with a .adb extension for
implementations.
Without further ado, we present the famous "Hello World" in three languages:
[Ada]
with Ada.Text_IO;
use Ada.Text_IO;
procedure Main is
begin
Put_Line ("Hello World");
end Main;
[C++]
#include <iostream>
using namespace std;
[Java]
The first line of Ada we see is the with clause, declaring that the unit (in this case, the
Main subprogram) will require the services of the package Ada.Text_IO. This is different
from how #include works in C++ in that it does not, in a logical sense, copy/paste the
code of Ada.Text_IO into Main. The with clause directs the compiler to make the public
1281
Learning Ada
interface of the Ada.Text_IO package visible to code in the unit (here Main) containing
the with clause. Note that this construct does not have a direct analog in Java, where the
entire CLASSPATH is always accessible. Also, the name Main for the main subprogram was
chosen for consistency with C++ and Java style but in Ada the name can be whatever the
programmer chooses.
The use clause is the equivalent of using namespace in C++, or import in Java (though
it wasn't necessary to use import in the Java example above). It allows you to omit the
full package name when referring to with'ed units. Without the use clause, any reference
to Ada.Text_IO items would have had to be fully qualified with the package name. The
Put_Line line would then have read:
The word "package" has different meanings in Ada and Java. In Java, a package is used as
a namespace for classes. In Ada, it's often a compilation unit. As a result Ada tends to have
many more packages than Java. Ada package specifications ("package specs" for short)
have the following structure:
package Package_Name is
-- public declarations
private
-- private declarations
end Package_Name;
The implementation in a package body (written in a .adb file) has the structure:
-- implementation
end Package_Name;
The private reserved word is used to mark the start of the private portion of a package
spec. By splitting the package spec into private and public parts, it is possible to make
an entity available for use while hiding its implementation. For instance, a common use is
declaring a record (Ada's struct) whose fields are only visible to its package and not to
the caller. This allows the caller to refer to objects of that type, but not to change any of its
contents directly.
The package body contains implementation code, and is only accessible to outside code
through declarations in the package spec.
An entity declared in the private part of a package in Ada is roughly equivalent to a protected
member of a C++ or Java class. An entity declared in the body of an Ada package is roughly
equivalent to a private member of a C++ or Java class.
FIFTYFIVE
The following code samples are all equivalent, and illustrate the use of comments and
working with integer variables:
[Ada]
--
-- Ada program to declare and modify Integers
--
procedure Main is
-- Variable declarations
A, B : Integer := 0;
C : Integer := 100;
D : Integer;
begin
-- Ada uses a regular assignment statement for incrementation.
A := A + 1;
-- Regular addition
D := A + B + C;
end Main;
[C++]
/*
* C++ program to declare and modify ints
*/
int main(int argc, const char* argv[]) {
// Variable declarations
int a = 0, b = 0, c = 100, d;
// Regular addition
d = a + b + c;
}
[Java]
/*
* Java program to declare and modify ints
*/
public class Main {
(continues on next page)
1283
Learning Ada
// Regular addition
d = a + b + c;
}
}
Statements are terminated by semicolons in all three languages. In Ada, blocks of code
are surrounded by the reserved words begin and end rather than by curly braces. We can
use both multi-line and single-line comment styles in the C++ and Java code, and only
single-line comments in the Ada code.
Ada requires variable declarations to be made in a specific area called the declarative part,
seen here before the begin keyword. Variable declarations start with the identifier in Ada,
as opposed to starting with the type as in C++ and Java (also note Ada's use of the :
separator). Specifying initializers is different as well: in Ada an initialization expression can
apply to multiple variables (but will be evaluated separately for each), whereas in C++ and
Java each variable is initialized individually. In all three languages, if you use a function as
an initializer and that function returns different values on every invocation, each variable
will get initialized to a different value.
Let's move on to the imperative statements. Ada does not provide ++ or -- shorthand
expressions for increment/decrement operations; it is necessary to use a full assignment
statement. The := symbol is used in Ada to perform value assignment. Unlike C++'s and
Java's = symbol, := can not be used as part of an expression. So, a statement like A := B
:= C; doesn't make sense to an Ada compiler, and neither does a clause like if A := B
then .... Both are compile-time errors.
You can nest a block of code within an outer block if you want to create an inner scope:
procedure Main is
begin
Put_Line ("Before the inner block");
declare
Alpha : Integer := 0;
begin
Alpha := Alpha + 1;
Put_Line ("Now inside the inner block");
end;
It is OK to have an empty declarative part or to omit the declarative part entirely — just
start the inner block with begin if you have no declarations to make. However it is not OK
to have an empty sequence of statements. You must at least provide a null; statement,
which does nothing and indicates that the omission of statements is intentional.
55.2 Conditions
[C++]
if (Variable > 0)
cout << " > 0 " << endl;
else if (Variable < 0)
cout << " < 0 " << endl;
else
cout << " = 0 " << endl;
[Java]
if (Variable > 0)
System.out.println (" > 0 ");
else if (Variable < 0)
System.out.println (" < 0 ");
else
System.out.println (" = 0 ");
In Ada, everything that appears between the if and then keywords is the conditional ex-
pression — no parentheses required. Comparison operators are the same, except for equal-
ity (=) and inequality (/=). The English words not, and, and or replace the symbols !, &,
and |, respectively, for performing boolean operations.
It's more customary to use && and || in C++ and Java than & and | when writing boolean
expressions. The difference is that && and || are short-circuit operators, which evaluate
terms only as necessary, and & and | will unconditionally evaluate all terms. In Ada, and
and or will evaluate all terms; and then and or else direct the compiler to employ short
circuit evaluation.
Here are what switch/case statements look like:
[Ada]
case Variable is
when 0 =>
Put_Line ("Zero");
when 1 .. 9 =>
Put_Line ("Positive Digit");
when 10 | 12 | 14 | 16 | 18 =>
Put_Line ("Even Number between 10 and 18");
when others =>
Put_Line ("Something else");
end case;
[C++]
switch (Variable) {
case 0:
(continues on next page)
[Java]
switch (Variable) {
case 0:
System.out.println ("Zero");
break;
case 1: case 2: case 3: case 4: case 5:
case 6: case 7: case 8: case 9:
System.out.println ("Positive Digit");
break;
case 10: case 12: case 14: case 16: case 18:
System.out.println ("Even Number between 10 and 18");
break;
default:
System.out.println ("Something else");
}
In Ada, the case and end case lines surround the whole case statement, and each case
starts with when. So, when programming in Ada, replace switch with case, and replace
case with when.
Case statements in Ada require the use of discrete types (integers or enumeration types),
and require all possible cases to be covered by when statements. If not all the cases are han-
dled, or if duplicate cases exist, the program will not compile. The default case, default:
in C++ and Java, can be specified using when others => in Ada.
In Ada, the break instruction is implicit and program execution will never fall through to
subsequent cases. In order to combine cases, you can specify ranges using .. and enu-
merate disjoint values using | which neatly replaces the multiple case statements seen in
the C++ and Java versions.
55.3 Loops
In Ada, loops always start with the loop reserved word and end with end loop. To leave
the loop, use exit — the C++ and Java equivalent being break. This statement can specify
a terminating condition using the exit when syntax. The loop opening the block can be
preceded by a while or a for.
The while loop is the simplest one, and is very similar across all three languages:
[Ada]
[C++]
[Java]
Ada's for loop, however, is quite different from that in C++ and Java. It always increments
or decrements a loop index within a discrete range. The loop index (or "loop parameter" in
Ada parlance) is local to the scope of the loop and is implicitly incremented or decremented
at each iteration of the loop statements; the program cannot directly modify its value.
The type of the loop parameter is derived from the range. The range is always given in
ascending order even if the loop iterates in descending order. If the starting bound is greater
than the ending bound, the interval is considered to be empty and the loop contents will
not be executed. To specify a loop iteration in decreasing order, use the reverse reserved
word. Here are examples of loops going in both directions:
[Ada]
-- Outputs 0, 1, 2, ..., 9
for Variable in 0 .. 9 loop
Put_Line (Integer'Image (Variable));
end loop;
-- Outputs 9, 8, 7, ..., 0
for Variable in reverse 0 .. 9 loop
Put_Line (Integer'Image (Variable));
end loop;
[C++]
// Outputs 0, 1, 2, ..., 9
for (int Variable = 0; Variable <= 9; Variable++) {
cout << Variable << endl;
}
// Outputs 9, 8, 7, ..., 0
for (int Variable = 9; Variable >=0; Variable--) {
cout << Variable << endl;
}
[Java]
// Outputs 0, 1, 2, ..., 9
for (int Variable = 0; Variable <= 9; Variable++) {
System.out.println (Variable);
}
// Outputs 9, 8, 7, ..., 0
for (int Variable = 9; Variable >= 0; Variable--) {
System.out.println (Variable);
}
Ada uses the Integer type's 'Image attribute to convert a numerical value to a String.
There is no implicit conversion between Integer and String as there is in C++ and Java.
We'll have a more in-depth look at such attributes later on.
It's easy to express iteration over the contents of a container (for instance, an array, a list,
or a map) in Ada and Java. For example, assuming that Int_List is defined as an array of
Integer values, you can use:
[Ada]
[Java]
FIFTYSIX
TYPE SYSTEM
One of the main characteristics of Ada is its strong typing (i.e., relative absence of implicit
type conversions). This may take some getting used to. For example, you can't divide an
integer by a float. You need to perform the division operation using values of the same type,
so one value must be explicitly converted to match the type of the other (in this case the
more likely conversion is from integer to float). Ada is designed to guarantee that what's
done by the program is what's meant by the programmer, leaving as little room for compiler
interpretation as possible. Let's have a look at the following example:
[Ada]
procedure Strong_Typing is
Alpha : Integer := 1;
Beta : Integer := 10;
Result : Float;
begin
Result := Float (Alpha) / Float (Beta);
end Strong_Typing;
[C++]
void weakTyping () {
int alpha = 1;
int beta = 10;
float result;
[Java]
void weakTyping () {
int alpha = 1;
int beta = 10;
float result;
Are the three programs above equivalent? It may seem like Ada is just adding extra com-
plexity by forcing you to make the conversion from Integer to Float explicit. In fact it signif-
icantly changes the behavior of the computation. While the Ada code performs a floating
point operation 1.0 / 10.0 and stores 0.1 in Result, the C++ and Java versions instead
store 0.0 in result. This is because the C++ and Java versions perform an integer opera-
tion between two integer variables: 1 / 10 is 0. The result of the integer division is then
converted to a float and stored. Errors of this sort can be very hard to locate in complex
1289
Learning Ada
pieces of code, and systematic specification of how the operation should be interpreted
helps to avoid this class of errors. If an integer division was actually intended in the Ada
case, it is still necessary to explicitly convert the final result to Float:
In Ada, a floating point literal must be written with both an integral and decimal part. 10 is
not a valid literal for a floating point value, while 10.0 is.
The principal scalar types predefined by Ada are Integer, Float, Boolean, and Character.
These correspond to int, float, bool/boolean, and char, respectively. The names for
these types are not reserved words; they are regular identifiers.
Ada's type system encourages programmers to think about data at a high level of abstrac-
tion. The compiler will at times output a simple efficient machine instruction for a full line of
source code (and some instructions can be eliminated entirely). The careful programmer's
concern that the operation really makes sense in the real world would be satisfied, and so
would the programmer's concern about performance.
The next example below defines two different metrics: area and distance. Mixing these
two metrics must be done with great care, as certain operations do not make sense, like
adding an area to a distance. Others require knowledge of the expected semantics; for
example, multiplying two distances. To help avoid errors, Ada requires that each of the
binary operators +, -, *, and / for integer and floating-point types take operands of the
same type and return a value of that type.
procedure Main is
type Distance is new Float;
type Area is new Float;
D1 : Distance := 2.0;
D2 : Distance := 3.0;
A : Area;
begin
D1 := D1 + D2; -- OK
D1 := D1 + A; -- NOT OK: incompatible types for "+" operator
A := D1 * D2; -- NOT OK: incompatible types for ":=" assignment
A := Area (D1 * D2); -- OK
end Main;
Even though the Distance and Area types above are just Floats, the compiler does not
allow arbitrary mixing of values of these different types. An explicit conversion (which does
not necessarily mean any additional object code) is necessary.
The predefined Ada rules are not perfect; they admit some problematic cases (for example
multiplying two Distances yields a Distance) and prohibit some useful cases (for exam-
ple multiplying two Distances should deliver an Area). These situations can be handled
through other mechanisms. A predefined operation can be identified as abstract to make
it unavailable; overloading can be used to give new interpretations to existing operator
symbols, for example allowing an operator to return a value from a type different from its
operands; and more generally, GNAT has introduced a facility that helps perform dimen-
sionality checking.
Ada enumerations work similarly to C++ and Java's enums.
[Ada]
type Day is
(Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday);
[C++]
enum Day {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday};
[Java]
enum Day {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday}
But even though such enumerations may be implemented using a machine word, at the
language level Ada will not confuse the fact that Monday is a Day and is not an Integer.
You can compare a Day with another Day, though. To specify implementation details like
the numeric values that correspond with enumeration values in C++ you include them in
the original enum statement:
[C++]
enum Day {
Monday = 10,
Tuesday = 11,
Wednesday = 12,
Thursday = 13,
Friday = 14,
Saturday = 15,
Sunday = 16};
But in Ada you must use both a type definition for Day as well as a separate representation
clause for it like:
[Ada]
Contracts can be associated with types and variables, to refine values and define what are
considered valid values. The most common kind of contract is a range constraint introduced
with the range reserved word, for example:
procedure Main is
type Grade is range 0 .. 100;
G1, G2 : Grade;
N : Integer;
begin
... -- Initialization of N
G1 := 80; -- OK
G1 := N; -- Illegal (type mismatch)
G1 := Grade (N); -- Legal, run-time range check
G2 := G1 + 10; -- Legal, run-time range check
G1 := (G1 + G2)/2; -- Legal, run-time range check
end Main;
In the above example, Grade is a new integer type associated with a range check. Range
checks are dynamic and are meant to enforce the property that no object of the given type
can have a value outside the specified range. In this example, the first assignment to G1
is correct and will not raise a run-time exceprion. Assigning N to G1 is illegal since Grade is
a different type than Integer. Converting N to Grade makes the assignment legal, and a
range check on the conversion confirms that the value is within 0 .. 100. Assigning G1+10
to G2 is legal since + for Grade returns a Grade (note that the literal 10 is interpreted as a
Grade value in this context), and again there is a range check.
The final assignment illustrates an interesting but subtle point. The subexpression G1 + G2
may be outside the range of Grade, but the final result will be in range. Nevertheless, de-
pending on the representation chosen for Grade, the addition may overflow. If the compiler
represents Grade values as signed 8-bit integers (i.e., machine numbers in the range -128
.. 127) then the sum G1+G2 may exceed 127, resulting in an integer overflow. To prevent
this, you can use explicit conversions and perform the computation in a sufficiently large
integer type, for example:
Range checks are useful for detecting errors as early as possible. However, there may
be some impact on performance. Modern compilers do know how to remove redundant
checks, and you can deactivate these checks altogether if you have sufficient confidence
that your code will function correctly.
Types can be derived from the representation of any other type. The new derived type can
be associated with new constraints and operations. Going back to the Day example, one
can write:
Since these are new types, implicit conversions are not allowed. In this case, it's more
natural to create a new set of constraints for the same type, instead of making completely
new ones. This is the idea behind subtypes in Ada. A subtype is a type with optional
additional constraints. For example:
These declarations don't create new types, just new names for constrained ranges of their
base types.
Range checks are a special form of type contracts; a more general method is provided by
Ada subtype predicates, introduced in Ada 2012. A subtype predicate is a boolean expres-
sion defining conditions that are required for a given type or subtype. For example, the
Dice_Throw subtype shown above can be defined in the following way:
The clause beginning with with introduces an Ada aspect, which is additional information
provided for declared entities such as types and subtypes. The Dynamic_Predicate aspect
is the most general form. Within the predicate expression, the name of the (sub)type refers
to the current value of the (sub)type. The predicate is checked on assignment, parameter
passing, and in several other contexts. There is a Static_Predicate form which introduce
some optimization and constrains on the form of these predicates, outside of the scope of
this document.
Of course, predicates are useful beyond just expressing ranges. They can be used to repre-
sent types with arbitrary constraints, in particular types with discontinuities, for example:
56.6 Attributes
Attributes start with a single apostrophe ("tick"), and they allow you to query properties of,
and perform certain actions on, declared entities such as types, objects, and subprograms.
For example, you can determine the first and last bounds of scalar types, get the sizes
of objects and types, and convert values to and from strings. This section provides an
overview of how attributes work. For more information on the many attributes defined by
the language, you can refer directly to the Ada Language Reference Manual.
The 'Image and 'Value attributes allow you to transform a scalar value into a String and
vice-versa. For example:
declare
A : Integer := 99;
begin
Put_Line (Integer'Image (A));
(continues on next page)
Certain attributes are provided only for certain kinds of types. For example, the 'Val and
'Pos attributes for an enumeration type associates a discrete value with its position among
its peers. One circuitous way of moving to the next character of the ASCII table is:
[Ada]
declare
C : Character := 'a';
begin
C := Character'Val (Character'Pos (C) + 1);
end;
A more concise way to get the next value in Ada is to use the 'Succ attribute:
declare
C : Character := 'a';
begin
C := Character'Succ (C);
end;
You can get the previous value using the 'Pred attribute. Here is the equivalent in C++
and Java:
[C++]
char c = 'a';
c++;
[Java]
char c = 'a';
c++;
Other interesting examples are the 'First and 'Last attributes which, respectively, return
the first and last values of a scalar type. Using 32-bit integers, for instance, Integer'First
returns -231 and Integer'Last returns 231 - 1.
C++ arrays are pointers with offsets, but the same is not the case for Ada and Java. Arrays
in the latter two languages are not interchangable with operations on pointers, and array
types are considered first-class citizens. Arrays in Ada have dedicated semantics such as
the availability of the array's boundaries at run-time. Therefore, unhandled array overflows
are impossible unless checks are suppressed. Any discrete type can serve as an array
index, and you can specify both the starting and ending bounds — the lower bound doesn't
necessarily have to be 0. Most of the time, array types need to be explicitly declared prior
to the declaration of an object of that array type.
Here's an example of declaring an array of 26 characters, initializing the values from 'a'
to 'z':
[Ada]
declare
type Arr_Type is array (Integer range <>) of Character;
(continues on next page)
[C++]
[Java]
In C++ and Java, only the size of the array is given during declaration. In Ada, array index
ranges are specified using two values of a discrete type. In this example, the array type
declaration specifies the use of Integer as the index type, but does not provide any con-
straints (use <>, pronounced box, to specify "no constraints"). The constraints are defined
in the object declaration to be 1 to 26, inclusive. Arrays have an attribute called 'Range. In
our example, Arr'Range can also be expressed as Arr'First .. Arr'Last; both expres-
sions will resolve to 1 .. 26. So the 'Range attribute supplies the bounds for our for loop.
There is no risk of stating either of the bounds incorrectly, as one might do in C++ where
I <= 26 may be specified as the end-of-loop condition.
As in C++, Ada Strings are arrays of Characters. The C++ or Java String class is the
equivalent of the Ada type Ada.Strings.Unbounded_String which offers additional capa-
bilities in exchange for some overhead. Ada strings, importantly, are not delimited with
the special character '\0' like they are in C++. It is not necessary because Ada uses the
array's bounds to determine where the string starts and stops.
Ada's predefined String type is very straighforward to use:
Unlike C++ and Java, Ada does not offer escape sequences such as '\n'. Instead, explicit
values from the ASCII package must be concatenated (via the concatenation operator, &).
Here for example, is how to initialize a line of text ending with a new line:
You see here that no constraints are necessary for this variable definition. The initial value
given allows the automatic determination of My_String's bounds.
Ada offers high-level operations for copying, slicing, and assigning values to arrays. We'll
start with assignment. In C++ or Java, the assignment operator doesn't make a copy of the
value of an array, but only copies the address or reference to the target variable. In Ada,
the actual array contents are duplicated. To get the above behavior, actual pointer types
would have to be defined and used.
[Ada]
declare
type Arr_Type is array (Integer range <>) of Integer;
A1 : Arr_Type (1 .. 2);
A2 : Arr_Type (1 .. 2);
begin
A1 (1) := 0;
A1 (2) := 1;
A2 := A1;
end;
[C++]
int A1 [2];
int A2 [2];
A1 [0] = 0;
A1 [1] = 1;
[Java]
A1 [0] = 0;
A1 [1] = 1;
A2 = Arrays.copyOf(A1, A1.length);
In all of the examples above, the source and destination arrays must have precisely the
same number of elements. Ada allows you to easily specify a portion, or slice, of an array.
So you can write the following:
[Ada]
declare
type Arr_Type is array (Integer range <>) of Integer;
A1 : Arr_Type (1 .. 10);
A2 : Arr_Type (1 .. 5);
begin
A2 (1 .. 3) := A1 (4 .. 6);
end;
This assigns the 4th, 5th, and 6th elements of A1 into the 1st, 2nd, and 3rd elements of A2.
Note that only the length matters here: the values of the indexes don't have to be equal;
they slide automatically.
Ada also offers high level comparison operations which compare the contents of arrays as
opposed to their addresses:
[Ada]
declare
type Arr_Type is array (Integer range <>) of Integer;
(continues on next page)
[C++]
int A1 [2];
int A2 [2];
bool eq = true;
if (eq) {
[Java]
int [] A1 = new int [2];
int [] A2 = new int [2];
You can assign to all the elements of an array in each language in different ways. In Ada,
the number of elements to assign can be determined by looking at the right-hand side,
the left-hand side, or both sides of the assignment. When bounds are known on the left-
hand side, it's possible to use the others expression to define a default value for all the
unspecified array elements. Therefore, you can write:
declare
type Arr_Type is array (Integer range <>) of Integer;
A1 : Arr_Type := (1, 2, 3, 4, 5, 6, 7, 8, 9);
A2 : Arr_Type (-2 .. 42) := (others => 0);
begin
A1 := (1, 2, 3, others => 10);
In Ada, there's no distinction between struct and class as there is in C++. All heteroge-
neous data structures are records. Here are some simple records:
[Ada]
declare
type R is record
A, B : Integer;
C : Float;
end record;
V : R;
(continues on next page)
[C++]
struct R {
int A, B;
float C;
};
R V;
V.A = 0;
[Java]
class R {
public int A, B;
public float C;
}
R V = new R ();
V.A = 0;
Ada allows specification of default values for fields just like C++ and Java. The values spec-
ified can take the form of an ordered list of values, a named list of values, or an incomplete
list followed by others => <> to specify that fields not listed will take their default values.
For example:
type R is record
A, B : Integer := 0;
C : Float := 0.0;
end record;
V1 : R := (1, 2, 1.0);
V2 : R := (A => 1, B => 2, C => 1.0);
V3 : R := (C => 1.0, A => 1, B => 2);
V4 : R := (C => 1.0, others => <>);
56.9 Pointers
Pointers, references, and access types differ in significant ways across the languages that
we are examining. In C++, pointers are integral to a basic understanding of the language,
from array manipulation to proper declaration and use of function parameters. In Java,
direct pointer manipulation is abstracted by the Java runtime. And in Ada, direct pointer
manipulation is possible, but unlike C++, they are not required for basic usage with arrays
and parameter passing.
We'll continue this section by explaining the difference between objects allocated on the
stack and objects allocated on the heap using the following example:
[Ada]
declare
type R is record
A, B : Integer;
end record;
[C++]
struct R {
int A, B;
};
R V1, V2;
V1.A = 0;
V2 = V1;
V2.A = 1;
[Java]
public class R {
public int A, B;
}
R V1, V2;
V1 = new R ();
V1.A = 0;
V2 = V1;
V2.A = 1;
There's a fundamental difference between the Ada and C++ semantics above and the se-
mantics for Java. In Ada and C++, objects are allocated on the stack and are directly
accessed. V1 and V2 are two different objects and the assignment statement copies the
value of V1 into V2. In Java, V1 and V2 are two references to objects of class R. Note that
when V1 and V2 are declared, no actual object of class R yet exists in memory: it has to
be allocated later with the new allocator operator. After the assignment V2 = V1, there's
only one R object in memory: the assignment is a reference assignment, not a value as-
signment. At the end of the Java code, V1 and V2 are two references to the same objects
and the V2.A = 1 statement changes the field of that one object, while in the Ada and the
C++ case V1 and V2 are two distinct objects.
To obtain similar behavior in Ada, you can use pointers. It can be done through Ada's access
type:
[Ada]
declare
type R is record
A, B : Integer;
end record;
type R_Access is access R;
V1 : R_Access;
V2 : R_Access;
begin
V1 := new R;
V1.A := 0;
V2 := V1;
V2.A := 1;
end;
[C++]
struct R {
int A, B;
};
R * V1, * V2;
V1 = new R ();
V1->A = 0;
V2 = V1;
V2->A = 0;
For those coming from the Java world: there's no garbage collector in Ada, so objects allo-
cated by the new operator need to be expressly freed.
Dereferencing is performed automatically in certain situations, for instance when it is clear
that the type required is the dereferenced object rather than the pointer itself, or when ac-
cessing record members via a pointer. To explicitly dereference an access variable, append
.all. The equivalent of V1->A in C++ can be written either as V1.A or V1.all.A.
Pointers to scalar objects in Ada and C++ look like:
[Ada]
procedure Main is
type A_Int is access Integer;
Var : A_Int := new Integer;
begin
Var.all := 0;
end Main;
[C++]
When using Ada pointers to reference objects on the stack, the referenced objects must
be declared as being aliased. This directs the compiler to implement the object using a
memory region, rather than using registers or eliminating it entirely via optimization. The
access type needs to be declared as either access all (if the referenced object needs to
be assigned to) or access constant (if the referenced object is a constant). The 'Access
attribute works like the C++ & operator to get a pointer to the object, but with a "scope
accessibility" check to prevent references to objects that have gone out of scope. For ex-
ample:
[Ada]
[C++]
int Var;
int * Ptr = &Var;
To deallocate objects from the heap in Ada, it is necessary to use a deallocation subprogram
that accepts a specific access type. A generic procedure is provided that can be customized
to fit your needs — it's called Ada.Unchecked_Deallocation. To create your customized
deallocator (that is, to instantiate this generic), you must provide the object type as well as
the access type as follows:
[Ada]
with Ada.Unchecked_Deallocation;
procedure Main is
type Integer_Access is access all Integer;
procedure Free is new Ada.Unchecked_Deallocation (Integer, Integer_Access);
My_Pointer : Integer_Access := new Integer;
begin
Free (My_Pointer);
end Main;
[C++]
FIFTYSEVEN
Subroutines in C++ and Java are always expressed as functions (methods) which may or
may not return a value. Ada explicitly differentiates between functions and procedures.
Functions must return a value and procedures must not. Ada uses the more general term
"subprogram" to refer to both functions and procedures.
Parameters can be passed in three distinct modes: in, which is the default, is for input
parameters, whose value is provided by the caller and cannot be changed by the subpro-
gram. out is for output parameters, with no initial value, to be assigned by the subprogram
and returned to the caller. in out is a parameter with an initial value provided by the
caller, which can be modified by the subprogram and returned to the caller (more or less
the equivalent of a non-constant reference in C++). Ada also provides access parameters,
in effect an explicit pass-by-reference indicator.
In Ada the programmer specifies how the parameter will be used and in general the compiler
decides how it will be passed (i.e., by copy or by reference). (There are some exceptions
to the "in general". For example, parameters of scalar types are always passed by copy,
for all three modes.) C++ has the programmer specify how to pass the parameter, and
Java forces primitive type parameters to be passed by copy and all other parameters to be
passed by reference. For this reason, a 1:1 mapping between Ada and Java isn't obvious
but here's an attempt to show these differences:
[Ada]
procedure Proc
(Var1 : Integer;
Var2 : out Integer;
Var3 : in out Integer);
procedure Proc
(Var1 : Integer;
Var2 : out Integer;
Var3 : in out Integer)
is
begin
Var2 := Func (Var1);
Var3 := Var3 + 1;
end Proc;
1303
Learning Ada
[C++]
void Proc
(int Var1,
int & Var2,
int & Var3);
void Proc
(int Var1,
int & Var2,
int & Var3) {
[Java]
The first two declarations for Proc and Func are specifications of the subprograms which are
being provided later. Although optional here, it's still considered good practice to separately
define specifications and implementations in order to make it easier to read the program.
In Ada and C++, a function that has not yet been seen cannot be used. Here, Proc can call
Func because its specification has been declared. In Java, it's fine to have the declaration
of the subprogram later .
Parameters in Ada subprogram declarations are separated with semicolons, because com-
mas are reserved for listing multiple parameters of the same type. Parameter declaration
syntax is the same as variable declaration syntax, including default values for parame-
ters. If there are no parameters, the parentheses must be omitted entirely from both the
declaration and invocation of the subprogram.
57.2 Overloading
Different subprograms may share the same name; this is called "overloading." As long as
the subprogram signatures (subprogram name, parameter types, and return types) are dif-
ferent, the compiler will be able to resolve the calls to the proper destinations. For example:
The Ada compiler knows that an assignment to V requires an Integer. So, it chooses the
Value function that returns an Integer to satisfy this requirement.
Operators in Ada can be treated as functions too. This allows you to define local operators
that override operators defined at an outer scope, and provide overloaded operators that
operate on and compare different types. To express an operator as a function, enclose it in
quotes:
[Ada]
[C++]
You can express the expected inputs and outputs of subprograms by specifying subprogram
contracts. The compiler can then check for valid conditions to exist when a subprogram is
called and can check that the return value makes sense. Ada allows defining contracts in
the form of Pre and Post conditions; this facility was introduced in Ada 2012. They look
like:
The above example adds a Pre condition, stating that Right cannot be equal to 0.0. While
the IEEE floating point standard permits divide-by-zero, you may have determined that use
of the result could still lead to issues in a particular application. Writing a contract helps
to detect this as early as possible. This declaration also provides a Post condition on the
result.
Postconditions can also be expressed relative to the value of the input:
V'Old in the postcondition represents the value that V had before entering Increment.
FIFTYEIGHT
PACKAGES
The package is the basic modularization unit of the Ada language, as is the class for Java and
the header and implementation pair for C++. An Ada package contains three parts that, for
GNAT, are separated into two files: .ads files contain public and private Ada specifications,
and .adb files contain the implementation, or Ada bodies.
Java doesn't provide any means to cleanly separate the specification of methods from their
implementation: they all appear in the same file. You can use interfaces to emulate having
separate specifications, but this requires the use of OOP techniques which is not always
practical.
Ada and C++ do offer separation between specifications and implementations out of the
box, independent of OOP.
package Package_Name is
-- public specifications
private
-- private specifications
end Package_Name;
Private types are useful for preventing the users of a package's types from depending on the
types' implementation details. The private keyword splits the package spec into "public"
and "private" parts. That is somewhat analogous to C++'s partitioning of the class construct
into different sections with different visibility properties. In Java, the encapsulation has to
be done field by field, but in Ada the entire definition of a type can be hidden. For example:
package Types is
type Type_1 is private;
type Type_2 is private;
type Type_3 is private;
procedure P (X : Type_1);
...
private
procedure Q (Y : Type_1);
type Type_1 is new Integer range 1 .. 1000;
type Type_2 is array (Integer range 1 .. 1000) of Integer;
type Type_3 is record
A, B : Integer;
end record;
end Types;
1307
Learning Ada
Subprograms declared above the private separator (such as P) will be visible to the pack-
age user, and the ones below (such as Q) will not. The body of the package, the implemen-
tation, has access to both parts.
Ada packages can be organized into hierarchies. A child unit can be declared in the following
way:
-- root-child.ads
package Root.Child is
-- package spec goes here
end Root.Child;
-- root-child.adb
Here, Root.Child is a child package of Root. The public part of Root.Child has access to
the public part of Root. The private part of Child has access to the private part of Root,
which is one of the main advantages of child packages. However, there is no visibility
relationship between the two bodies. One common way to use this capability is to define
subsystems around a hierarchical naming scheme.
Entities declared in the visible part of a package specification can be made accessible using
a with clause that references the package, which is similar to the C++ #include directive.
Visibility is implicit in Java: you can always access all classes located in your CLASSPATH.
After a with clause, entities needs to be prefixed by the name of their package, like a C++
namespace or a Java package. This prefix can be omitted if a use clause is employed,
similar to a C++ using namespace or a Java import.
[Ada]
-- pck.ads
package Pck is
My_Glob : Integer;
end Pck;
-- main.adb
with Pck;
procedure Main is
begin
Pck.My_Glob := 0;
end Main;
[C++]
// pck.h
namespace pck {
extern int myGlob;
}
// pck.cpp
namespace pck {
int myGlob;
}
// main.cpp
#include "pck.h"
[Java]
// Globals.java
package pck;
// Main.java
FIFTYNINE
Primitive subprograms in Ada are basically the subprograms that are eligible for inheritance
/ derivation. They are the equivalent of C++ member functions and Java instance methods.
While in C++ and Java these subprograms are located within the nested scope of the type, in
Ada they are simply declared in the same scope as the type. There's no syntactic indication
that a subprogram is a primitive of a type.
The way to determine whether P is a primitive of a type T is if
1. it is declared in the same scope as T, and
2. it contains at least one parameter of type T, or returns a result of type T.
In C++ or Java, the self reference this is implicitly declared. It may need to be explicitly
stated in certain situations, but usually it's omitted. In Ada the self-reference, called the
controlling parameter, must be explicitly specified in the subprogram parameter list. While
it can be any parameter in the profile with any name, we'll focus on the typical case where
the first parameter is used as the self parameter. Having the controlling parameter listed
first also enables the use of OOP prefix notation which is convenient.
A class in C++ or Java corresponds to a tagged type in Ada. Here's an example of the
declaration of an Ada tagged type with two parameters and some dispatching and non-
dispatching primitives, with equivalent examples in C++ and Java:
[Ada]
procedure P2 (V : T_Access);
[C++]
class T {
public:
int V, W;
int F ();
void P1 ();
(continues on next page)
1311
Learning Ada
void P2 (T * v);
[Java]
public class T {
public int V, W;
Note that P2 is not a primitive of T — it does not have any parameters of type T. Its param-
eter is of type T_Access, which is a different type.
Once declared, primitives can be called like any subprogram with every necessary param-
eter specified, or called using prefix notation. For example:
[Ada]
declare
V : T;
begin
V.P1;
end;
[C++]
{
T v;
v.P1 ();
}
[Java]
{
T v = new T ();
v.P1 ();
}
overriding
procedure Method_1 (Self : Child);
[C++]
class Root {
public:
int f1;
virtual void method1 ();
};
[Java]
Like Java, Ada primitives on tagged types are always subject to dispatching; there is no
need to mark them virtual. Also like Java, there's an optional keyword overriding to
ensure that a method is indeed overriding something from the parent type.
Unlike many other OOP languages, Ada differentiates between a reference to a specific
tagged type, and a reference to an entire tagged type hierarchy. While Root is used to
mean a specific type, Root'Class — a class-wide type — refers to either that type or any
of its descendants. A method using a parameter of such a type cannot be overridden, and
must be passed a parameter whose type is of any of Root's descendants (including Root
itself).
Next, we'll take a look at how each language finds the appropriate method to call within
an OO class hierarchy; that is, their dispatching rules. In Java, calls to non-private in-
stance methods are always dispatching. The only case where static selection of an instance
method is possible is when calling from a method to the super version.
In C++, by default, calls to virtual methods are always dispatching. One common mistake is
to use a by-copy parameter hoping that dispatching will reach the real object. For example:
proc (*v);
In the above code, p.method1() will not dispatch. The call to proc makes a copy of the
Root part of v, so inside proc, p.method1() refers to the method1() of the root object. The
intended behavior may be specified by using a reference instead of a copy:
proc (*v);
In Ada, tagged types are always passed by reference but dispatching only occurs on class-
wide types. The following Ada code is equivalent to the latter C++ example:
declare
procedure Proc (P : Root'Class) is
begin
P.Method_1;
end;
Dispatching from within primitives can get tricky. Let's consider a call to Method_1 in the
implementation of Method_2. The first implementation that might come to mind is:
However, Method_2 is called with a parameter that is of the definite type Root. More pre-
cisely, it is a definite view of a child. So, this call is not dispatching; it will always call
Method_1 of Root even if the object passed is a child of Root. To fix this, a view conversion
is necessary:
This is called "redispatching." Be careful, because this is the most common mistake made
in Ada when using OOP. In addition, it's possible to convert from a class wide view to a
definite view, and to select a given primitive, like in C++:
[Ada]
[C++]
Ada does not have constructors and destructors in quite the same way as C++ and Java, but
there is analagous functionality in Ada in the form of default initialization and finalization.
Default initialization may be specified for a record component and will occur if a variable of
the record type is not assigned a value at initialization. For example:
V1 : T;
V2 : T := (F => 0);
In the declaration of V1, T.F receives a value computed by the subprogram Com-
pute_Default_F. This is part of the default initialization. V2 is initialized manually and
thus will not use the default initialization.
For additional expressive power, Ada provides a type called Ada.Finalization.
Controlled from which you can derive your own type. Then, by overriding the Initialize
procedure you can create a constructor for the type:
V1 : T;
V2 : T := (F => 0);
Again, this default initialization subprogram is only called for V1; V2 is initialized manu-
ally. Furthermore, unlike a C++ or Java constructor, Initialize is a normal subprogram
and does not perform any additional initialization such as calling the parent's initialization
routines.
When deriving from Controlled, it's also possible to override the subprogram Finalize,
which is like a destructor and is called for object finalization. Like Initialize, this is a
regular subprogram. Do not expect any other finalizers to be automatically invoked for
you.
Controlled types also provide functionality that essentially allows overriding the meaning of
the assignment operation, and are useful for defining types that manage their own storage
reclamation (for example, implementing a reference count reclamation strategy).
59.4 Encapsulation
While done at the class level for C++ and Java, Ada encapsulation occurs at the package
level and targets all entities of the language, as opposed to only methods and attributes.
For example:
[Ada]
package Pck is
type T is tagged private;
procedure Method1 (V : T);
private
type T is tagged record
F1, F2 : Integer;
end record;
procedure Method2 (V : T);
end Pck;
[C++]
class T {
public:
virtual void method1 ();
protected:
int f1, f2;
virtual void method2 ();
};
[Java]
public class T {
public void method1 ();
protected int f1, f2;
protected void method2 ();
}
The C++ and Java code's use of protected and the Ada code's use of private here demon-
strates how to map these concepts between languages. Indeed, the private part of an Ada
child package would have visibility of the private part of its parents, mimicking the notion
of protected. Only entities declared in the package body are completely isolated from
access.
Ada, C++ and Java all offer similar functionality in terms of abstract classes, or pure virtual
classes. It is necessary in Ada and Java to explicitly specify whether a tagged type or class
is abstract, whereas in C++ the presence of a pure virtual function implicitly makes the
class an abstract base class. For example:
[Ada]
package P is
end P;
[C++]
class T {
public:
virtual void method () = 0;
protected:
int f1, f2;
};
[Java]
All abstract methods must be implemented when implementing a concrete type based on
an abstract type.
Ada doesn't offer multiple inheritance the way C++ does, but it does support a Java-like
notion of interfaces. An interface is like a C++ pure virtual class with no attributes and only
abstract members. While an Ada tagged type can inherit from at most one tagged type, it
may implement multiple interfaces. For example:
[Ada]
type I1 is interface;
procedure M2 (Self : I1) is abstract;
type I2 is interface;
procedure M3 (Self : I2) is abstract;
[C++]
class Root {
public:
virtual void M1();
int f1;
};
class I1 {
public:
virtual void M2 () = 0;
(continues on next page)
class I2 {
public:
virtual void M3 () = 0;
};
[Java]
public class Root {
public void M1();
public int f1;
}
public interface I1 {
public void M2 ();
}
public interface I2 {
public void M3 ();
}
59.6 Invariants
Any private type in Ada may be associated with a Type_Invariant contract. An invariant
is a property of a type that must always be true after the return from of any of its primitive
subprograms. (The invariant might not be maintained during the execution of the primitive
subprograms, but will be true after the return.) Let's take the following example:
package Int_List_Pkg is
end Int_List_Pkg;
The Is_Sorted function checks that the type stays consistent. It will be called at the exit of
every primitive above. It is permissible if the conditions of the invariant aren't met during
execution of the primitive. In To_Int_List for example, if the source array is not in sorted
order, the invariant will not be satisfied at the "begin", but it will be checked at the end.
SIXTY
GENERICS
Ada, C++, and Java all have support for generics or templates, but on different sets of
language entities. A C++ template can be applied to a class or a function. So can a Java
generic. An Ada generic can be either a package or a subprogram.
In this example, we will swap two generic objects. This is possible in Ada and C++ using
a temporary variable. In Java, parameters are a copy of a reference value that is passed
into the function, so modifying those references in the function scope has no effect from
the caller's context. A generic swap method, like the below Ada or C++ examples is not
possible in Java, so we will skip the Java version of this example.
[Ada]
generic
type A_Type is private;
procedure Swap (Left, Right : in out A_Type) is
Temp : A_Type := Left;
begin
Left := Right;
Right := Temp;
end Swap;
[C++]
declare
type R is record
F1, F2 : Integer;
end record;
1321
Learning Ada
[C++]
class R {
public:
int f1, f2;
};
R a, b;
...
swap (a, b);
The C++ template becomes usable once defined. The Ada generic needs to be explicitly
instantiated using a local name and the generic's parameters.
Next, we're going to create a generic unit containing data and subprograms. In Java or
C++, this is done through a class, while in Ada, it's a generic package. The Ada and C++
model is fundamentally different from the Java model. Indeed, upon instantiation, Ada and
C++ generic data are duplicated; that is, if they contain global variables (Ada) or static
attributes (C++), each instance will have its own copy of the variable, properly typed and
independent from the others. In Java, generics are only a mechanism to have the compiler
do consistency checks, but all instances are actually sharing the same data where the
generic parameters are replaced by java.lang.Object. Let's look at the following example:
[Ada]
generic
type T is private;
package Gen is
type C is tagged record
V : T;
end record;
G : Integer;
end Gen;
[C++]
[Java]
In all three cases, there's an instance variable (v) and a static variable (G). Let's now look
at the behavior (and syntax) of these three instantiations:
[Ada]
declare
package I1 is new Gen (Integer);
package I2 is new Gen (Integer);
subtype Str10 is String (1..10);
package I3 is new Gen (Str10);
begin
I1.G := 0;
I2.G := 1;
I3.G := 2;
end;
[C++]
C <int>::G = 0;
C <int>::G = 1;
C <char *>::G = 2;
[Java]
C.G = 0;
C.G = 1;
C.G = 2;
In the Java case, we access the generic entity directly without using a parametric type. This
is because there's really only one instance of C, with each instance sharing the same global
variable G. In C++, the instances are implicit, so it's not possible to create two different
instances with the same parameters. The first two assignments are manipulating the same
global while the third one is manipulating a different instance. In the Ada case, the three
instances are explicitly created, named, and referenced individually.
Ada offers a wide variety of generic parameters which is difficult to translate into other
languages. The parameters used during instantiation — and as a consequence those on
which the generic unit may rely on — may be variables, types, or subprograms with certain
properties. For example, the following provides a sort algorithm for any kind of array:
generic
type Component is private;
type Index is (<>);
with function "<" (Left, Right : Component) return Boolean;
type Array_Type is array (Index range <>) of Component;
procedure Sort (A : in out Array_Type);
The above declaration states that we need a type (Component), a discrete type (Index),
a comparison subprogram ("<"), and an array definition (Array_Type). Given these, it's
possible to write an algorithm that can sort any Array_Type. Note the usage of the with
reserved word in front of the function name, to differentiate between the generic parameter
and the beginning of the generic subprogram.
Here is a non-exhaustive overview of the kind of constraints that can be put on types:
type T is private; -- T is a constrained type, such as Integer
type T (<>) is private; -- T can be an unconstrained type, such as String
type T is tagged private; -- T is a tagged type
type T is new T2 with private; -- T is an extension of T2
type T is (<>); -- T is a discrete type
(continues on next page)
SIXTYONE
EXCEPTIONS
Exceptions are a mechanism for dealing with run-time occurrences that are rare, that usu-
ally correspond to errors (such as improperly formed input data), and whose occurrence
causes an unconditional transfer of control.
Compared with Java and C++, the notion of an Ada exception is very simple. An exception
in Ada is an object whose "type" is exception, as opposed to classes in Java or any type in
C++. The only piece of user data that can be associated with an Ada exception is a String.
Basically, an exception in Ada can be raised, and it can be handled; information associated
with an occurrence of an exception can be interrogated by a handler.
Ada makes heavy use of exceptions especially for data consistency check failures at run
time. These include, but are not limited to, checking against type ranges and array bound-
aries, null pointers, various kind of concurrency properties, and functions not returning a
value. For example, the following piece of code will raise the exception Constraint_Error:
procedure P is
V : Positive;
begin
V := -1;
end P;
In the above code, we're trying to assign a negative value to a variable that's declared to
be positive. The range check takes place during the assignment operation, and the failure
raises the Constraint_Error exception at that point. (Note that the compiler may give a
warning that the value is out of range, but the error is manifest as a run-time exception.)
Since there is no local handler, the exception is propagated to the caller; if P is the main
procedure, then the program will be terminated.
Java and C++ can throw and catch exceptions when trying code. All Ada code is already
implicitly within try blocks, and exceptions are raised and handled.
[Ada]
begin
Some_Call;
exception
when Exception_1 =>
Put_Line ("Error 1");
when Exception_2 =>
Put_Line ("Error 2");
when others =>
Put_Line ("Unknown error");
end;
1325
Learning Ada
[C++]
try {
someCall ();
} catch (Exception1) {
cout << "Error 1" << endl;
} catch (Exception2) {
cout << "Error 2" << endl;
} catch (...) {
cout << "Unknown error" << endl;
}
[Java]
try {
someCall ();
} catch (Exception1 e1) {
System.out.println ("Error 1");
} catch (Exception2 e2) {
System.out.println ("Error 2");
} catch (Throwable e3) {
System.out.println ("Unknown error");
}
Custom exception declarations resemble object declarations, and they can be created in
Ada using the exception keyword:
My_Exception : exception;
Your exceptions can then be raised using a raise statement, optionally accompanied by a
message following the with reserved word:
[Ada]
[C++]
[Java]
raise Constraint_Error;
SIXTYTWO
CONCURRENCY
62.1 Tasks
Java and Ada both provide support for concurrency in the language. The C++ language has
added a concurrency facility in its most recent revision, C++11, but we are assuming that
most C++ programmers are not (yet) familiar with these new features. We thus provide
the following mock API for C++ which is similar to the Java Thread class:
class Thread {
public:
virtual void run (); // code to execute
void start (); // starts a thread and then call run ()
void join (); // waits until the thread is finished
};
Each of the following examples will display the 26 letters of the alphabet twice, using two
concurrent threads/tasks. Since there is no synchronization between the two threads of
control in any of the examples, the output may be interspersed.
[Ada]
procedure Main is -- implicitly called by the environment task
task My_Task;
[C++]
class MyThread : public Thread {
public:
void run () {
for (char i = 'A'; i <= 'Z'; ++i) {
cout << i << endl;
}
}
};
1327
Learning Ada
myTask.join ();
return 0;
}
[Java]
Any number of Ada tasks may be declared in any declarative region. A task declaration
is very similar to a procedure or package declaration. They all start automatically when
control reaches the begin. A block will not exit until all sequences of statements defined
within that scope, including those in tasks, have been completed.
A task type is a generalization of a task object; each object of a task type has the same
behavior. A declared object of a task type is started within the scope where it is declared,
and control does not leave that scope until the task has terminated.
An Ada task type is somewhat analogous to a Java Thread subclass, but in Java the instances
of such a subclass are always dynamically allocated. In Ada an instance of a task type may
either be declared or dynamically allocated.
Task types can be parametrized; the parameter serves the same purpose as an argument
to a constructor in Java. The following example creates 10 tasks, each of which displays a
subset of the alphabet contained between the parameter and the 'Z' Character. As with
the earlier example, since there is no synchronization among the tasks, the output may be
interspersed depending on the implementation's task scheduling algorithm.
[Ada]
procedure Main is
Tab : array (0 .. 9) of My_Task ('G');
begin
null;
end Main;
[C++]
char first;
void run () {
for (char i = first; i <= 'Z'; ++i) {
cout << i << endl;
}
}
};
return 0;
}
[Java]
In Ada a task may be allocated on the heap as opposed to the stack. The task will then start
as soon as it has been allocated, and terminates when its work is completed. This model is
probably the one that's the most similar to Java:
[Ada]
procedure Main is
T : Ptr_Task;
begin
T := new My_Task ('G');
end Main;
[C++]
[Java]
t.start ();
}
}
62.2 Rendezvous
procedure Main is
task After is
entry Go;
end After ;
begin
Put_Line ("Before");
After.Go;
end;
The Go entry declared in After is the external interface to the task. In the task body, the
accept statement causes the task to wait for a call on the entry. This particular entry and
accept pair doesn't do much more than cause the task to wait until Main calls After.Go.
So, even though the two tasks start simultaneously and execute independently, they can
coordinate via Go. Then, they both continue execution independently after the rendezvous.
The entry/accept pair can take/pass parameters, and the accept statement can contain a
sequence of statements; while these statements are executed, the caller is blocked.
Let's look at a more ambitious example. The rendezvous below accepts parameters and
executes some code:
procedure Main is
task After is
entry Go (Text : String);
end After ;
begin
Put_Line ("Before");
After.Go ("Main");
end;
In the above example, the Put_Line is placed in the accept statement. Here's a possible
execution trace, assuming a uniprocessor:
1. At the begin of Main, task After is started and the main procedure is suspended.
2. After reaches the accept statement and is suspended, since there is no pending call
on the Go entry.
3. The main procedure is awakened and executes the Put_Line invocation, displaying
the string "Before".
4. The main procedure calls the Go entry. Since After is suspended on its accept state-
ment for this entry, the call succeeds.
5. The main procedure is suspended, and the task After is awakened to execute the
body of the accept statement. The actual parameter "Main" is passed to the accept
statement, and the Put_Line invocation is executed. As a result, the string "After:
Main" is displayed.
6. When the accept statement is completed, both the After task and the main proce-
dure are ready to run. Suppose that the Main procedure is given the processor. It
reaches its end, but the local task After has not yet terminated. The main procedure
is suspended.
7. The After task continues, and terminates since it is at its end. The main procedure is
resumed, and it too can terminate since its dependent task has terminated.
The above description is a conceptual model; in practice the implementation can perform
various optimizations to avoid unnecessary context switches.
The accept statement by itself can only wait for a single event (call) at a time. The select
statement allows a task to listen for multiple events simultaneously, and then to deal with
the first event to occur. This feature is illustrated by the task below, which maintains an
integer value that is modified by other tasks that call Increment, Decrement, and Get:
task Counter is
entry Get (Result : out Integer);
entry Increment;
entry Decrement;
end Counter;
When the task's statement flow reaches the select, it will wait for all four events — three
entries and a delay — in parallel. If the delay of one minute is exceeded, the task will
execute the statements following the delay statement (and in this case will exit the loop,
in effect terminating the task). The accept bodies for the Increment, Decrement, or Get
entries will be otherwise executed as they're called. These four sections of the select
statement are mutually exclusive: at each iteration of the loop, only one will be invoked.
This is a critical point; if the task had been written as a package, with procedures for the
various operations, then a "race condition" could occur where multiple tasks simultaneously
calling, say, Increment, cause the value to only get incremented once. In the tasking
version, if multiple tasks simultaneously call Increment then only one at a time will be
accepted, and the value will be incremented by each of the tasks when it is accepted.
More specifically, each entry has an associated queue of pending callers. If a task calls one
of the entries and Counter is not ready to accept the call (i.e., if Counter is not suspended
at the select statement) then the calling task is suspended, and placed in the queue of
the entry that it is calling. From the perspective of the Counter task, at any iteration of the
loop there are several possibilities:
• There is no call pending on any of the entries. In this case Counter is suspended. It
will be awakened by the first of two events: a call on one of its entries (which will then
be immediately accepted), or the expiration of the one minute delay (whose effect
was noted above).
• There is a call pending on exactly one of the entries. In this case control passes to the
select branch with an accept statement for that entry. The choice of which caller to
accept, if more than one, depends on the queuing policy, which can be specified via
a pragma defined in the Real-Time Systems Annex of the Ada standard; the default is
First-In First-Out.
• There are calls pending on more than one entry. In this case one of the entries with
pending callers is chosen, and then one of the callers is chosen to be de-queued (the
choices depend on the queueing policy).
Although the rendezvous may be used to implement mutually exclusive access to a shared
data object, an alternative (and generally preferable) style is through a protected object,
an efficiently implementable mechanism that makes the effect more explicit. A protected
object has a public interface (its protected operations) for accessing and manipulating the
object's components (its private part). Mutual exclusion is enforced through a conceptual
lock on the object, and encapsulation ensures that the only external access to the compo-
nents are through the protected operations.
Two kinds of operations can be performed on such objects: read-write operations by pro-
cedures or entries, and read-only operations by functions. The lock mechanism is imple-
mented so that it's possible to perform concurrent read operations but not concurrent write
or read/write operations.
Let's reimplement our earlier tasking example with a protected object called Counter:
protected Counter is
function Get return Integer;
procedure Increment;
procedure Decrement;
private
Value : Integer := 0;
end Counter;
procedure Increment is
begin
Value := Value + 1;
end Increment;
procedure Decrement is
begin
Value := Value - 1;
end Decrement;
end Counter;
Having two completely different ways to implement the same paradigm might seem compli-
cated. However, in practice the actual problem to solve usually drives the choice between
an active structure (a task) or a passive structure (a protected object).
A protected object can be accessed through prefix notation:
Counter.Increment;
Counter.Decrement;
Put_Line (Integer'Image (Counter.Get));
A protected object may look like a package syntactically, since it contains declarations that
can be accessed externally using prefix notation. However, the declaration of a protected
object is extremely restricted; for example, no public data is allowed, no types can be
declared inside, etc. And besides the syntactic differences, there is a critical semantic
distinction: a protected object has a conceptual lock that guarantees mutual exclusion;
there is no such lock for a package.
Like tasks, it's possible to declare protected types that can be instantiated several times:
declare
protected type Counter is
-- as above
end Counter;
C1 : Counter;
C2 : Counter;
begin
C1.Increment;
C2.Decrement;
...
end;
Protected objects and types can declare a procedure-like operation known as an "entry".
An entry is somewhat similar to a procedure but includes a so-called barrier condition that
must be true in order for the entry invocation to succeed. Calling a protected entry is
thus a two step process: first, acquire the lock on the object, and then evaluate the barrier
condition. If the condition is true then the caller will execute the entry body. If the condition
is false, then the caller is placed in the queue for the entry, and relinquishes the lock.
Barrier conditions (for entries with non-empty queues) are reevaluated upon completion of
protected procedures and protected entries.
Here's an example illustrating protected entries: a protected type that models a binary
semaphore / persistent signal.
procedure Signal is
(continues on next page)
Ada concurrency features provide much further generality than what's been presented here.
For additional information please consult one of the works cited in the References section.
SIXTYTHREE
We've seen in the previous chapters how Ada can be used to describe high level semantics
and architecture. The beauty of the language, however, is that it can be used all the way
down to the lowest levels of the development, including embedded assembly code or bit-
level data management.
One very interesting feature of the language is that, unlike C, for example, there are no data
representation constraints unless specified by the developer. This means that the compiler
is free to choose the best trade-off in terms of representation vs. performance. Let's start
with the following example:
[Ada]
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record
with Pack;
[C++]
struct R {
unsigned int v:8;
bool b1;
bool b2;
};
[Java]
public class R {
public byte v;
public boolean b1;
public boolean b2;
}
The Ada and the C++ code above both represent efforts to create an object that's as small
as possible. Controlling data size is not possible in Java, but the language does specify the
size of values for the primitive types.
Although the C++ and Ada code are equivalent in this particular example, there's an inter-
esting semantic difference. In C++, the number of bits required by each field needs to be
specified. Here, we're stating that v is only 8 bits, effectively representing values from 0 to
255. In Ada, it's the other way around: the developer specifies the range of values required
and the compiler decides how to represent things, optimizing for speed or size. The Pack
1337
Learning Ada
aspect declared at the end of the record specifies that the compiler should optimize for size
even at the expense of decreased speed in accessing record components.
Other representation clauses can be specified as well, along with compile-time consistency
checks between requirements in terms of available values and specified sizes. This is par-
ticularly useful when a specific layout is necessary; for example when interfacing with hard-
ware, a driver, or a communication protocol. Here's how to specify a specific data layout
based on the previous example:
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record;
We omit the with Pack directive and instead use a record representation clause following
the record declaration. The compiler is directed to spread objects of type R across two
bytes. The layout we're specifying here is fairly inefficient to work with on any machine,
but you can have the compiler construct the most efficient methods for access, rather than
coding your own machine-dependent bit-level methods manually.
When performing low-level development, such as at the kernel or hardware driver level,
there can be times when it is necessary to implement functionality with assembly code.
Every Ada compiler has its own conventions for embedding assembly code, based on the
hardware platform and the supported assembler(s). Our examples here will work with GNAT
and GCC on the x86 architecture.
All x86 processors since the Intel Pentium offer the rdtsc instruction, which tells us the
number of cycles since the last processor reset. It takes no inputs and places an unsigned
64 bit value split between the edx and eax registers.
GNAT provides a subprogram called System.Machine_Code.Asm that can be used for assem-
bly code insertion. You can specify a string to pass to the assembler as well as source-level
variables to be used for input and output:
with System.Machine_Code; use System.Machine_Code;
with Interfaces; use Interfaces;
Counter :=
Unsigned_64 (High) * 2 ** 32 +
Unsigned_64 (Low);
return Counter;
end Get_Processor_Cycles;
Much effort was spent making Ada easy to interface with other languages. The Interfaces
package hierarchy and the pragmas Convention, Import, and Export allow you to make
inter-language calls while observing proper data representation for each language.
Let's start with the following C code:
struct my_struct {
int A, B;
};
To call that function from Ada, the Ada compiler requires a description of the data struc-
ture to pass as well as a description of the function itself. To capture how the C struct
my_struct is represented, we can use the following record along with a pragma Conven-
tion. The pragma directs the compiler to lay out the data in memory the way a C compiler
would.
Describing a foreign subprogram call to Ada code is called "binding" and it is performed in
two stages. First, an Ada subprogram specification equivalent to the C function is coded.
A C function returning a value maps to an Ada function, and a void function maps to an
Ada procedure. Then, rather than implementing the subprogram using Ada code, we use a
pragma Import:
The Import pragma specifies that whenever Call is invoked by Ada code, it should invoke
the call function with the C calling convention.
And that's all that's necessary. Here's an example of a call to Call:
declare
V : my_struct := (A => 1, B => 2);
begin
Call (V);
end;
You can also make Ada subprograms available to C code, and examples of this can be found
in the GNAT User's Guide. Interfacing with C++ and Java use implementation-dependent
features that are also available with GNAT.
SIXTYFOUR
CONCLUSION
All the usual paradigms of imperative programming can be found in all three languages
that we surveyed in this document. However, Ada is different from the rest in that it's
more explicit when expressing properties and expectations. This is a good thing: being
more formal affords better communication among programmers on a team and between
programmers and machines. You also get more assurance of the coherence of a program
at many levels. Ada can help reduce the cost of software maintenance by shifting the effort
to creating a sound system the first time, rather than working harder, more often, and at
greater expense, to fix bugs found later in systems already in production. Applications that
have reliability needs, long term maintenance requirements, or safety/security concerns
are those for which Ada has a proven track record.
It's becoming increasingly common to find systems implemented in multiple languages,
and Ada has standard interfacing facilities to allow Ada code to invoke subprograms and/or
reference data structures from other language environments, or vice versa. Use of Ada
thus allows easy interfacing between different technologies, using each for what it's best
at.
We hope this guide has provided some insight into the Ada software engineer's world and
has made Ada more accessible to programmers already familiar with programming in other
languages.
1341
Learning Ada
SIXTYFIVE
REFERENCES
1343
Learning Ada
1345
Learning Ada
This course introduces you to the Ada language by comparing it to C. It assumes that you
have good knowledge of the C language. It also assumes that the choice of learning Ada
is guided by considerations linked to reliability, safety or security. In that sense, it teaches
you Ada paradigms that should be applied in replacement of those usually applied in C.
This course also introduces you to the SPARK subset of the Ada programming language,
which removes a few features of the language with undefined behavior, so that the code is
fit for sound static analysis techniques.
This course was written by Quentin Ochem, Robert Tice, Gustavo A. Hoffmann, and Patrick
Rogers and reviewed by Patrick Rogers, Filip Gajowniczek, and Tucker Taft.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn web-
site318 . The directory structure in the zip file is based on the code block metadata. For
example, if you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
1. Unpack the zip file;
2. Go to target directory;
3. Start GNAT Studio on this directory;
4. Build (or compile) the project;
317 http://creativecommons.org/licenses/by-sa/4.0
318 https://learn.adacore.com/zip/learning-ada_code.zip
1347
Learning Ada
1348
CHAPTER
SIXTYSIX
INTRODUCTION
To answer this question let's introduce Ada as it compares to C for an embedded application.
C developers are used to a certain coding semantic and style of programming. Especially
in the embedded domain, developers are used to working at a very low level near the
hardware to directly manipulate memory and registers. Normal operations involve math-
ematical operations on pointers, complex bit shifts, and logical bitwise operations. C is
well designed for such operations as it is a low level language that was designed to re-
place assembly language for faster, more efficient programming. Because of this minimal
abstraction, the programmer has to model the data that represents the problem they are
trying to solve using the language of the physical hardware.
Let's look at an example of this problem in action by comparing the same program in Ada
and C:
[C]
Listing 1: main.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
16 return sum;
17 }
18
1349
Learning Ada
Project: Courses.Ada_For_Embedded_C_Dev.Introduction.Add_Angles_C
MD5: a6d184caaec372c538634c578b5e144b
Runtime output
Sum: 0
[Ada]
Listing 2: sum_angles.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Sum_Angles is
5
19 return Sum;
20 end Add_Angles;
21
Project: Courses.Ada_For_Embedded_C_Dev.Introduction.Add_Angles_Ada
MD5: b5a446e5c27aa18c917ae8c2cc6c1605
Runtime output
Sum: 0
Here we have a piece of code in C and in Ada that takes some numbers from the command
line and stores them in an array. We then sum all of the values in the array and print
the result. The tricky part here is that we are working with values that model an angle
in degrees. We know that angles are modular types, meaning that angles greater than
360° can also be represented as Angle mod 360. So if we have an angle of 400°, this is
equivalent to 40°. In order to model this behavior in C we had to create the MOD_DEGREES
macro, which performs the modulus operation. As we read values from the command line,
we convert them to integers and perform the modulus before storing them into the array.
We then call add_angles which returns the sum of the values in the array. Can you spot the
problem with the C code?
Try running the Ada and C examples using the input sequence 340 2 50 70. What does
the C program output? What does the Ada program output? Why are they different?
The problem with the C code is that we forgot to call MOD_DEGREES in the for loop of
add_angles. This means that it is possible for add_angles to return values greater than
DEGREES_MAX. Let's look at the equivalent Ada code now to see how Ada handles the situa-
tion. The first thing we do in the Ada code is to create the type Degrees which is a modular
type. This means that the compiler is going to handle performing the modulus operation
for us. If we use the same for loop in the Add_Angles function, we can see that we aren't
doing anything special to make sure that our resulting value is within the 360° range we
need it to be in.
The takeaway from this example is that Ada tries to abstract some concepts from the de-
veloper so that the developer can focus on solving the problem at hand using a data model
that models the real world rather than using data types prescribed by the hardware. The
main benefit of this is that the compiler takes some responsibility from the developer for
generating correct code. In this example we forgot to put in a check in the C code. The
compiler inserted the check for us in the Ada code because we told the compiler what we
were trying to accomplish by defining strong types.
Ideally, we want all the power that the C programming language can give us to manipulate
the hardware we are working on while also allowing us the ability to more accurately model
data in a safe way. So, we have a dilemma; what can give us the power of operations
like the C language, but also provide us with features that can minimize the potential for
developer error? Since this course is about Ada, it's a good bet we're about to introduce
the Ada language as the answer to this question…
Unlike C, the Ada language was designed as a higher level language from its conception;
giving more responsibility to the compiler to generate correct code. As mentioned above,
with C, developers are constantly shifting, masking, and accessing bits directly on memory
pointers. In Ada, all of these operations are possible, but in most cases, there is a better way
to perform these operations using higher level constructs that are less prone to mistakes,
like off-by-one or unintentional buffer overflows. If we were to compare the same application
written using C and with Ada using high level constructs, we would see similar performance
in terms of speed and memory efficiency. If we compare the object code generated by both
compilers, it's possible that they even look identical!
Like C, Ada is a compiled language. This means that the compiler will parse the source
code and emit machine code native to the target hardware. The Ada compiler we will be
discussing in this course is the GNAT compiler. This compiler is based on the GCC technology
like many C and C++ compilers available. When the GNAT compiler is invoked on Ada code,
the GNAT front-end expands and translates the Ada code into an intermediate language
which is passed to GCC where the code is optimized and translated to machine code. A
C compiler based on GCC performs the same steps and uses the same intermediate GCC
representation. This means that the optimizations we are used to seeing with a GCC based
C compiler can also be applied to Ada code. The main difference between the two compilers
is that the Ada compiler is expanding high level constructs into intermediate code. After
expansion, the Ada code will be very similar to the equivalent C code.
It is possible to do a line-by-line translation of C code to Ada. This feels like a natural step for
a developer used to C paradigms. However, there may be very little benefit to doing so. For
the purpose of this course, we're going to assume that the choice of Ada over C is guided by
considerations linked to reliability, safety or security. In order to improve upon the reliabil-
ity, safety and security of our application, Ada paradigms should be applied in replacement
of those usually applied in C. Constructs such as pointers, preprocessor macros, bitwise
operations and defensive code typically get expressed in Ada in very different ways, im-
proving the overall reliability and readability of the applications. Learning these new ways
of coding, often, requires effort by the developer at first, but proves more efficient once the
paradigms are understood.
In this course we will also introduce the SPARK subset of the Ada programming language.
The SPARK subset removes a few features of the language, i.e., those that make proof
difficult, such as pointer aliasing. By removing these features we can write code that is fit
for sound static analysis techniques. This means that we can run mathematical provers on
the SPARK code to prove certain safety or security properties about the code.
SIXTYSEVEN
The Ada programming language is a general programming language, which means it can
be used for many different types of applications. One type of application where it partic-
ularly shines is reliable and safety-critical embedded software; meaning, a platform with
a microprocessor such as ARM, PowerPC, x86, or RISC-V. The application may be running
on top of an embedded operating system, such as an embedded Linux, or directly on bare
metal. And the application domain can range from small entities such as firmware or device
controllers to flight management systems, communication based train control systems, or
advanced driver assistance systems.
The toolchain used throughout this course is called GNAT, which is a suite of tools with a
compiler based on the GCC environment. It can be obtained from AdaCore, either as part
of a commercial contract with GNAT Pro319 or at no charge with the GNAT Community edi-
tion320 . The information in this course will be relevant no matter which edition you're using.
Most examples will be runnable on the native Linux or Windows version for convenience.
Some will only be relevant in the context of a cross toolchain, in which case we'll be using
the embedded ARM bare metal toolchain.
As for any Ada compiler, GNAT takes advantage of implementation permissions and offers
a project management system. Because we're talking about embedded platforms, there
are a lot of topics that we'll go over which will be specific to GNAT, and sometimes to
specific platforms supported by GNAT. We'll try to make the distinction between what is
GNAT-specific and Ada generic as much as possible throughout this course.
For an introduction to the GNAT Toolchain for the GNAT Community edition, you may refer
to the Introduction to GNAT Toolchain (page 1681) course.
319 https://www.adacore.com/gnatpro
320 https://www.adacore.com/community
1353
Learning Ada
When we're discussing embedded programming, our target device is often different from
the host, which is the device we're using to actually write and build an application. In
this case, we're talking about cross compilation platforms (concisely referred to as cross
platforms).
The GNAT toolchain supports cross platform compilation for various target devices. This
section provides a short introduction to the topic. For more details, please refer to the
GNAT User’s Guide Supplement for Cross Platforms321
GNAT supports two types of cross platforms:
• cross targets, where the target device has an embedded operating system.
– ARM-Linux, which is commonly found in a Raspberry-Pi, is a prominent example.
• bareboard targets, where the run-times do not depend on an operating system.
– In this case, the application has direct access to the system hardware.
For each platform, a set of run-time libraries is available. Run-time libraries implement a
subset of the Ada language for different use cases, and they're different for each target
platform. They may be selected via an attribute in the project's GPR project file or as a
command-line switch to GPRbuild. Although the run-time libraries may vary from target to
target, the user interface stays the same, providing portability for the application.
Run-time libraries consists of:
1. Files that are dependent on the target board.
• These files are responsible for configuring and interacting with the hardware.
• They are known as a Board Support Package — commonly referred to by their
abbrevation BSP.
2. Code that is target-independent.
• This code implements language-defined functionality.
The bareboard run-time libraries are provided as customized run-times that are config-
ured to target a very specific micro-controller or processor. Therefore, for different micro-
controllers and processors, the run-time libraries need to be ported to the specific target.
These are some examples of what needs to be ported:
• startup code / scripts;
• clock frequency initializations;
• memory mapping / allocation;
• interrupts and interrupt priorities;
• register descriptions.
For more details on the topic, please refer to the following chapters of the GNAT User’s
Guide Supplement for Cross Platforms322 :
• Bareboard Topics323
• Customized Run-Time Libraries324
321 https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx.html
322 https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx.html
323 http://docs.adacore.com/live/wave/gnat_ugx/html/gnat_ugx/gnat_ugx/bareboard_topics.html
324 http://docs.adacore.com/live/wave/gnat_ugx/html/gnat_ugx/gnat_ugx/customized_run-time_libraries.html
The first piece of code to translate from C to Ada is the usual Hello World program:
[C]
Listing 1: main.c
1 #include <stdio.h>
2
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Hello_World_C
MD5: 59685c72296a032893cda71dade24196
Runtime output
Hello World
[Ada]
Listing 2: hello_world.adb
1 with Ada.Text_IO;
2
3 procedure Hello_World
4 is
5 begin
6 Ada.Text_IO.Put_Line ("Hello World");
7 end Hello_World;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Hello_World_Ada
MD5: f1a7c6a4fd679c4caea7ee31d14aab2e
Runtime output
Hello World
The resulting program will print Hello World on the screen. Let's now dissect the Ada
version to describe what is going on:
The first line of the Ada code is giving us access to the Ada.Text_IO library which contains
the Put_Line function we will use to print the text to the console. This is similar to C's
#include <stdio.h>. We then create a procedure which executes Put_Line which prints
to the console. This is similar to C's printf function. For now, we can assume these Ada
and C features have similar functionality. In reality, they are very different. We will explore
that more as we delve further into the Ada language.
You may have noticed that the Ada syntax is more verbose than C. Instead of using braces
{} to declare scope, Ada uses keywords. is opens a declarative scope — which is empty
here as there's no variable to declare. begin opens a sequence of statements. Within this
sequence, we're calling the function Put_Line, prefixing explicitly with the name of the
library unit where it's declared, Ada.Text_IO. The absence of the end of line \n can also be
noted, as Put_Line always terminates by an end of line.
Ada syntax might seem peculiar at first glance. Unlike many other languages, it's not
derived from the popular C style of notation with its ample use of brackets; rather, it uses a
more expository syntax coming from Pascal. In many ways, Ada is a more explicit language
— its syntax was designed to increase readability and maintainability, rather than making
it faster to write in a condensed manner. For example:
• full words like begin and end are used in place of curly braces.
• Conditions are written using if, then, elsif, else, and end if.
• Ada's assignment operator does not double as an expression, eliminating potential
mistakes that could be caused by = being used where == should be.
All languages provide one or more ways to express comments. In Ada, two consecutive
hyphens -- mark the start of a comment that continues to the end of the line. This is
exactly the same as using // for comments in C. Multi line comments like C's /* */ do not
exist in Ada.
Ada compilers are stricter with type and range checking than most C programmers are used
to. Most beginning Ada programmers encounter a variety of warnings and error messages
when coding, but this helps detect problems and vulnerabilities at compile time — early
on in the development cycle. In addition, checks (such as array bounds checks) provide
verification that could not be done at compile time but can be performed either at run-
time, or through formal proof (with the SPARK tooling).
Ada identifiers and reserved words are case insensitive. The identifiers VAR, var and VaR are
treated as the same identifier; likewise begin, BEGIN, Begin, etc. Identifiers may include
letters, digits, and underscores, but must always start with a letter. There are 73 reserved
keywords in Ada that may not be used as identifiers, and these are:
Both C and Ada were designed with the idea that the code specification and code imple-
mentation could be separated into two files. In C, the specification typically lives in the
.h, or header file, and the implementation lives in the .c file. Ada is superficially similar to
C. With the GNAT toolchain, compilation units are stored in files with an .ads extension for
specifications and with an .adb extension for implementations.
One main difference between the C and Ada compilation structure is that Ada compilation
units are structured into something called packages.
67.7 Packages
The package is the basic modularization unit of the Ada language, as is the class for Java
and the header and implementation pair for C. A specification defines a package and the
implementation implements the package. We saw this in an earlier example when we
included the Ada.Text_IO package into our application. The package specification has the
structure:
[Ada]
-- my_package.ads
package My_Package is
-- public declarations
private
-- private declarations
end My_Package;
-- my_package.adb
package body My_Package is
-- implementation
end My_Package;
An Ada package contains three parts that, for GNAT, are separated into two files: .ads files
contain public and private Ada specifications, and .adb files contain the implementation,
or Ada bodies.
[Ada]
package Package_Name is
-- public specifications
private
-- private specifications
end Package_Name;
Private types are useful for preventing the users of a package's types from depending on the
types' implementation details. Another use-case is the prevention of package users from
accessing package state/data arbitrarily. The private reserved word splits the package spec
into public and private parts. For example:
[Ada]
Listing 3: types.ads
1 package Types is
2 type Type_1 is private;
3 type Type_2 is private;
4 type Type_3 is private;
5 procedure P (X : Type_1);
6 -- ...
7 private
8 procedure Q (Y : Type_1);
9 type Type_1 is new Integer range 1 .. 1000;
10 type Type_2 is array (Integer range 1 .. 1000) of Integer;
11 type Type_3 is record
12 A, B : Integer;
13 end record;
14 end Types;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Private_Types
MD5: ae4a9e4d10b55e7efd92d7952ba22f4f
Subprograms declared above the private separator (such as P) will be visible to the pack-
age user, and the ones below (such as Q) will not. The body of the package, the imple-
mentation, has access to both parts. A package specification does not require a private
section.
Ada packages can be organized into hierarchies. A child unit can be declared in the following
way:
[Ada]
-- root-child.ads
package Root.Child is
-- package spec goes here
end Root.Child;
-- root-child.adb
Here, Root.Child is a child package of Root. The public part of Root.Child has access to
the public part of Root. The private part of Child has access to the private part of Root,
which is one of the main advantages of child packages. However, there is no visibility
relationship between the two bodies. One common way to use this capability is to define
subsystems around a hierarchical naming scheme.
Entities declared in the visible part of a package specification can be made accessible using
a with clause that references the package, which is similar to the C #include directive.
After a with clause makes a package available, references to the package contents require
the name of the package as a prefix, with a dot after the package name. This prefix can be
omitted if a use clause is employed.
[Ada]
Listing 4: pck.ads
1 -- pck.ads
2
3 package Pck is
4 My_Glob : Integer;
5 end Pck;
Listing 5: main.adb
1 -- main.adb
2
3 with Pck;
4
5 procedure Main is
6 begin
7 Pck.My_Glob := 0;
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Using_Pkg_Entities
MD5: 4215ba710eb54478538dc001bb74ce09
In contrast to C, the Ada with clause is a semantic inclusion mechanism rather than a
text inclusion mechanism; for more information on this difference please refer to Packages
(page 35).
The following code samples are all equivalent, and illustrate the use of comments and
working with integer variables:
[C]
Listing 6: main.c
1 #include <stdio.h>
2
11 // regular addition
12 d = a + b + c;
13
17 return 0;
18 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Var_Decl_C
MD5: ba258dac5c052a97da475239e2f2ce96
Runtime output
d = 101
[Ada]
Listing 7: main.adb
1 with Ada.Text_IO;
2
3 procedure Main
4 is
5 -- variable declaration
6 A, B : Integer := 0;
7 C : Integer := 100;
8 D : Integer;
9 begin
10 -- Ada does not have a shortcut format for increment like in C
11 A := A + 1;
12
13 -- regular addition
14 D := A + B + C;
15
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Var_Decl_Ada
MD5: eaff76f36d5f938bd806d29048df7865
Runtime output
D = 101
You'll notice that, in both languages, statements are terminated with a semicolon. This
means that you can have multi-line statements.
In the Ada example above, there are two distinct sections to the procedure Main. This first
section is delimited by the is keyword and the begin keyword. This section is called the
declarative block of the subprogram. The declarative block is where you will define all the
local variables which will be used in the subprogram. C89 had something similar, where
developers were required to declare their variables at the top of the scope block. Most C
developers may have run into this before when trying to write a for loop:
[C]
Listing 8: main.c
1 /* The C89 version */
2
3 #include <stdio.h>
4
22 return 0;
23 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Average_C89
MD5: 5c89aa28cba0bae4d963b235c53aedf2
Runtime output
Average: 3
[C]
Listing 9: main.c
1 // The modern C way
2
3 #include <stdio.h>
4
22 return 0;
23 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Average_C_Modern
MD5: 6354863137d78adb974743915d1d4530
Runtime output
Average: 3
For the fun of it, let's also see the Ada way to do this:
[Ada]
3 procedure Main is
4 type Int_Array is array (Natural range <>) of Integer;
5
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Average_Ada
MD5: 52abb574d7a8b3bdb56715735dcd1d54
Runtime output
Average: 3
We will explore more about the syntax of loops in Ada in a future section of this course; but
for now, notice that the I variable used as the loop index is not declared in the declarative
section!
Something peculiar that you may have noticed about declarations in Ada is that they are
backwards from the way C does declarations. The C language expects the type followed by
the variable name. Ada expects the variable name followed by a colon and then the type.
The next block in the Ada example is between the begin and end keywords. This is where
your statements will live. You can create new scopes by using the declare keyword:
[Ada]
3 procedure Main
4 is
5 -- variable declaration
6 A, B : Integer := 0;
7 C : Integer := 100;
8 D : Integer;
9 begin
10 -- Ada does not have a shortcut format for increment like in C
11 A := A + 1;
12
13 -- regular addition
14 D := A + B + C;
15
19 declare
20 E : constant Integer := D * 100;
21 begin
22 -- printing the result
23 Ada.Text_IO.Put_Line ("E =" & E'Img);
24 end;
25
26 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Var_Decl_Block_Ada
MD5: 9239b993a7eadb13a27bd3618a03431f
Runtime output
D = 101
E = 10100
Notice that we declared a new variable E whose scope only exists in our newly defined
block. The equivalent C code is:
[C]
11 // regular addition
12 d = a + b + c;
13
17 {
18 const int e = d * 100;
19 printf("e = %d\n", e);
20 }
21
22 return 0;
23 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Var_Decl_Block_C
MD5: 1a837795575ddc026738d92c8655ab6c
Runtime output
d = 101
e = 10100
Fun Fact about the C language assignment operator =: Did you know that an assignment
in C can be used in an expression? Let's look at an example:
[C]
7 if (a = 10)
8 printf("True\n");
9 else
10 printf("False\n");
11
12 return 0;
13 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Equal_C
MD5: 2d00ddf7e154cb888082c86b8fd36c58
Runtime output
True
Run the above code example. What does it output? Is that what you were expecting?
The author of the above code example probably meant to test if a == 10 in the if statement
but accidentally typed = instead of ==. Because C treats assignment as an expression, it
was able to evaluate a = 10.
3 procedure Main
4 is
5 A : Integer := 0;
6 begin
7
8 if A := 10 then
9 Put_Line ("True");
10 else
11 Put_Line ("False");
12 end if;
13 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Equal_Ada
MD5: 1500b264531dfcc7a62eeed2f22f511b
The above code will not compile. This is because Ada does no allow assignment as an
expression.
67.9 Conditions
9 if (v > 0) {
10 printf("Positive\n");
11 }
12 else if (v < 0) {
(continues on next page)
19 return 0;
20 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Condition_C
MD5: 69203e679085e73394d3620a5954262a
Runtime output
Zero
[Ada]
3 procedure Main
4 is
5 -- try changing the initial value to change the
6 -- output of the program
7 V : constant Integer := 0;
8 begin
9 if V > 0 then
10 Put_Line ("Positive");
11 elsif V < 0 then
12 Put_Line ("Negative");
13 else
14 Put_Line ("Zero");
15 end if;
16 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Condition_Ada
MD5: 417e557708472f9022db7d8c1ed6aa33
Runtime output
Zero
In Ada, everything that appears between the if and then keywords is the conditional ex-
pression, no parentheses are required. Comparison operators are the same except for:
Operator C Ada
Equality == =
Inequality != /=
Not ! not
And && and
Or || or
[C]
9 switch(v) {
10 case 0:
11 printf("Zero\n");
12 break;
13 case 1: case 2: case 3: case 4: case 5:
14 case 6: case 7: case 8: case 9:
15 printf("Positive\n");
16 break;
17 case 10: case 12: case 14: case 16: case 18:
18 printf("Even number between 10 and 18\n");
19 break;
20 default:
21 printf("Something else\n");
22 break;
23 }
24
25 return 0;
26 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Switch_Case_C
MD5: 1bdb3d0c151d71280ef9039841f7ee58
Runtime output
Zero
[Ada]
3 procedure Main
4 is
5 -- try changing the initial value to change the
6 -- output of the program
7 V : constant Integer := 0;
8 begin
9 case V is
10 when 0 =>
11 Put_Line ("Zero");
12 when 1 .. 9 =>
13 Put_Line ("Positive");
14 when 10 | 12 | 14 | 16 | 18 =>
15 Put_Line ("Even number between 10 and 18");
16 when others =>
17 Put_Line ("Something else");
18 end case;
19 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Switch_Case_Ada
MD5: 09e2318b56069281c95f23310dc121d1
Runtime output
Zero
Switch or Case?
A switch statement in C is the same as a case statement in Ada. This may be a little strange
because C uses both keywords in the statement syntax. Let's make an analogy between C
and Ada: C's switch is to Ada's case as C's case is to Ada's when.
Notice that in Ada, the case statement does not use the break keyword. In C, we use break
to stop the execution of a case branch from falling through to the next branch. Here is an
example:
[C]
7 switch(v) {
8 case 0:
9 printf("Zero\n");
10 case 1:
11 printf("One\n");
12 default:
13 printf("Other\n");
14 }
15
16 return 0;
17 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Switch_Case_Break_C
MD5: fd0389205476f161655caf32244d9054
Runtime output
Zero
One
Other
Run the above code with v = 0. What prints? What prints when we change the assignment
to v = 1?
When v = 0 the program outputs the strings Zero then One then Other. This is called
fall through. If you add the break statements back into the switch you can stop this fall
through behavior from happening. The reason why fall through is allowed in C is to allow
the behavior from the previous example where we want a specific branch to execute for
multiple inputs. Ada solves this a different way because it is possible, or even probable,
that the developer might forget a break statement accidentally. So Ada does not allow
fall through. Instead, you can use Ada's syntax to identify when a specific branch can be
executed by more than one input. If you want a range of values for a specific branch you
can use the First .. Last notation. If you want a few non-consecutive values you can
use the Value1 | Value2 | Value3 notation.
Instead of using the word default to denote the catch-all case, Ada uses the others key-
word.
67.10 Loops
50 return 0;
51 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Loops_C
MD5: bcd8963884e2b2a5e364219f9b6b8fbc
Runtime output
v = 128
v = 256
v = 30
v = 10
sum = 55
[Ada]
3 procedure Main is
4 V : Integer;
5 begin
6 -- this is a while loop
7 V := 1;
8 while V < 100 loop
9 V := V * 2;
10 end loop;
11 Ada.Text_IO.Put_Line ("V = " & Integer'Image (V));
12
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Loops_Ada
MD5: c09a092f8d2f682ce758d4bf059b954a
Runtime output
V = 128
V = 256
V = 30
V = 10
Sum = 55
The loop syntax in Ada is pretty straightforward. The loop and end loop keywords are
used to open and close the loop scope. Instead of using the break keyword to exit the loop,
Ada has the exit statement. The exit statement can be combined with a logic expression
using the exit when syntax.
The major deviation in loop syntax is regarding for loops. You'll notice, in C, that you some-
times declare, and at least initialize a loop counter variable, specify a loop predicate, or an
expression that indicates when the loop should continue executing or complete, and last
you specify an expression to update the loop counter.
[C]
In Ada, you don't declare or initialize a loop counter or specify an update expression. You
only name the loop counter and give it a range to loop over. The loop counter is read-only!
You cannot modify the loop counter inside the loop like you can in C. And the loop counter
will increment consecutively along the specified range. But what if you want to loop over
the range in reverse order?
[C]
12 return 0;
13 }
Runtime output
10
9
8
7
6
5
4
3
2
1
0
[Ada]
3 procedure Main
4 is
5 My_Range : constant := 10;
6 begin
7 for I in reverse 0 .. My_Range loop
8 Put_Line (I'Img);
9 end loop;
10 end Main;
Runtime output
10
9
8
7
6
5
4
3
2
1
0
Tick Image
Strangely enough, Ada people call the single apostrophe symbol, ', "tick". This "tick" says
the we are accessing an attribute of the variable. When we do 'Img on a variable of a
numerical type, we are going to return the string version of that numerical type. So in
the for loop above, I'Img, or "I tick image" will return the string representation of the
numerical value stored in I. We have to do this because Put_Line is expecting a string as an
input parameter.
We'll discuss attributes in more details later in this chapter (page 1389).
In the above example, we are traversing over the range in reverse order. In Ada, we use
the reverse keyword to accomplish this.
In many cases, when we are writing a for loop, it has something to do with traversing an
array. In C, this is a classic location for off-by-one errors. Let's see an example in action:
[C]
17 if (i % 10 == 0) {
18 printf("\n");
19 }
20 }
21
22 return 0;
23 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Loop_Reverse_C
MD5: 710ce30066551d1aada8d4e98a6004b1
Runtime output
791621423
99 98 97 96 95 94 93 92 91 90
89 88 87 86 85 84 83 82 81 80
79 78 77 76 75 74 73 72 71 70
69 68 67 66 65 64 63 62 61 60
59 58 57 56 55 54 53 52 51 50
49 48 47 46 45 44 43 42 41 40
39 38 37 36 35 34 33 32 31 30
29 28 27 26 25 24 23 22 21 20
19 18 17 16 15 14 13 12 11 10
9 8 7 6 5 4 3 2 1
[Ada]
3 procedure Main
4 is
5 type Int_Array is array (Natural range 1 .. 100) of Integer;
6
7 List : Int_Array;
8 begin
9
17 if I mod 10 = 0 then
18 New_Line;
19 end if;
20 end loop;
21
22 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Loop_Reverse_Ada
MD5: 340b935d42a80671bb050bdad1b032f7
Runtime output
99 98 97 96 95 94 93 92 91 90
89 88 87 86 85 84 83 82 81 80
79 78 77 76 75 74 73 72 71 70
69 68 67 66 65 64 63 62 61 60
59 58 57 56 55 54 53 52 51 50
49 48 47 46 45 44 43 42 41 40
39 38 37 36 35 34 33 32 31 30
29 28 27 26 25 24 23 22 21 20
19 18 17 16 15 14 13 12 11 10
9 8 7 6 5 4 3 2 1 0
The above Ada and C code should initialize an array using a for loop. The initial values in
the array should be contiguously decreasing from 99 to 0 as we index from the first index
to the last index. In other words, the first index has a value of 99, the next has 98, the next
97 ... the last has a value of 0.
If you run both the C and Ada code above you'll notice that the outputs of the two programs
are different. Can you spot why?
In the C code there are two problems:
1. There's a buffer overflow in the first iteration of the loop. We would need to modify
the loop initialization to int i = LIST_LENGTH - 1;. The loop predicate should be
modified to i >= 0;
2. The C code also has another off-by-one problem in the math to compute the value
stored in list[i]. The expression should be changed to be list[i] = LIST_LENGTH
- i - 1;.
These are typical off-by-one problems that plagues C programs. You'll notice that we didn't
have this problem with the Ada code because we aren't defining the loop with arbitrary
numeric literals. Instead we are accessing attributes of the array we want to manipulate
and are using a keyword to determine the indexing direction.
We can actually simplify the Ada for loop a little further using iterators:
[Ada]
3 procedure Main
4 is
5 type Int_Array is array (Natural range 1 .. 100) of Integer;
6
7 List : Int_Array;
8 begin
9
17 if I mod 10 = 0 then
18 New_Line;
19 end if;
20 end loop;
21
22 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Loop_Reverse_Ada_Simplified
MD5: 612046826199b00ed61271d6215596fe
Runtime output
99 98 97 96 95 94 93 92 91 90
89 88 87 86 85 84 83 82 81 80
79 78 77 76 75 74 73 72 71 70
69 68 67 66 65 64 63 62 61 60
59 58 57 56 55 54 53 52 51 50
49 48 47 46 45 44 43 42 41 40
39 38 37 36 35 34 33 32 31 30
29 28 27 26 25 24 23 22 21 20
19 18 17 16 15 14 13 12 11 10
9 8 7 6 5 4 3 2 1 0
In the second for loop, we changed the syntax to for I of List. Instead of I being the
index counter, it is now an iterator that references the underlying element. This example
of Ada code is identical to the last bit of Ada code. We just used a different method to index
over the second for loop. There is no C equivalent to this Ada feature, but it is similar to
C++'s range based for loop.
Ada is considered a "strongly typed" language. This means that the language does not
define any implicit type conversions. C does define implicit type conversions, sometimes
referred to as integer promotion. The rules for promotion are fairly straightforward in simple
expressions but can get confusing very quickly. Let's look at a typical place of confusion
with implicit type conversion:
[C]
8 printf("Does a == b?\n");
9 if(a == b)
10 printf("Yes.\n");
11 else
12 printf("No.\n");
13
16 return 0;
17 }
Runtime output
Does a == b?
No.
a: 0x000000FF, b: 0xFFFFFFFF
Run the above code. You will notice that a != b! If we look at the output of the last printf
statement we will see the problem. a is an unsigned number where b is a signed number.
We stored a value of 0xFF in both variables, but a treated this as the decimal number 255
while b treated this as the decimal number -1. When we compare the two variables, of
course they aren't equal; but that's not very intuitive. Let's look at the equivalent Ada
example:
[Ada]
3 procedure Main
4 is
5 type Char is range 0 .. 255;
6 type Unsigned_Char is mod 256;
7
14 if A = B then
15 Put_Line ("Yes");
16 else
17 Put_Line ("No");
18 end if;
19
20 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Strong_Typing_Ada
MD5: d6ef2668809159e9fb0d42f91e893222
Build output
If you try to run this Ada example you will get a compilation error. This is because the
compiler is telling you that you cannot compare variables of two different types. We would
need to explicitly cast one side to make the comparison against two variables of the same
type. By enforcing the explicit cast we can't accidentally end up in a situation where we
assume something will happen implicitly when, in fact, our assumption is incorrect.
Another example: you can't divide an integer by a float. You need to perform the division
operation using values of the same type, so one value must be explicitly converted to
match the type of the other (in this case the more likely conversion is from integer to float).
Ada is designed to guarantee that what's done by the program is what's meant by the
programmer, leaving as little room for compiler interpretation as possible. Let's have a
look at the following example:
[Ada]
3 procedure Strong_Typing is
4 Alpha : constant Integer := 1;
5 Beta : constant Integer := 10;
6 Result : Float;
7 begin
8 Result := Float (Alpha) / Float (Beta);
9
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Strong_Typing_Ada_2
MD5: bf91f01b499bcd7da1df751a9f91a767
Runtime output
1.00000E-01
[C]
10 printf("%f\n", result);
11 }
12
17 return 0;
18 }
Runtime output
0.000000
Are the three programs above equivalent? It may seem like Ada is just adding extra com-
plexity by forcing you to make the conversion from Integer to Float explicit. In fact, it
significantly changes the behavior of the computation. While the Ada code performs a
floating point operation 1.0 / 10.0 and stores 0.1 in Result, the C version instead store 0.0
in result. This is because the C version perform an integer operation between two integer
variables: 1 / 10 is 0. The result of the integer division is then converted to a float and
stored. Errors of this sort can be very hard to locate in complex pieces of code, and sys-
tematic specification of how the operation should be interpreted helps to avoid this class
of errors. If an integer division was actually intended in the Ada case, it is still necessary to
explicitly convert the final result to Float:
[Ada]
-- Perform an Integer division then convert to Float
Result := Float (Alpha / Beta);
3 procedure Strong_Typing is
4 Alpha : constant Integer := 1;
5 Beta : constant Integer := 10;
6 Result : Float;
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Strong_Typing_Ada_2
MD5: 50d6a6a3270b51880c43c07f077760b6
Runtime output
0.00000E+00
The principal scalar types predefined by Ada are Integer, Float, Boolean, and Character.
These correspond to int, float, int (when used for Booleans), and char, respectively. The
names for these types are not reserved words; they are regular identifiers. There are other
language-defined integer and floating-point types as well. All have implementation-defined
ranges and precision.
Ada's type system encourages programmers to think about data at a high level of abstrac-
tion. The compiler will at times output a simple efficient machine instruction for a full line of
source code (and some instructions can be eliminated entirely). The careful programmer's
concern that the operation really makes sense in the real world would be satisfied, and so
would the programmer's concern about performance.
The next example below defines two different metrics: area and distance. Mixing these
two metrics must be done with great care, as certain operations do not make sense, like
adding an area to a distance. Others require knowledge of the expected semantics; for
example, multiplying two distances. To help avoid errors, Ada requires that each of the
binary operators +, -, *, and / for integer and floating-point types take operands of the
same type and return a value of that type.
[Ada]
5 D1 : Distance := 2.0;
6 D2 : Distance := 3.0;
7 A : Area;
8 begin
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Application_Defined_Types
MD5: 6a21d6281cc529bbf8ce2216d7e4a770
Build output
Even though the Distance and Area types above are just Float, the compiler does not
allow arbitrary mixing of values of these different types. An explicit conversion (which does
not necessarily mean any additional object code) is necessary.
The predefined Ada rules are not perfect; they admit some problematic cases (for example
multiplying two Distance yields a Distance) and prohibit some useful cases (for exam-
ple multiplying two Distances should deliver an Area). These situations can be handled
through other mechanisms. A predefined operation can be identified as abstract to make
it unavailable; overloading can be used to give new interpretations to existing operator
symbols, for example allowing an operator to return a value from a type different from its
operands; and more generally, GNAT has introduced a facility that helps perform dimen-
sionality checking.
Ada enumerations work similarly to C enum:
[Ada]
11 D : Day := Monday;
12 begin
13 null;
14 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enumeration_Ada
MD5: 51abd1863970e14ff86859c1aae11fe8
[C]
15 return 0;
16 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enumeration_C
MD5: d9f6724759375a126a6b5d8dceea3f24
But even though such enumerations may be implemented by the compiler as numeric val-
ues, at the language level Ada will not confuse the fact that Monday is a Day and is not
an Integer. You can compare a Day with another Day, though. To specify implementation
details like the numeric values that correspond with enumeration values in C you include
them in the original enum declaration:
[C]
3 enum Day {
4 Monday = 10,
5 Tuesday = 11,
6 Wednesday = 12,
7 Thursday = 13,
8 Friday = 14,
9 Saturday = 15,
10 Sunday = 16
11 };
12
19 return 0;
20 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enumeration_Values_C
MD5: 48ae1c84dafabde7a16de5305e106a80
Runtime output
d = 10
But in Ada you must use both a type definition for Day as well as a separate representation
clause for it like:
[Ada]
3 procedure Main is
4 type Day is
5 (Monday,
6 Tuesday,
7 Wednesday,
8 Thursday,
9 Friday,
10 Saturday,
11 Sunday);
12
23 D : Day := Monday;
24 V : Integer;
25 begin
26 V := Day'Enum_Rep (D);
27 Ada.Text_IO.Put_Line (Integer'Image (V));
28 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enumeration_Values
MD5: 9a4fa1a899cb8c240105bf8ad6dbfde3
Runtime output
10
Note that however, unlike C, values for enumerations in Ada have to be unique.
Contracts can be associated with types and variables, to refine values and define what are
considered valid values. The most common kind of contract is a range constraint introduced
with the range reserved word, for example:
[Ada]
4 G1, G2 : Grade;
5 N : Integer;
6 begin
7 -- ... -- Initialization of N
8 G1 := 80; -- OK
9 G1 := N; -- Illegal (type mismatch)
10 G1 := Grade (N); -- Legal, run-time range check
11 G2 := G1 + 10; -- Legal, run-time range check
12 G1 := (G1 + G2) / 2; -- Legal, run-time range check
13 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Range_Check
MD5: 0f249b06e373497ae94b6055a37187c8
Build output
In the above example, Grade is a new integer type associated with a range check. Range
checks are dynamic and are meant to enforce the property that no object of the given type
can have a value outside the specified range. In this example, the first assignment to G1
is correct and will not raise a run-time exception. Assigning N to G1 is illegal since Grade is
a different type than Integer. Converting N to Grade makes the assignment legal, and a
range check on the conversion confirms that the value is within 0 .. 100. Assigning G1 +
10 to G2 is legal since + for Grade returns a Grade (note that the literal 10 is interpreted as
a Grade value in this context), and again there is a range check.
The final assignment illustrates an interesting but subtle point. The subexpression G1 +
G2 may be outside the range of Grade, but the final result will be in range. Nevertheless,
depending on the representation chosen for Grade, the addition may overflow. If the com-
piler represents Grade values as signed 8-bit integers (i.e., machine numbers in the range
-128 .. 127) then the sum G1 + G2 may exceed 127, resulting in an integer overflow. To
prevent this, you can use explicit conversions and perform the computation in a sufficiently
large integer type, for example:
[Ada]
3 procedure Main is
4 type Grade is range 0 .. 100;
5
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Range_And_Explicit_Conversion
MD5: d317fd95099e49017c4a4c1c52b7f8be
Runtime output
99
Range checks are useful for detecting errors as early as possible. However, there may
be some impact on performance. Modern compilers do know how to remove redundant
checks, and you can deactivate these checks altogether if you have sufficient confidence
that your code will function correctly.
Types can be derived from the representation of any other type. The new derived type can
be associated with new constraints and operations. Going back to the Day example, one
can write:
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enum_Ranges_1
MD5: fd775ad4990d5636607d3a0d9b00044d
Since these are new types, implicit conversions are not allowed. In this case, it's more
natural to create a new set of constraints for the same type, instead of making completely
new ones. This is the idea behind subtypes in Ada. A subtype is a type with optional
additional constraints. For example:
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Enum_Ranges_2
MD5: 5bcbde5b9f1aea57ff172fcfc89e1c41
These declarations don't create new types, just new names for constrained ranges of their
base types.
The purpose of numeric ranges is to express some application-specific constraint that we
want the compiler to help us enforce. More importantly, we want the compiler to tell us
when that constraint cannot be met — when the underlying hardware cannot support the
range given. There are two things to consider:
• just a range constraint, such as A : Integer range 0 .. 10;, or
• a type declaration, such as type Result is range 0 .. 1_000_000_000;.
Both represent some sort of application-specific constraint, but in addition, the type declara-
tion promotes portability because it won't compile on targets that do not have a sufficiently
large hardware numeric type. That's a definition of portability that is preferable to having
something compile anywhere but not run correctly, as in C.
Unsigned integer numbers are quite common in embedded applications. In C, you can use
them by declaring unsigned int variables. In Ada, you have two options:
• declare custom unsigned range types;
– In addition, you can declare custom range subtypes or use existing subtypes such
as Natural.
• declare custom modular types.
The following table presents the main features of each type. We discuss these types right
after.
When declaring custom range types in Ada, you may use the full range in the same way
as in C. For example, this is the declaration of a 32-bit unsigned integer type and the X
variable in Ada:
[Ada]
3 procedure Main is
4 type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1;
5
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Unsigned_32_Ada
MD5: 0a179ce327c022468f66b6814a981b62
Runtime output
X = 42
In C, when unsigned int has a size of 32 bits, this corresponds to the following declaration:
[C]
9 return 0;
10 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Unsigned_32_C
MD5: 546068de216de96282490e81a0f7df26
Runtime output
x = 42
Another strategy is to declare subtypes for existing signed types and specify just the range
that excludes negative numbers. For example, let's declare a custom 32-bit signed type
and its unsigned subtype:
[Ada]
3 procedure Main is
4 type Signed_Int_32 is range -2 ** 31 .. 2 ** 31 - 1;
5
10 X : Unsigned_Int_31 := 42;
11 begin
12 Put_Line ("X = " & Unsigned_Int_31'Image (X));
13 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Unsigned_31_Ada
MD5: 2ef2b5bfd54821ceb35faa222e649156
Runtime output
X = 42
In this case, we're just skipping the sign bit of the Signed_Int_32 type. In other words,
while Signed_Int_32 has a size of 32 bits, Unsigned_Int_31 has a range of 31 bits, even
if the base type has 32 bits.
Note that the declaration above is actually similar to the existing Natural subtype. Ada
provides the following standard subtypes:
Since they're standard subtypes, you can declare variables of those subtypes directly in
your implementation, in the same way as you can declare Integer variables.
As indicated in the table above, however, there is a difference in behavior for the variables
we just declared, which occurs in case of overflow. Let's consider this C example:
[C]
11 return 0;
12 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Overflow_Wraparound_C
MD5: 7d5dcf65471304ff8f303195359b4790
Runtime output
x = 0
3 procedure Main is
4 type Unsigned_Int_32 is range 0 .. 2 ** 32 - 1;
5
6 X : Unsigned_Int_32 := Unsigned_Int_32'Last + 1;
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Overflow_Wraparound_Ada
MD5: ee4c3e905c59f5c8d87311e13d079836
Build output
Runtime output
While the C uses modulo arithmetic for unsigned integer, Ada doesn't use it for the Un-
signed_Int_32 type. Ada does, however, support modular types via type definitions using
the mod keyword. In this example, we declare a 32-bit modular type:
[Ada]
3 procedure Main is
4 type Unsigned_32 is mod 2**32;
5
6 X : Unsigned_32 := Unsigned_32'Last + 1;
7 -- Now: X = 0
8 begin
9 Put_Line ("X = " & Unsigned_32'Image (X));
10 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Overflow_Wraparound_Ada
MD5: 4ed963ab372cafc8e7a19d9c3107276b
Runtime output
X = 0
67.11.6 Attributes
Attributes start with a single apostrophe ("tick"), and they allow you to query properties of,
and perform certain actions on, declared entities such as types, objects, and subprograms.
For example, you can determine the first and last bounds of scalar types, get the sizes
of objects and types, and convert values to and from strings. This section provides an
overview of how attributes work. For more information on the many attributes defined by
the language, you can refer directly to the Ada Language Reference Manual.
The 'Image and 'Value attributes allow you to transform a scalar value into a String and
vice-versa. For example:
[Ada]
3 procedure Main is
4 A : Integer := 10;
5 begin
6 Put_Line (Integer'Image (A));
7 A := Integer'Value ("99");
8 Put_Line (Integer'Image (A));
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Image_Attribute
MD5: 1fcfc79ec599a26e21aef7eacffcf96e
Runtime output
10
99
Important
Semantically, attributes are equivalent to subprograms. For example, Integer'Image is
defined as follows:
Certain attributes are provided only for certain kinds of types. For example, the 'Val and
'Pos attributes for an enumeration type associates a discrete value with its position among
its peers. One circuitous way of moving to the next character of the ASCII table is:
[Ada]
3 procedure Main is
4 C : Character := 'a';
5 begin
6 Put (C);
7 C := Character'Val (Character'Pos (C) + 1);
8 Put (C);
9 end Main;
Runtime output
ab
A more concise way to get the next value in Ada is to use the 'Succ attribute:
[Ada]
3 procedure Main is
4 C : Character := 'a';
5 begin
6 Put (C);
7 C := Character'Succ (C);
8 Put (C);
9 end Main;
Runtime output
ab
You can get the previous value using the 'Pred attribute. Here is the equivalent in C:
[C]
10 return 0;
11 }
Runtime output
ab
Other interesting examples are the 'First and 'Last attributes which, respectively, return
the first and last values of a scalar type. Using 32-bit integers, for instance, Integer'First
returns -231 and Integer'Last returns 231 - 1.
C arrays are pointers with offsets, but the same is not the case for Ada. Arrays in Ada are
not interchangeable with operations on pointers, and array types are considered first-class
citizens. They have dedicated semantics such as the availability of the array's boundaries
at run-time. Therefore, unhandled array overflows are impossible unless checks are sup-
pressed. Any discrete type can serve as an array index, and you can specify both the
starting and ending bounds — the lower bound doesn't necessarily have to be 0. Most of
the time, array types need to be explicitly declared prior to the declaration of an object of
that array type.
Here's an example of declaring an array of 26 characters, initializing the values from 'a'
to 'z':
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Character;
5 Arr : Arr_Type (1 .. 26);
6 C : Character := 'a';
7 begin
8 for I in Arr'Range loop
9 Arr (I) := C;
10 C := Character'Succ (C);
11
14 if I mod 7 = 0 then
15 New_Line;
16 end if;
17 end loop;
18 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Range_Ada
MD5: 8e0597f6c040c740b35c79bc4706829b
Runtime output
a b c d e f g
h i j k l m n
o p q r s t u
v w x y z
[C]
12 if ((I + 1) % 7 == 0) {
13 printf ("\n");
14 }
15 }
16
17 return 0;
18 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Range_C
MD5: 1182155f46a0b69f73cd5937c23ed67d
Runtime output
a b c d e f g
h i j k l m n
o p q r s t u
v w x y z
In C, only the size of the array is given during declaration. In Ada, array index ranges are
specified using two values of a discrete type. In this example, the array type declaration
specifies the use of Integer as the index type, but does not provide any constraints (use
<>, pronounced box, to specify "no constraints"). The constraints are defined in the object
declaration to be 1 to 26, inclusive. Arrays have an attribute called 'Range. In our example,
Arr'Range can also be expressed as Arr'First .. Arr'Last; both expressions will resolve
to 1 .. 26. So the 'Range attribute supplies the bounds for our for loop. There is no risk
of stating either of the bounds incorrectly, as one might do in C where I <= 26 may be
specified as the end-of-loop condition.
As in C, Ada String is an array of Character. Ada strings, importantly, are not delimited
with the special character '0' like they are in C. It is not necessary because Ada uses the
array's bounds to determine where the string starts and stops.
Ada's predefined String type is very straightforward to use:
[Ada]
3 procedure Main is
4 My_String : String (1 .. 19) := "This is an example!";
5 begin
6 Put_Line (My_String);
7 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Constrained_String
MD5: da2e88900c670f80b7380f87f2b89ec2
Runtime output
This is an example!
Unlike C, Ada does not offer escape sequences such as 'n'. Instead, explicit values from
the ASCII package must be concatenated (via the concatenation operator, &). Here for
example, is how to initialize a line of text ending with a new line:
[Ada]
3 procedure Main is
4 My_String : String := "This is a line" & ASCII.LF;
5 begin
6 Put (My_String);
7 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Constrained_String
MD5: 684bbbdf99d48ed6fd5c257183a6609f
Runtime output
This is a line
You see here that no constraints are necessary for this variable definition. The initial value
given allows the automatic determination of My_String's bounds.
Ada offers high-level operations for copying, slicing, and assigning values to arrays. We'll
start with assignment. In C, the assignment operator doesn't make a copy of the value of
an array, but only copies the address or reference to the target variable. In Ada, the actual
array contents are duplicated. To get the above behavior, actual pointer types would have
to be defined and used.
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 2);
6 A2 : Arr_Type (1 .. 2);
7 begin
8 A1 (1) := 0;
9 A1 (2) := 1;
10
11 A2 := A1;
12
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Copy_Ada
MD5: 4d4e9aa063c1f488e7cefa90083d06c2
Runtime output
0
1
[C]
9 A1 [0] = 0;
10 A1 [1] = 1;
11
18 return 0;
19 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Copy_C
MD5: 0dade800452673b7a82afe1c656f07e6
Runtime output
0
1
In all of the examples above, the source and destination arrays must have precisely the
same number of elements. Ada allows you to easily specify a portion, or slice, of an array.
So you can write the following:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 10) := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
6 A2 : Arr_Type (1 .. 5) := (1, 2, 3, 4, 5);
7 begin
8 A2 (1 .. 3) := A1 (4 .. 6);
9
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Slice
MD5: cb2a7de2cff8ea19025363886f8821e4
Runtime output
4
5
6
4
5
This assigns the 4th, 5th, and 6th elements of A1 into the 1st, 2nd, and 3rd elements of A2.
Note that only the length matters here: the values of the indexes don't have to be equal;
they slide automatically.
Ada also offers high level comparison operations which compare the contents of arrays as
opposed to their addresses:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (1 .. 2) := (10, 20);
6 A2 : Arr_Type (1 .. 2) := (10, 20);
7 begin
8 if A1 = A2 then
9 Put_Line ("A1 = A2");
10 else
11 Put_Line ("A1 /= A2");
12 end if;
13 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Equal_Ada
MD5: 650a734875a02b2fb3678bbc3f8dd82a
Runtime output
A1 = A2
[C]
8 int eq = 1;
9
17 if (eq) {
18 printf("A1 == A2\n");
(continues on next page)
24 return 0;
25 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Equal_C
MD5: efe8717d931324bcbe8b70b03693c92e
Runtime output
A1 == A2
You can assign to all the elements of an array in each language in different ways. In Ada, the
number of elements to assign can be determined by looking at the right-hand side, the left-
hand side, or both sides of the assignment. When bounds are known on the left-hand side,
it's possible to use the others expression to define a default value for all the unspecified
array elements. Therefore, you can write:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type (-2 .. 42) := (others => 0);
6 begin
7 -- use a slice to assign A1 elements 11 .. 19 to 1
8 A1 (11 .. 19) := (others => 1);
9
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Assignment_Ada
MD5: 673d31f633a32b6bb1cce238150cfc80
Runtime output
---- A1 ----
-2 => 0
-1 => 0
0 => 0
1 => 0
2 => 0
3 => 0
4 => 0
5 => 0
6 => 0
7 => 0
(continues on next page)
In this example, we're specifying that A1 has a range between -2 and 42. We use (others
=> 0) to initialize all array elements with zero. In the next example, the number of elements
is determined by looking at the right-hand side:
[Ada]
3 procedure Main is
4 type Arr_Type is array (Integer range <>) of Integer;
5 A1 : Arr_Type := (1, 2, 3, 4, 5, 6, 7, 8, 9);
6 begin
7 A1 := (1, 2, 3, others => 10);
8
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Array_Assignment_Ada
MD5: 3e3d69815373d1c61208df265903e89d
Runtime output
---- A1 ----
-2147483648 => 1
-2147483647 => 2
-2147483646 => 3
-2147483645 => 10
-2147483644 => 10
-2147483643 => 10
-2147483642 => 10
-2147483641 => 10
-2147483640 => 10
The structure corresponding to a C struct is an Ada record. Here are some simple records:
[Ada]
3 procedure Main is
4 type R is record
5 A, B : Integer;
6 C : Float;
7 end record;
8
9 V : R;
10 begin
11 V.A := 0;
12 Put_Line ("V.A = " & Integer'Image (V.A));
13 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Struct_Ada
MD5: 013f27dfc827355f32bea37fb267df9b
Runtime output
V.A = 0
[C]
3 struct R {
4 int A, B;
5 float C;
6 };
7
14 return 0;
15 }
Runtime output
V.A = 0
Ada allows specification of default values for fields just like C. The values specified can take
the form of an ordered list of values, a named list of values, or an incomplete list followed
by others => <> to specify that fields not listed will take their default values. For example:
[Ada]
3 procedure Main is
4
5 type R is record
6 A, B : Integer := 0;
7 C : Float := 0.0;
8 end record;
9
23 begin
24 Put_R (V1, "V1");
25 Put_R (V2, "V2");
26 Put_R (V3, "V3");
27 Put_R (V4, "V4");
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Struct_Default_Ada
MD5: d0a9713e3bd9804c00ebf68cc7c196b7
Runtime output
V1 = ( 1, 2, 1.00000E+00)
V2 = ( 1, 2, 1.00000E+00)
V3 = ( 1, 2, 1.00000E+00)
V4 = ( 0, 0, 1.00000E+00)
67.11.9 Pointers
As a foreword to the topic of pointers, it's important to keep in mind the fact that most
situations that would require a pointer in C do not in Ada. In the vast majority of cases,
indirect memory management can be hidden from the developer and thus saves from many
potential errors. However, there are situation that do require the use of pointers, or said
differently that require to make memory indirection explicit. This section will present Ada
access types, the equivalent of C pointers. A further section will provide more details as to
how situations that require pointers in C can be done without access types in Ada.
We'll continue this section by explaining the difference between objects allocated on the
stack and objects allocated on the heap using the following example:
[Ada]
3 procedure Main is
4 type R is record
5 A, B : Integer;
6 end record;
7
15 V1, V2 : R;
16
17 begin
18 V1.A := 0;
19 V2 := V1;
20 V2.A := 1;
21
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Pointers_Ada
MD5: dd1367d57574a46df830884b2a7be930
Runtime output
V1 = ( 0, 0)
V2 = ( 1, 0)
[C]
3 struct R {
4 int A, B;
5 };
6
20 print_r(&V1, "V1");
21 print_r(&V2, "V2");
22
23 return 0;
24 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Pointers_C
MD5: 4b4b79789444339b504bddc01d2d43da
Runtime output
V1 = (0, 0)
V2 = (1, 0)
There are many commonalities between the Ada and C semantics above. In Ada and C,
objects are allocated on the stack and are directly accessed. V1 and V2 are two different
objects and the assignment statement copies the value of V1 into V2. V1 and V2 are two
distinct objects.
Here's now a similar example, but using heap allocation instead:
[Ada]
3 procedure Main is
4 type R is record
(continues on next page)
17 V1 : R_Access;
18 V2 : R_Access;
19 begin
20 V1 := new R;
21 V1.A := 0;
22 V2 := V1;
23 V2.A := 1;
24
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Heap_Alloc_Ada
MD5: 963b48bb0a8585a9941d8fb2d0eda390
Runtime output
V1 = ( 1, 0)
V2 = ( 1, 0)
[C]
4 struct R {
5 int A, B;
6 };
7
22 print_r(V1, "V1");
23 print_r(V2, "V2");
(continues on next page)
25 return 0;
26 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Heap_Alloc_C
MD5: 5c832377403dfa8f00d70ef92bfeff65
Runtime output
V1 = (1, 0)
V2 = (1, 0)
In this example, an object of type R is allocated on the heap. The same object is then
referred to through V1 and V2. As in C, there's no garbage collector in Ada, so objects
allocated by the new operator need to be expressly freed (which is not the case here).
Dereferencing is performed automatically in certain situations, for instance when it is clear
that the type required is the dereferenced object rather than the pointer itself, or when ac-
cessing record members via a pointer. To explicitly dereference an access variable, append
.all. The equivalent of V1->A in C can be written either as V1.A or V1.all.A.
Pointers to scalar objects in Ada and C look like:
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Access_To_Scalars
MD5: 2e2bf53a9b5dc1098921d811be73a7f0
[C]
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Pointers_To_Scalars
MD5: f22d7b6f8170587009b0f6bb1299c0a0
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Access_Initialization
MD5: 5789253068f77100eec34919b8de66ec
When using Ada pointers to reference objects on the stack, the referenced objects must
be declared as being aliased. This directs the compiler to implement the object using a
memory region, rather than using registers or eliminating it entirely via optimization. The
access type needs to be declared as either access all (if the referenced object needs to
be assigned to) or access constant (if the referenced object is a constant). The 'Access
attribute works like the C & operator to get a pointer to the object, but with a scope acces-
sibility check to prevent references to objects that have gone out of scope. For example:
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Access_All
MD5: 520df34083e3517876e10710530380be
[C]
6 return 0;
7 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Access_All_C
MD5: a592fcf09dabe15f2aaf12fba047d74f
To deallocate objects from the heap in Ada, it is necessary to use a deallocation subprogram
that accepts a specific access type. A generic procedure is provided that can be customized
to fit your needs, it's called Ada.Unchecked_Deallocation. To create your customized
deallocator (that is, to instantiate this generic), you must provide the object type as well as
the access type as follows:
[Ada]
3 procedure Main is
4 type Integer_Access is access all Integer;
5 procedure Free is new Ada.Unchecked_Deallocation (Integer, Integer_Access);
6 My_Pointer : Integer_Access := new Integer;
7 begin
8 Free (My_Pointer);
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Unchecked_Deallocation
MD5: ef6ee170fea1f6c6c01037a09809916f
[C]
8 return 0;
9 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Free
MD5: 066046816dd1c4f9106b5e822cfe5e44
Subroutines in C are always expressed as functions which may or may not return a value.
Ada explicitly differentiates between functions and procedures. Functions must return a
value and procedures must not. Ada uses the more general term subprogram to refer to
both functions and procedures.
Parameters can be passed in three distinct modes:
• in, which is the default, is for input parameters, whose value is provided by the caller
and cannot be changed by the subprogram.
• out is for output parameters, with no initial value, to be assigned by the subprogram
and returned to the caller.
• in out is a parameter with an initial value provided by the caller, which can be mod-
ified by the subprogram and returned to the caller (more or less the equivalent of a
non-constant pointer in C).
Ada also provides access and aliased parameters, which are in effect explicit pass-by-
reference indicators.
In Ada, the programmer specifies how the parameter will be used and in general the com-
piler decides how it will be passed (i.e., by copy or by reference). C has the programmer
specify how to pass the parameter.
Important
There are some exceptions to the "general" rule in Ada. For example, parameters of scalar
types are always passed by copy, for all three modes.
3 procedure Proc
4 (Var1 : Integer;
5 Var2 : out Integer;
6 Var3 : in out Integer)
7 is
8 begin
9 Var2 := Func (Var1);
10 Var3 := Var3 + 1;
11 end Proc;
4 procedure Main is
5 V1, V2 : Integer;
6 begin
7 V2 := 2;
8 Proc (5, V1, V2);
9
Runtime output
V1: 6
V2: 3
[C]
3 void Proc
4 (int Var1,
5 int * Var2,
6 int * Var3)
7 {
8 *Var2 = Func (Var1);
9 *Var3 += 1;
10 }
8 v2 = 2;
9 Proc (5, &v1, &v2);
10
14 return 0;
15 }
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Subroutines_C
MD5: dd5645c832ef00b94061f204852084a3
Runtime output
v1: 6
v2: 3
The first two declarations for Proc and Func are specifications of the subprograms which are
being provided later. Although optional here, it's still considered good practice to separately
define specifications and implementations in order to make it easier to read the program.
In Ada and C, a function that has not yet been seen cannot be used. Here, Proc can call
Func because its specification has been declared.
Parameters in Ada subprogram declarations are separated with semicolons, because com-
mas are reserved for listing multiple parameters of the same type. Parameter declaration
syntax is the same as variable declaration syntax (except for the modes), including de-
fault values for parameters. If there are no parameters, the parentheses must be omitted
entirely from both the declaration and invocation of the subprogram.
In Ada 202X
Ada 202X allows for using static expression functions, which are evaluated at compile
time. To achieve this, we can use an aspect — we'll discuss aspects later in this chapter
(page 1411).
An expression function is static when the Static aspect is specified. For example:
procedure Main is
begin
null;
end Main;
In this example, we declare X1 using an expression. In the declaration of X2, we call the
static expression function If_Then_Else. Both X1 and X2 have the same constant value.
67.12.2 Overloading
In C, function names must be unique. Ada allows overloading, in which multiple subpro-
grams can share the same name as long as the subprogram signatures (the parameter
types, and function return types) are different. The compiler will be able to resolve the
calls to the proper routines or it will reject the calls. For example:
[Ada]
9 end Machine;
19 end Machine;
4 procedure Main is
5 S : Status;
6 C : Code;
7 T : Threshold;
8 begin
9 S := On;
10 C := Get (S);
11 T := Get (S);
12
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Overloading_Ada
MD5: 909cdf00b629917f7131489702cc26f1
Runtime output
S: ON
C: 3
T: 1.00000E+01
The Ada compiler knows that an assignment to C requires a Code value. So, it chooses the
Get function that returns a Code to satisfy this requirement.
Operators in Ada are functions too. This allows you to define local operators that override
operators defined at an outer scope, and provide overloaded operators that operate on and
compare different types. To declare an operator as a function, enclose its "name" in quotes:
[Ada]
9 end Machine_2;
19 end Machine_2;
4 procedure Main is
5 I : Input;
6 begin
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Overloading_Eq
MD5: c5580f15c1b93f73fff3afc147cd15a1
Runtime output
67.12.3 Aspects
Aspect specifications allow you to define certain characteristics of a declaration using the
with keyword after the declaration:
For example, you can inline a subprogram by specifying the Inline aspect:
[Ada]
8 end Float_Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Inline_Aspect
MD5: 6e25e81e4015d907d50aa9cf4a0a3fab
9 end Float_Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Inline_Aspect
MD5: bd5df14dce9577a054f0ec612d5bbe40
Aspects and attributes might refer to the same kind of information. For example, we can
use the Size aspect to define the expected minimum size of objects of a certain type:
[Ada]
6 end My_Device_Types;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Size_Aspect
MD5: 049be992b876dba42cf091afc256db35
In the same way, we can use the size attribute to retrieve the size of a type or of an object:
[Ada]
5 procedure Show_Device_Types is
6 UInt10_Obj : constant UInt10 := 0;
7 begin
8 Put_Line ("Size of UInt10 type: " & Positive'Image (UInt10'Size));
9 Put_Line ("Size of UInt10 object: " & Positive'Image (UInt10_Obj'Size));
10 end Show_Device_Types;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Size_Aspect
MD5: 4e46ad9cf54276b381b960672daa03b9
Runtime output
We'll explain both Size aspect and Size attribute later in this course (page 1443).
SIXTYEIGHT
Concurrent and real-time programming are standard parts of the Ada language. As such,
they have the same semantics, whether executing on a native target with an OS such as
Linux, on a real-time operating system (RTOS) such as VxWorks, or on a bare metal target
with no OS or RTOS at all.
For resource-constrained systems, two subsets of the Ada concurrency facilities are defined,
known as the Ravenscar and Jorvik profiles. Though restricted, these subsets have highly
desirable properties, including: efficiency, predictability, analyzability, absence of dead-
lock, bounded blocking, absence of priority inversion, a real-time scheduler, and a small
memory footprint. On bare metal systems, this means in effect that Ada comes with its
own real-time kernel.
Enhanced portability and expressive power are the primary advantages of using the stan-
dard concurrency facilities, potentially resulting in considerable cost savings. For example,
with little effort, it is possible to migrate from Windows to Linux to a bare machine without
requiring any changes to the code. Thread management and synchronization is all done by
the implementation, transparently. However, in some situations, it’s critical to be able to
access directly the services provided by the platform. In this case, it’s always possible to
make direct system calls from Ada code. Several targets of the GNAT compiler provide this
sort of API by default, for example win32ada for Windows and Florist for POSIX systems.
On native and RTOS-based platforms GNAT typically provides the full concurrency facilities.
In contrast, on bare metal platforms GNAT typically provides the two standard subsets:
Ravenscar and Jorvik.
68.2 Tasks
Ada offers a high level construct called a task which is an independent thread of execution.
In GNAT, tasks are either mapped to the underlying OS threads, or use a dedicated kernel
when not available.
The following example will display the 26 letters of the alphabet twice, using two concurrent
tasks. Since there is no synchronization between the two threads of control in any of the
examples, the output may be interspersed.
[Ada]
1415
Learning Ada
Listing 1: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
6 task My_Task;
7
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.My_Task
MD5: 154702197f0c02f5750838e51a99f548
Runtime output
ABCDEFGHIJKLMNOPQRSTUVWXYZ
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Any number of Ada tasks may be declared in any declarative region. A task declaration
is very similar to a procedure or package declaration. They all start automatically when
control reaches the begin. A block will not exit until all sequences of statements defined
within that scope, including those in tasks, have been completed.
A task type is a generalization of a task object; each object of a task type has the same
behavior. A declared object of a task type is started within the scope where it is declared,
and control does not leave that scope until the task has terminated.
Task types can be parameterized; the parameter serves the same purpose as an argument
to a constructor in Java. The following example creates 10 tasks, each of which displays a
subset of the alphabet contained between the parameter and the 'Z' Character. As with
the earlier example, since there is no synchronization among the tasks, the output may be
interspersed depending on the underlying implementation of the task scheduling algorithm.
[Ada]
Listing 2: my_tasks.ads
1 package My_Tasks is
2
5 end My_Tasks;
Listing 3: my_tasks.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
13 end My_Tasks;
Listing 4: main.adb
1 with My_Tasks; use My_Tasks;
2
3 procedure Main is
4 Dummy_Tab : array (0 .. 3) of My_Task ('W');
5 begin
6 null;
7 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.My_Task_Type
MD5: 81d88397b0548fdcc1ba31549a8de4fd
Runtime output
WXYZ
WXYZ
WXYZ
WXYZ
In Ada, a task may be dynamically allocated rather than declared statically. The task will
then start as soon as it has been allocated, and terminates when its work is completed.
[Ada]
Listing 5: main.adb
1 with My_Tasks; use My_Tasks;
2
3 procedure Main is
4 type Ptr_Task is access My_Task;
5
6 T : Ptr_Task;
7 begin
8 T := new My_Task ('W');
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.My_Task_Type
MD5: d88a96eecf50ebbcdfe9cb870f232a09
Runtime output
WXYZ
68.3 Rendezvous
A rendezvous is a synchronization between two tasks, allowing them to exchange data and
coordinate execution. Let's consider the following example:
[Ada]
Listing 6: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4
5 task After is
6 entry Go;
7 end After;
8
15 begin
16 Put_Line ("Before");
17 After.Go;
18 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Rendezvous
MD5: b0a595b1eecac793e40b6d1d41171766
Runtime output
Before
After
The Go entry declared in After is the client interface to the task. In the task body, the
accept statement causes the task to wait for a call on the entry. This particular entry
and accept pair simply causes the task to wait until Main calls After.Go. So, even though
the two tasks start simultaneously and execute independently, they can coordinate via Go.
Then, they both continue execution independently after the rendezvous.
The entry/accept pair can take/pass parameters, and the accept statement can contain a
sequence of statements; while these statements are executed, the caller is blocked.
Let's look at a more ambitious example. The rendezvous below accepts parameters and
executes some code:
[Ada]
Listing 7: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
(continues on next page)
5 task After is
6 entry Go (Text : String);
7 end After;
8
16 begin
17 Put_Line ("Before");
18 After.Go ("Main");
19 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Rendezvous_Params
MD5: 6430e88f5ae349128bb1f1d53f36251e
Runtime output
Before
After: Main
In the above example, the Put_Line is placed in the accept statement. Here's a possible
execution trace, assuming a uniprocessor:
1. At the begin of Main, task After is started and the main procedure is suspended.
2. After reaches the accept statement and is suspended, since there is no pending call
on the Go entry.
3. The main procedure is awakened and executes the Put_Line invocation, displaying
the string "Before".
4. The main procedure calls the Go entry. Since After is suspended on its accept state-
ment for this entry, the call succeeds.
5. The main procedure is suspended, and the task After is awakened to execute the
body of the accept statement. The actual parameter "Main" is passed to the accept
statement, and the Put_Line invocation is executed. As a result, the string "After:
Main" is displayed.
6. When the accept statement is completed, both the After task and the main proce-
dure are ready to run. Suppose that the Main procedure is given the processor. It
reaches its end, but the local task After has not yet terminated. The main procedure
is suspended.
7. The After task continues, and terminates since it is at its end. The main procedure is
resumed, and it too can terminate since its dependent task has terminated.
The above description is a conceptual model; in practice the implementation can perform
various optimizations to avoid unnecessary context switches.
The accept statement by itself can only wait for a single event (call) at a time. The select
statement allows a task to listen for multiple events simultaneously, and then to deal with
the first event to occur. This feature is illustrated by the task below, which maintains an
integer value that is modified by other tasks that call Increment, Decrement, and Get:
[Ada]
Listing 8: counters.ads
1 package Counters is
2
3 task Counter is
4 entry Get (Result : out Integer);
5 entry Increment;
6 entry Decrement;
7 end Counter;
8
9 end Counters;
Listing 9: counters.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
29 end Counters;
4 procedure Main is
5 V : Integer;
6 begin
(continues on next page)
9 Counter.Get (V);
10 Put_Line ("Got value. Value = " & Integer'Image (V));
11
12 Counter.Increment;
13 Put_Line ("Incremented value.");
14
15 Counter.Increment;
16 Put_Line ("Incremented value.");
17
18 Counter.Get (V);
19 Put_Line ("Got value. Value = " & Integer'Image (V));
20
21 Counter.Decrement;
22 Put_Line ("Decremented value.");
23
24 Counter.Get (V);
25 Put_Line ("Got value. Value = " & Integer'Image (V));
26
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Selective_Rendezvous
MD5: 619d009bcfcd8053bc132b2e32a29249
Runtime output
Main started.
Got value. Value = 0
Incremented value.
Incremented value.
Got value. Value = 2
Decremented value.
Got value. Value = 1
Main finished.
Exiting Counter task...
When the task's statement flow reaches the select, it will wait for all four events — three
entries and a delay — in parallel. If the delay of five seconds is exceeded, the task will
execute the statements following the delay statement (and in this case will exit the loop,
in effect terminating the task). The accept bodies for the Increment, Decrement, or Get
entries will be otherwise executed as they're called. These four sections of the select state-
ment are mutually exclusive: at each iteration of the loop, only one will be invoked. This is
a critical point; if the task had been written as a package, with procedures for the various
operations, then a race condition could occur where multiple tasks simultaneously calling,
say, Increment, cause the value to only get incremented once. In the tasking version, if
multiple tasks simultaneously call Increment then only one at a time will be accepted, and
the value will be incremented by each of the tasks when it is accepted.
More specifically, each entry has an associated queue of pending callers. If a task calls one
of the entries and Counter is not ready to accept the call (i.e., if Counter is not suspended
at the select statement) then the calling task is suspended, and placed in the queue of
the entry that it is calling. From the perspective of the Counter task, at any iteration of the
loop there are several possibilities:
• There is no call pending on any of the entries. In this case Counter is suspended. It
will be awakened by the first of two events: a call on one of its entries (which will then
be immediately accepted), or the expiration of the five second delay (whose effect
Although the rendezvous may be used to implement mutually exclusive access to a shared
data object, an alternative (and generally preferable) style is through a protected object,
an efficiently implementable mechanism that makes the effect more explicit. A protected
object has a public interface (its protected operations) for accessing and manipulating the
object's components (its private part). Mutual exclusion is enforced through a conceptual
lock on the object, and encapsulation ensures that the only external access to the compo-
nents are through the protected operations.
Two kinds of operations can be performed on such objects: read-write operations by pro-
cedures or entries, and read-only operations by functions. The lock mechanism is imple-
mented so that it's possible to perform concurrent read operations but not concurrent write
or read/write operations.
Let's reimplement our earlier tasking example with a protected object called Counter:
[Ada]
3 protected Counter is
4 function Get return Integer;
5 procedure Increment;
6 procedure Decrement;
7 private
8 Value : Integer := 0;
9 end Counter;
10
11 end Counters;
9 procedure Increment is
10 begin
11 Value := Value + 1;
12 end Increment;
13
14 procedure Decrement is
(continues on next page)
20 end Counters;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Protected_Counter
MD5: f29f21621dfcf092580f6a130101788e
Having two completely different ways to implement the same paradigm might seem compli-
cated. However, in practice the actual problem to solve usually drives the choice between
an active structure (a task) or a passive structure (a protected object).
A protected object can be accessed through prefix notation:
[Ada]
4 procedure Main is
5 begin
6 Counter.Increment;
7 Counter.Decrement;
8 Put_Line (Integer'Image (Counter.Get));
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Protected_Counter
MD5: 704e3a382fe38caa11ecd3d46fcd2beb
Runtime output
A protected object may look like a package syntactically, since it contains declarations that
can be accessed externally using prefix notation. However, the declaration of a protected
object is extremely restricted; for example, no public data is allowed, no types can be
declared inside, etc. And besides the syntactic differences, there is a critical semantic
distinction: a protected object has a conceptual lock that guarantees mutual exclusion;
there is no such lock for a package.
Like tasks, it's possible to declare protected types that can be instantiated several times:
declare
protected type Counter is
-- as above
end Counter;
C1 : Counter;
C2 : Counter;
(continues on next page)
Protected objects and types can declare a procedure-like operation known as an entry. An
entry is somewhat similar to a procedure but includes a so-called barrier condition that
must be true in order for the entry invocation to succeed. Calling a protected entry is
thus a two step process: first, acquire the lock on the object, and then evaluate the barrier
condition. If the condition is true then the caller will execute the entry body. If the condition
is false, then the caller is placed in the queue for the entry, and relinquishes the lock.
Barrier conditions (for entries with non-empty queues) are reevaluated upon completion of
protected procedures and protected entries.
Here's an example illustrating protected entries: a protected type that models a binary
semaphore / persistent signal.
[Ada]
10 end Binary_Semaphores;
9 procedure Signal is
10 begin
11 Signaled := True;
12 end Signal;
13 end Binary_Semaphore;
14
15 end Binary_Semaphores;
4 procedure Main is
5 B : Binary_Semaphore;
6
7 task T1;
(continues on next page)
10 task body T1 is
11 begin
12 Put_Line ("Task T1 waiting...");
13 B.Wait;
14
24 task body T2 is
25 begin
26 Put_Line ("Task T2 waiting...");
27 B.Wait;
28
38 begin
39 Put_Line ("Main started.");
40 B.Signal;
41 Put_Line ("Main finished.");
42 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Protected_Binary_Semaphore
MD5: aa064a9ec056d44c4217e64cd05726a4
Runtime output
Main started.
Main finished.
Task T1 waiting...
Task T1.
Task T2 waiting...
Task T1 will signal...
Task T1 finished.
Task T2
Task T2 will signal...
Task T2 finished.
Ada concurrency features provide much further generality than what's been presented here.
For additional information please consult one of the works cited in the References section.
68.6 Ravenscar
The Ravenscar profile is a subset of the Ada concurrency facilities that supports determin-
ism, schedulability analysis, constrained memory utilization, and certification to the highest
integrity levels. Four distinct application domains are intended:
• hard real-time applications requiring predictability,
• safety-critical systems requiring formal, stringent certification,
• high-integrity applications requiring formal static analysis and verification,
• embedded applications requiring both a small memory footprint and low execution
overhead.
Tasking constructs that preclude analysis, either technically or economically, are disal-
lowed. You can use the pragma Profile (Ravenscar) to indicate that the Ravenscar
restrictions must be observed in your program.
Some of the examples we've seen above will be rejected by the compiler when using the
Ravenscar profile. For example:
[Ada]
5 end My_Tasks;
13 end My_Tasks;
5 procedure Main is
6 Tab : array (0 .. 3) of My_Task ('W');
7 begin
8 null;
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Ravenscar
MD5: b7518a039c2b4cecece1de63eeaa208f
Build output
This code violates the No_Task_Hierarchy restriction of the Ravenscar profile. This is due to
the declaration of Tab in the Main procedure. Ravenscar requires task declarations to be
done at the library level. Therefore, a simple solution is to create a separate package and
reference it in the main application:
[Ada]
3 package My_Task_Inst is
4
7 end My_Task_Inst;
3 with My_Task_Inst;
4
5 procedure Main is
6 begin
7 null;
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Concurrency.Ravenscar
MD5: b38943dc1c962b5e691f2b6d9933a3ec
Runtime output
WXYZ
WXYZ
WXYZ
WXYZ
Also, Ravenscar prohibits entries for tasks. For example, we're not allowed to write this
declaration:
You can use, however, one entry per protected object. As an example, the declaration of
the Binary_Semaphore type that we've discussed before compiles fine with Ravenscar:
We could add more procedures and functions to the declaration of Binary_Semaphore, but
we wouldn't be able to add another entry when using Ravenscar.
Similar to the previous example with the task array declaration, objects of Bi-
nary_Semaphore cannot be declared in the main application:
procedure Main is
B : Binary_Semaphore;
begin
null;
end Main;
325 https://blog.adacore.com/theres-a-mini-rtos-in-my-language
SIXTYNINE
Ada supports a high level of abstractness and expressiveness. In some cases, the compiler
translates those constructs directly into machine code. However, there are many high-level
constructs for which a direct compilation would be difficult. In those cases, the compiler
links to a library containing an implementation of those high-level constructs: this is the
so-called run-time library.
One typical example of high-level constructs that can be cumbersome for direct machine
code generation is Ada source-code using tasking. In this case, linking to a low-level im-
plementation of multithreading support — for example, an implementation using POSIX
threads — is more straightforward than trying to make the compiler generate all the ma-
chine code.
In the case of GNAT, the run-time library is implemented using both C and Ada source-
code. Also, depending on the operating system, the library will interface with low-level
functionality from the target operating system.
There are basically two types of run-time libraries:
• the standard run-time library: in many cases, this is the run-time library available on
desktop operating systems or on some embedded platforms (such as ARM-Linux on a
Raspberry-Pi).
• the configurable run-time library: this is a capability that is used to create custom
run-time libraries for specific target devices.
Configurable run-time libraries are usually used for constrained target devices where sup-
port for the full library would be difficult or even impossible. In this case, configurable
run-time libraries may support just a subset of the full Ada language. There are many
reasons that speak for this approach:
• Some aspects of the Ada language may not translate well to limited operating systems.
• Memory constraints may require reducing the size of the run-time library, so that de-
velopers may need to replace or even remove parts of the library.
• When certification is required, those parts of the library that would require too much
certification effort can be removed.
When using a configurable run-time library, the compiler checks whether the library sup-
ports certain features of the language. If a feature isn't supported, the compiler will give
an error message.
You can find further information about the run-time library on this chapter of the GNAT
User's Guide Supplement for Cross Platforms326
326 https://docs.adacore.com/gnat_ugx-docs/html/gnat_ugx/gnat_ugx/the_gnat_configurable_run_time_facility.
html
1429
Learning Ada
We've seen in the previous chapters how Ada can be used to describe high level semantics
and architecture. The beauty of the language, however, is that it can be used all the way
down to the lowest levels of the development, including embedded assembly code or bit-
level data management.
One very interesting feature of the language is that, unlike C, for example, there are no data
representation constraints unless specified by the developer. This means that the compiler
is free to choose the best trade-off in terms of representation vs. performance. Let's start
with the following example:
[Ada]
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record
with Pack;
[C]
struct R {
unsigned int v:8;
bool b1;
bool b2;
};
The Ada and the C code above both represent efforts to create an object that's as small
as possible. Controlling data size is not possible in Java, but the language does specify the
size of values for the primitive types.
Although the C and Ada code are equivalent in this particular example, there's an interesting
semantic difference. In C, the number of bits required by each field needs to be specified.
Here, we're stating that v is only 8 bits, effectively representing values from 0 to 255. In
Ada, it's the other way around: the developer specifies the range of values required and the
compiler decides how to represent things, optimizing for speed or size. The Pack aspect
declared at the end of the record specifies that the compiler should optimize for size even
at the expense of decreased speed in accessing record components. We'll see more details
about the Pack aspect in the sections about bitwise operations (page 1486) and mapping
structures to bit-fields (page 1488) in chapter 6.
Other representation clauses can be specified as well, along with compile-time consistency
checks between requirements in terms of available values and specified sizes. This is par-
ticularly useful when a specific layout is necessary; for example when interfacing with hard-
ware, a driver, or a communication protocol. Here's how to specify a specific data layout
based on the previous example:
[Ada]
type R is record
V : Integer range 0 .. 255;
B1 : Boolean;
B2 : Boolean;
end record;
We omit the with Pack directive and instead use a record representation clause following
the record declaration. The compiler is directed to spread objects of type R across two
bytes. The layout we're specifying here is fairly inefficient to work with on any machine,
but you can have the compiler construct the most efficient methods for access, rather than
coding your own machine-dependent bit-level methods manually.
When performing low-level development, such as at the kernel or hardware driver level,
there can be times when it is necessary to implement functionality with assembly code.
Every Ada compiler has its own conventions for embedding assembly code, based on the
hardware platform and the supported assembler(s). Our examples here will work with GNAT
and GCC on the x86 architecture.
All x86 processors since the Intel Pentium offer the rdtsc instruction, which tells us the
number of cycles since the last processor reset. It takes no inputs and places an unsigned
64-bit value split between the edx and eax registers.
GNAT provides a subprogram called System.Machine_Code.Asm that can be used for assem-
bly code insertion. You can specify a string to pass to the assembler as well as source-level
variables to be used for input and output:
[Ada]
Listing 1: get_processor_cycles.adb
1 with System.Machine_Code; use System.Machine_Code;
2 with Interfaces; use Interfaces;
3
14 Counter :=
15 Unsigned_64 (High) * 2 ** 32 +
16 Unsigned_64 (Low);
17
18 return Counter;
19 end Get_Processor_Cycles;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Assembly_Code
MD5: 092be19e223946ebb9fb9f4786003b94
Listing 2: signal_handlers.ads
1 with System.OS_Interface;
2
3 package Signal_Handlers is
4
10 --
11 -- Declaration of an interrupt handler for the "quit" interrupt:
12 --
13 procedure Handle_Quit_Signal
14 with Attach_Handler => System.OS_Interface.SIGQUIT;
15 end Quit_Handler;
16
17 end Signal_Handlers;
Listing 3: signal_handlers.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
10 procedure Handle_Quit_Signal is
11 begin
12 Put_Line ("Quit request detected!");
13 Quit_Request := True;
14 end Handle_Quit_Signal;
15
16 end Quit_Handler;
17
18 end Signal_Handlers;
Listing 4: test_quit_handler.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Signal_Handlers;
3
4 procedure Test_Quit_Handler is
5 Quit : Signal_Handlers.Quit_Handler;
6
7 begin
8 while True loop
9 delay 1.0;
10 exit when Quit.Requested;
11 end loop;
12
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Quit_Handler
MD5: d272c5bc59576444e09007a04a615ccf
The specification of the Signal_Handlers package from this example contains the decla-
ration of Quit_Handler, which is a protected type. In the private part of this protected
type, we declare the Handle_Quit_Signal procedure. By using the Attach_Handler as-
pect in the declaration of Handle_Quit_Signal and indicating the quit interrupt (System.
OS_Interface.SIGQUIT), we're instructing the operating system to call this procedure for
any quit request. So when the user presses CTRL+\ on their keyboard, for example, the
application will behave as follows:
• the operating system calls the Handle_Quit_Signal procedure , which displays a
message to the user ("Quit request detected!") and sets a Boolean variable —
Quit_Request, which is declared in the Quit_Handler type;
• the main application checks the status of the quit handler by calling the Requested
function as part of the while True loop;
– This call is in the exit when Quit.Requested line.
– The Requested function returns True in this case because the Quit_Request flag
was set by the Handle_Quit_Signal procedure.
• the main applications exits the loop, displays a message and finishes.
Note that the code example above isn't portable because it makes use of interrupts from
the Linux operating system. When programming embedded devices, we would use instead
Many numerical applications typically use floating-point types to compute values. However,
in some platforms, a floating-point unit may not be available. Other platforms may have a
floating-point unit, but using it in certain numerical algorithms can be prohibitive in terms
of performance. For those cases, fixed-point arithmetic can be a good alternative.
The difference between fixed-point and floating-point types might not be so obvious when
looking at this code snippet:
[Ada]
Listing 5: fixed_definitions.ads
1 package Fixed_Definitions is
2
7 end Fixed_Definitions;
Listing 6: show_float_and_fixed_point.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Show_Float_And_Fixed_Point is
6 Float_Value : Float := 0.25;
7 Fixed_Value : Fixed := 0.25;
8 begin
9
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Fixed_Point
MD5: 881817bb310304bc285f01454ab446f7
Runtime output
Float_Value = 5.00000E-01
Fixed_Value = 0.5000000000
In this example, the application will show the value 0.5 for both Float_Value and
Fixed_Value.
The major difference between floating-point and fixed-point types is in the way the values
are stored. Values of ordinary fixed-point types are, in effect, scaled integers. The scaling
used for ordinary fixed-point types is defined by the type's small, which is derived from
the specified delta and, by default, is a power of two. Therefore, ordinary fixed-point types
are sometimes called binary fixed-point types. In that sense, ordinary fixed-point types can
be thought of being close to the actual representation on the machine. In fact, ordinary
fixed-point types make use of the available integer shift instructions, for example.
Another difference between floating-point and fixed-point types is that Ada doesn't provide
standard fixed-point types — except for the Duration type, which is used to represent an
interval of time in seconds. While the Ada standard specifies floating-point types such as
Float and Long_Float, we have to declare our own fixed-point types. Note that, in the
previous example, we have used a fixed-point type named Fixed: this type isn't part of the
standard, but must be declared somewhere in the source-code of our application.
The syntax for an ordinary fixed-point type is
By default, the compiler will choose a scale factor, or small, that is a power of 2 no greater
than <delta_value>.
For example, we may define a normalized range between -1.0 and 1.0 as following:
[Ada]
Listing 7: normalized_fixed_point_type.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Normalized_Fixed_Point_Type is
4 D : constant := 2.0 ** (-31);
5 type TQ31 is delta D range -1.0 .. 1.0 - D;
6 begin
7 Put_Line ("TQ31 requires " & Integer'Image (TQ31'Size) & " bits");
8 Put_Line ("The delta value of TQ31 is " & TQ31'Image (TQ31'Delta));
9 Put_Line ("The minimum value of TQ31 is " & TQ31'Image (TQ31'First));
10 Put_Line ("The maximum value of TQ31 is " & TQ31'Image (TQ31'Last));
11 end Normalized_Fixed_Point_Type;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Normalized_Fixed_Point_Type
MD5: 2fe6e9f9bd20d2cfab959d1c0273280b
Runtime output
In this example, we are defining a 32-bit fixed-point data type for our normalized range.
When running the application, we notice that the upper bound is close to one, but not
exactly one. This is a typical effect of fixed-point data types — you can find more details
in this discussion about the Q format328 . We may also rewrite this code with an exact type
definition:
[Ada]
Listing 8: normalized_adapted_fixed_point_type.ads
1 package Normalized_Adapted_Fixed_Point_Type is
2
3 type TQ31 is delta 2.0 ** (-31) range -1.0 .. 1.0 - 2.0 ** (-31);
4
5 end Normalized_Adapted_Fixed_Point_Type;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Normalized_Adapted_Fixed_Point_
↪Type
MD5: abe5f4e029c7c3c7a069890882b17f50
Listing 9: custom_fixed_point_range.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Numerics; use Ada.Numerics;
3
4 procedure Custom_Fixed_Point_Range is
5 type Inv_Trig is delta 2.0 ** (-15) * Pi range -Pi / 2.0 .. Pi / 2.0;
6 begin
7 Put_Line ("Inv_Trig requires " & Integer'Image (Inv_Trig'Size)
8 & " bits");
9 Put_Line ("The delta value of Inv_Trig is "
10 & Inv_Trig'Image (Inv_Trig'Delta));
11 Put_Line ("The minimum value of Inv_Trig is "
12 & Inv_Trig'Image (Inv_Trig'First));
13 Put_Line ("The maximum value of Inv_Trig is "
14 & Inv_Trig'Image (Inv_Trig'Last));
15 end Custom_Fixed_Point_Range;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Custom_Fixed_Point_Range
MD5: 0d9a4bc96191d1341bbb1c081555b613
Runtime output
In this example, we are defining a 16-bit type called Inv_Trig, which has a range from -π/2
to π/2.
All standard operations are available for fixed-point types. For example:
[Ada]
328 https://en.wikipedia.org/wiki/Q_(number_format)
3 procedure Fixed_Point_Op is
4 type TQ31 is delta 2.0 ** (-31) range -1.0 .. 1.0 - 2.0 ** (-31);
5
6 A, B, R : TQ31;
7 begin
8 A := 0.25;
9 B := 0.50;
10 R := A + B;
11 Put_Line ("R is " & TQ31'Image (R));
12 end Fixed_Point_Op;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Fixed_Point_Op
MD5: 78bafd93b25da898c00cc38c9d518e2a
Runtime output
R is 0.7500000000
4 #define SHIFT_FACTOR 32
5
31 printf("Original value\n");
32 display_fixed(fixed_value);
33
34 printf("... + 0.25\n");
35 fixed_value = add(fixed_value, TO_FIXED(0.25));
36 display_fixed(fixed_value);
37
38 printf("... * 0.5\n");
39 fixed_value = mult(fixed_value, TO_FIXED(0.5));
40 display_fixed(fixed_value);
41
42 return 0;
43 }
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Fixed_Point_C
MD5: 61016e8fc0dbc4d0eefd2c86915489e5
Runtime output
Original value
value (integer) = 536870912
value (float) = 0.25000
... + 0.25
value (integer) = 1073741824
value (float) = 0.50000
... * 0.5
value (integer) = 536870912
value (float) = 0.25000
Here, we declare the fixed-point type fixed based on int and two operations for it: addition
(via the add function) and multiplication (via the mult function). Note that, while fixed-
point addition is quite straightforward, multiplication requires right-shifting to match the
correct internal representation. In Ada, since fixed-point operations are part of the language
specification, they don't need to be emulated. Therefore, no extra effort is required from
the programmer.
Also note that the example above is very rudimentary, so it doesn't take some of the side-
effects of fixed-point arithmetic into account. In C, you have to manually take all side-effects
deriving from fixed-point arithmetic into account, while in Ada, the compiler takes care of
selecting the right operations for you.
Ada has built-in support for handling both volatile and atomic data. Let's start by discussing
volatile objects.
69.5.1 Volatile
A volatile329 object can be described as an object in memory whose value may change
between two consecutive memory accesses of a process A — even if process A itself hasn't
changed the value. This situation may arise when an object in memory is being shared by
multiple threads. For example, a thread B may modify the value of that object between two
read accesses of a thread A. Another typical example is the one of memory-mapped I/O330 ,
where the hardware might be constantly changing the value of an object in memory.
Because the value of a volatile object may be constantly changing, a compiler cannot gen-
erate code that stores the value of that object into a register and use the value from the
register in subsequent operations. Storing into a register is avoided because, if the value
is stored there, it would be outdated if another process had changed the volatile object in
the meantime. Instead, the compiler generates code in such a way that the process must
read the value of the volatile object from memory for each access.
Let's look at a simple example of a volatile variable in C:
[C]
14 return 0;
15 }
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Volatile_Object_C
MD5: 863c7dda4acb3286976a1edab29bab08
Runtime output
val: 999000.000
In this example, val has the modifier volatile, which indicates that the compiler must han-
dle val as a volatile object. Therefore, each read and write access in the loop is performed
by accessing the value of val in then memory.
This is the corresponding implementation in Ada:
329 https://en.wikipedia.org/wiki/Volatile_(computer_programming)
330 https://en.wikipedia.org/wiki/Memory-mapped_I/O
[Ada]
3 procedure Show_Volatile_Object is
4 Val : Long_Float with Volatile;
5 begin
6 Val := 0.0;
7 for I in 0 .. 999 loop
8 Val := Val + 2.0 * Long_Float (I);
9 end loop;
10
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Volatile_Object_Ada
MD5: aa1e276e64e69813bfc3e3ef39f3dd47
Runtime output
Val: 9.99000000000000E+05
In this example, Val has the Volatile aspect, which makes the object volatile. We can
also use the Volatile aspect in type declarations. For example:
[Ada]
3 procedure Show_Volatile_Type is
4 type Volatile_Long_Float is new Long_Float with Volatile;
5
6 Val : Volatile_Long_Float;
7 begin
8 Val := 0.0;
9 for I in 0 .. 999 loop
10 Val := Val + 2.0 * Volatile_Long_Float (I);
11 end loop;
12
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Volatile_Type
MD5: 41ecf028803a58ce244c421eaeb118e4
Runtime output
Val: 9.99000000000000E+05
Here, we're declaring a new type Volatile_Long_Float based on the Long_Float type
and using the Volatile aspect. Any object of this type is automatically volatile.
In addition to that, we can declare components of an array to be volatile. In this case, we
can use the Volatile_Components aspect in the array declaration. For example:
[Ada]
3 procedure Show_Volatile_Array_Components is
4 Arr : array (1 .. 2) of Long_Float with Volatile_Components;
5 begin
6 Arr := (others => 0.0);
7
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Volatile_Array_Components
MD5: 601d61dd01888c60ae1a51ec513138d5
Runtime output
Note that it's possible to use the Volatile aspect for the array declaration as well:
[Ada]
69.5.2 Atomic
An atomic object is an object that only accepts atomic reads and updates. The Ada standard
specifies that "for an atomic object (including an atomic component), all reads and updates
of the object as a whole are indivisible." In this case, the compiler must generate Assembly
code in such a way that reads and updates of an atomic object must be done in a single
instruction, so that no other instruction could execute on that same object before the read
or update completes.
In other contexts
Generally, we can say that operations are said to be atomic when they can be completed
without interruptions. This is an important requirement when we're performing operations
on objects in memory that are shared between multiple processes.
This definition of atomicity above is used, for example, when implementing databases.
However, for this section, we're using the term "atomic" differently. Here, it really means
that reads and updates must be performed with a single Assembly instruction.
For example, if we have a 32-bit object composed of four 8-bit bytes, the compiler cannot
generate code to read or update the object using four 8-bit store / load instructions, or even
two 16-bit store / load instructions. In this case, in order to maintain atomicity, the compiler
must generate code using one 32-bit store / load instruction.
Because of this strict definition, we might have objects for which the Atomic aspect cannot
be specified. Lots of machines support integer types that are larger than the native word-
sized integer. For example, a 16-bit machine probably supports both 16-bit and 32-bit
integers, but only 16-bit integer objects can be marked as atomic — or, more generally,
only objects that fit into at most 16 bits.
Atomicity may be important, for example, when dealing with shared hardware registers.
In fact, for certain architectures, the hardware may require that memory-mapped registers
are handled atomically. In Ada, we can use the Atomic aspect to indicate that an object is
atomic. This is how we can use the aspect to declare a shared hardware register:
[Ada]
3 procedure Show_Shared_HW_Register is
4 R : Integer
5 with Atomic, Address => System'To_Address (16#FFFF00A0#);
6 begin
7 null;
8 end Show_Shared_HW_Register;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Atomic_Object
MD5: 7ef148adf393819fc3fbc25eb45afe46
Note that the Address aspect allows for assigning a variable to a specific location in the
memory. In this example, we're using this aspect to specify the address of the memory-
mapped register. We'll discuss more about the Address aspect later in the section about
mapping structures to bit-fields (page 1488) (in chapter 6).
In addition to atomic objects, we can declare atomic types and atomic array components
— similarly to what we've seen before for volatile objects. For example:
[Ada]
3 procedure Show_Shared_HW_Register is
4 type Atomic_Integer is new Integer with Atomic;
5
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Atomic_Types_Arrays
MD5: 11475b5152087eff7f36abfe2c5ae9a1
In this example, we're declaring the Atomic_Integer type, which is an atomic type. Objects
of this type — such as R in this example — are automatically atomic. This example also
includes the declaration of the Arr array, which has atomic components.
Previously, we've seen that we can use representation clauses (page 1430) to specify a
particular layout for a record type. As mentioned before, this is useful when interfacing with
hardware, drivers, or communication protocols. In this section, we'll extend this concept
for two specific use-cases: register overlays and data streams. Before we discuss those
use-cases, though, we'll first explain the Size aspect and the Size attribute.
The Size aspect indicates the minimum number of bits required to represent an object.
When applied to a type, the Size aspect is telling the compiler to not make record or array
components of a type T any smaller than X bits. Therefore, a common usage for this aspect
is to just confirm expectations: developers specify 'Size to tell the compiler that T should
fit X bits, and the compiler will tell them if they are right (or wrong).
When the specified size value is larger than necessary, it can cause objects to be bigger
in memory than they would be otherwise. For example, for some enumeration types, we
could say for type Enum'Size use 32; when the number of literals would otherwise have
required only a byte. That's useful for unchecked conversions because the sizes of the two
types need to be the same. Likewise, it's useful for interfacing with C, where enum types
are just mapped to the int type, and thus larger than Ada might otherwise require. We'll
discuss unchecked conversions later in the course (page 1502).
Let's look at an example from an earlier chapter:
[Ada]
6 end My_Device_Types;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Size_Aspect
MD5: 049be992b876dba42cf091afc256db35
Here, we're saying that objects of type UInt10 must have at least 10 bits. In this case, if
the code compiles, it is a confirmation that such values can be represented in 10 bits when
packed into an enclosing record or array type.
If the size specified was larger than what the compiler would use by default, then it could
affect the size of objects. For example, for UInt10, anything up to and including 16 would
make no difference on a typical machine. However, anything over 16 would then push
the compiler to use a larger object representation. That would be important for unchecked
conversions, for example.
The Size attribute indicates the number of bits required to represent a type or an object.
We can use the size attribute to retrieve the size of a type or of an object:
[Ada]
5 procedure Show_Device_Types is
6 UInt10_Obj : constant UInt10 := 0;
7 begin
8 Put_Line ("Size of UInt10 type: " & Positive'Image (UInt10'Size));
9 Put_Line ("Size of UInt10 object: " & Positive'Image (UInt10_Obj'Size));
10 end Show_Device_Types;
Project: Courses.Ada_For_Embedded_C_Dev.Perspective.Size_Aspect
MD5: 4e46ad9cf54276b381b960672daa03b9
Runtime output
Here, we're retrieving the actual sizes of the UInt10 type and an object of that type. Note
that the sizes don't necessarily need to match. For example, although the size of UInt10
type is expected to be 10 bits, the size of UInt10_Obj may be 16 bits, depending on the
platform. Also, components of this type within composite types (arrays, records) will prob-
ably be 16 bits as well unless they are packed.
Register overlays make use of representation clauses to create a structure that facilitates
manipulating bits from registers. Let's look at a simplified example of a power manage-
ment controller containing registers such as a system clock enable register. Note that this
example is based on an actual architecture:
[Ada]
3 package Registers is
4
54 end Registers;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.PMC_Peripheral
MD5: d6f37976ca653d65d71ee5ea463df81c
First, we declare the system clock enable register — this is PMC_SCER_Register type in the
code example. Most of the bits in that register are reserved. However, we're interested
in bit #5, which is used to activate or deactivate the system clock. To achieve a correct
representation of this bit, we do the following:
• We declare the USBCLK component of this record using the USB_Clock_Enable type,
which has a size of one bit; and
• we use a representation clause to indicate that the USBCLK component is specifically
at bit #5 of byte #0.
After declaring the system clock enable register and specifying its individual bits as
components of a record type, we declare the power management controller type —
PMC_Peripheral record type in the code example. Here, we declare two 16-bit registers as
record components of PMC_Peripheral. These registers are used to enable or disable the
system clock. The strategy we use in the declaration is similar to the one we've just seen
above:
• We declare these registers as components of the PMC_Peripheral record type;
• we use a representation clause to specify that the PMC_SCER register is at byte #0 and
the PMC_SCDR register is at byte #2.
– Since these registers have 16 bits, we use a range of bits from 0 to 15.
The actual power management controller becomes accessible by the declaration of the
PMC_Periph object of PMC_Peripheral type. Here, we specify the actual address of the
memory-mapped registers (400E0600 in hexadecimal) using the Address aspect in the
declaration. When we use the Address aspect in an object declaration, we're indicating the
address in memory of that object.
Because we specify the address of the memory-mapped registers in the declaration of
PMC_Periph, this object is now an overlay for those registers. This also means that any
operation on this object corresponds to an actual operation on the registers of the power
management controller. We'll discuss more details about overlays in the section about
mapping structures to bit-fields (page 1488) (in chapter 6).
Finally, in a test application, we can access any bit of any register of the power management
controller with simple record component selection. For example, we can set the USBCLK bit
of the PMC_SCER register by using PMC_Periph.PMC_SCER.USBCLK:
[Ada]
3 procedure Enable_USB_Clock is
4 begin
5 Registers.PMC_Periph.PMC_SCER.USBCLK := 1;
6 end Enable_USB_Clock;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.PMC_Peripheral
MD5: b8f35a80d5f04cd362e5309aef33a100
This code example makes use of many aspects and keywords of the Ada language. One
of them is the Volatile aspect, which we've discussed in the section about volatile and
atomic objects (page 1439). Using the Volatile aspect for the PMC_SCER_Register type
ensures that objects of this type won't be stored in a register.
In the declaration of the PMC_SCER_Register record type of the example, we use the
Bit_Order aspect to specify the bit ordering of the record type. Here, we can select one of
these options:
• High_Order_First: first bit of the record is the most significant bit;
• Low_Order_First: first bit of the record is the least significant bit.
The declarations from the Registers package also makes use of the Import, which is some-
times necessary when creating overlays. When used in the context of object declarations,
it avoids default initialization (for data types that have it.). Aspect Import will be discussed
in the section that explains how to map structures to bit-fields (page 1488) in chapter 6.
Please refer to that chapter for more details.
3 procedure Show_Bit_Declaration is
4
8 B : constant Bit := 0;
9 -- ^ Although Bit'Size is 1, B'Size is almost certainly 8
10 begin
11 Put_Line ("Bit'Size = " & Positive'Image (Bit'Size));
12 Put_Line ("B'Size = " & Positive'Image (B'Size));
13 end Show_Bit_Declaration;
Runtime output
Bit'Size = 1
B'Size = 8
In this case, B is almost certainly going to be 8-bits wide on a typical machine, even though
the language requires that Bit'Size is 1 by default.
In the declaration of the components of the PMC_Peripheral record type, we use the
aliased keyword to specify that those record components are accessible via other paths
besides the component name. Therefore, the compiler won't store them in registers. This
makes sense because we want to ensure that we're accessing specific memory-mapped
registers, and not registers assigned by the compiler. Note that, for the same reason, we
also use the aliased keyword in the declaration of the PMC_Periph object.
Creating data streams — in the context of interfacing with devices — means the serialization
of arbitrary information and its transmission over a communication channel. For example,
we might want to transmit the content of memory-mapped registers as byte streams using
a serial port. To do this, we first need to get a serialized representation of those registers
as an array of bytes, which we can then transmit over the serial port.
Serialization of arbitrary record types — including register overlays — can be achieved by
declaring an array of bytes as an overlay. By doing this, we're basically interpreting the
information from those record types as bytes while ignoring their actual structure — i.e.
their components and representation clause. We'll discuss details about overlays in the
section about mapping structures to bit-fields (page 1488) (in chapter 6).
Let's look at a simple example of serialization of an arbitrary record type:
[Ada]
9 end Arbitrary_Types;
9 --
10 -- We can access the serialized data in Raw_TX, which is our overlay
11 --
12 Raw_TX : UByte_Array (1 .. Some_Object'Size / 8)
13 with Address => Some_Object'Address;
14 begin
15 null;
16 --
17 -- Now, we could stream the data from Some_Object.
18 --
19 -- For example, we could send the bytes (from Raw_TX) via the
20 -- serial port.
21 --
22 end Serialize_Data;
4 procedure Data_Stream_Declaration is
5 Dummy_Object : Arbitrary_Types.Arbitrary_Record;
6
7 begin
8 Serialize_Data (Dummy_Object);
9 end Data_Stream_Declaration;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Data_Stream_Declaration
MD5: 1de6f518520010c28fd8deb29a2bf209
The most important part of this example is the implementation of the Serial-
ize_Data procedure, where we declare Raw_TX as an overlay for our arbitrary object
(Some_Object of Arbitrary_Record type). In simple terms, by writing with Address
=> Some_Object'Address; in the declaration of Raw_TX, we're specifying that Raw_TX and
Some_Object have the same address in memory. Here, we are:
• taking the address of Some_Object — using the Address attribute —, and then
• using it as the address of Raw_TX — which is specified with the Address aspect.
By doing this, we're essentially saying that both Raw_TX and Some_Object are different
representations of the same object in memory.
Because the Raw_TX overlay is completely agnostic about the actual structure of the record
type, the Arbitrary_Record type could really be anything. By declaring Raw_TX, we create
an array of bytes that we can use to stream the information from Some_Object.
We can use this approach and create a data stream for the register overlay example that
we've seen before. This is the corresponding implementation:
[Ada]
3 package Registers is
4
54 end Registers;
16 end Serial_Ports;
30 end Serial_Ports;
4 package Data_Stream is
5
12 end Data_Stream;
23 end Data_Stream;
3 with Registers;
4 with Data_Stream;
5 with Serial_Ports;
6
7 procedure Test_Data_Stream is
8
9 procedure Display_Registers is
10 use Ada.Text_IO;
11 begin
12 Put_Line ("---- Registers ----");
13 Put_Line ("PMC_SCER.USBCLK: "
14 & Registers.PMC_Periph.PMC_SCER.USBCLK'Image);
15 Put_Line ("PMC_SCDR.USBCLK: "
16 & Registers.PMC_Periph.PMC_SCDR.USBCLK'Image);
17 Put_Line ("-------------- ----");
18 end Display_Registers;
(continues on next page)
20 Port : Serial_Ports.Serial_Port;
21 begin
22 Registers.PMC_Periph.PMC_SCER.USBCLK := 1;
23 Registers.PMC_Periph.PMC_SCDR.USBCLK := 1;
24
25 Display_Registers;
26
33 Display_Registers;
34 end Test_Data_Stream;
Project: Courses.Ada_For_Embedded_C_Dev.Embedded.Data_Stream
MD5: 3f4e1a184e52a83b1b9de9e3d5cb43bf
Runtime output
In this example, we can find the overlay in the implementation of the Send and Receive
procedures from the Data_Stream package. Because the overlay doesn't need to know the
internals of the PMC_Peripheral type, we're declaring it in the same way as in the previous
example (where we created an overlay for Some_Object). In this case, we're creating an
overlay for the PMC parameter.
Note that, for this section, we're not really interested in the details about the serial port.
Thus, package Serial_Ports in this example is just a stub. However, because the Se-
rial_Port type in that package only sees arrays of bytes, after implementing an actual
serial port interface for a specific device, we could create data streams for any type.
As we've seen in the previous section about interfacing with devices (page 1443), Ada of-
fers powerful features to describe low-level details about the hardware architecture without
giving up its strong typing capabilities. However, it can be cumbersome to create a specifi-
cation for all those low-level details when you have a complex architecture. Fortunately, for
ARM Cortex-M devices, the GNAT toolchain offers an Ada binding generator called svd2ada,
which takes CMSIS-SVD descriptions for those devices and creates Ada specifications that
match the architecture. CMSIS-SVD description files are based on the Cortex Microcon-
troller Software Interface Standard (CMSIS), which is a hardware abstraction layer for ARM
Cortex microcontrollers.
Please refer to the svd2ada project page331 for details about this tool.
331 https://github.com/AdaCore/svd2ada
SEVENTY
In Ada, several common programming errors that are not already detected at compile-time
are detected instead at run-time, triggering "exceptions" that interrupt the normal flow of
execution. For example, an exception is raised by an attempt to access an array component
via an index that is out of bounds. This simple check precludes exploits based on buffer
overflow. Several other cases also raise language-defined exceptions, such as scalar range
constraint violations and null pointer dereferences. Developers may declare and raise their
own application-specific exceptions too. (Exceptions are software artifacts, although an
implementation may map hardware events to exceptions.)
Exceptions are raised during execution of what we will loosely define as a "frame." A frame
is a language construct that has a call stack entry when called, for example a procedure or
function body. There are a few other constructs that are also pertinent but this definition
will suffice for now.
Frames have a sequence of statements implementing their functionality. They can also
have optional "exception handlers" that specify the response when exceptions are "raised"
by those statements. These exceptions could be raised directly within the statements, or
indirectly via calls to other procedures and functions.
For example, the frame below is a procedure including three exceptions handlers:
Listing 1: p.adb
1 procedure P is
2 begin
3 Statements_That_Might_Raise_Exceptions;
4 exception
5 when A =>
6 Handle_A;
7 when B =>
8 Handle_B;
9 when C =>
10 Handle_C;
11 end P;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exceptions
MD5: bf7a8740dfca9f3da993f054e22ca97d
The three exception handlers each start with the word when (lines 5, 7, and 9). Next comes
one or more exception identifiers, followed by the so-called "arrow." In Ada, the arrow always
associates something on the left side with something on the right side. In this case, the left
side is the exception name and the right side is the handler's code for that exception.
1455
Learning Ada
Each handler's code consists of an arbitrary sequence of statements, in this case specific
procedures called in response to those specific exceptions. If exception A is raised we call
procedure Handle_A (line 6), dedicated to doing the actual work of handling that exception.
The other two exceptions are dealt with similarly, on lines 8 and 10.
Structurally, the exception handlers are grouped together and textually separated from
the rest of the code in a frame. As a result, the sequence of statements representing the
normal flow of execution is distinct from the section representing the error handling. The
reserved word exception separates these two sections (line 4 above). This separation
helps simplify the overall flow, increasing understandability. In particular, status result
codes are not required so there is no mixture of error checking and normal processing. If no
exception is raised the exception handler section is automatically skipped when the frame
exits.
Note how the syntactic structure of the exception handling section resembles that of an
Ada case statement. The resemblance is intentional, to suggest similar behavior. When
something in the statements of the normal execution raises an exception, the corresponding
exception handler for that specific exception is executed. After that, the routine completes.
The handlers do not "fall through" to the handlers below. For example, if exception B is
raised, procedure Handle_B is called but Handle_C is not called. There's no need for a
break statement, just as there is no need for it in a case statement. (There's no break
statement in Ada anyway.)
So far, we've seen a frame with three specific exceptions handled. What happens if a frame
has no handler for the actual exception raised? In that case the run-time library code goes
"looking" for one.
Specifically, the active exception is propagated up the dynamic call chain. At each point in
the chain, normal execution in that caller is abandoned and the handlers are examined. If
that caller has a handler for the exception, the handler is executed. That caller then returns
normally to its caller and execution continues from there. Otherwise, propagation goes up
one level in the call chain and the process repeats. The search continues until a matching
handler is found or no callers remain. If a handler is never found the application terminates
abnormally. If the search reaches the main procedure and it has a matching handler it will
execute the handler, but, as always, the routine completes so once again the application
terminates.
For a concrete example, consider the following:
Listing 2: arrays.ads
1 package Arrays is
2
7 end Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exceptions
MD5: a2dfa05b56144e21d5796d39c88ceac2
Listing 3: arrays.adb
1 package body Arrays is
2
8 end Arrays;
Listing 4: some_process.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Arrays; use Arrays;
3
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("Constraint_Error caught in Some_Process");
11 Put_Line ("Some_Process completes normally");
12 end Some_Process;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exceptions
MD5: 7733854601db37eb53f4c4094fe5ca0d
Listing 5: main.adb
1 with Some_Process;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Main is
5 begin
6 Some_Process;
7 Put_Line ("Main completes normally");
8 end Main;
Procedure Main calls Some_Process, which in turn calls function Value (line 7).
Some_Process declares the array object L of type List on line 5, with bounds 1 through
100. The call to Value has arguments, including variable L, leading to an attempt to access
an array component via an out-of-bounds index (1 + 10 * 10 = 101, beyond the last index
of L). This attempt will trigger an exception in Value prior to actually accessing the array
object's memory. Function Value doesn't have any exception handlers so the exception
is propagated up to the caller Some_Process. Procedure Some_Process has an exception
handler for Constraint_Error and it so happens that Constraint_Error is the exception
raised in this case. As a result, the code for that handler will be executed, printing some
messages on the screen. Then procedure Some_Process will return to Main normally. Main
then continues to execute normally after the call to Some_Process and prints its completion
message.
If procedure Some_Process had also not had a handler for Constraint_Error, that proce-
dure call would also have returned abnormally and the exception would have been propa-
gated further up the call chain to procedure Main. Normal execution in Main would likewise
be abandoned in search of a handler. But Main does not have any handlers so Main would
have completed abnormally, immediately, without printing its closing message.
This semantic model is the same as with many other programming languages, in which
the execution of a frame's sequence of statements is unavoidably abandoned when an
exception becomes active. The model is a direct reaction to the use of status codes returned
from functions as in C, where it is all too easy to forget (intentionally or otherwise) to check
the status values returned. With the exception model errors cannot be ignored.
However, full exception propagation as described above is not the norm for embedded
applications when the highest levels of integrity are required. The run-time library code
implementing exception propagation can be rather complex and expensive to certify. Those
problems apply to the application code too, because exception propagation is a form of
control flow without any explicit construct in the source. Instead of the full exception model,
designers of high-integrity applications often take alternative approaches.
One alternative consists of deactivating exceptions altogether, or more precisely, deac-
tivating language-defined checks, which means that the compiler will not generate code
checking for conditions giving rise to exceptions. Of course, this makes the code vulnerable
to attacks, such as buffer overflow, unless otherwise verified (e.g. through static analysis).
Deactivation can be applied at the unit level, through the -gnatp compiler switch, or lo-
cally within a unit via the pragma Suppress. (Refer to the GNAT User’s Guide for Native
Platforms332 for more details about the switch.)
For example, we can write the following. Note the pragma on line 4 of arrays.adb within
function Value:
Listing 6: arrays.ads
1 package Arrays is
2
7 end Arrays;
Listing 7: arrays.adb
1 package body Arrays is
2
9 end Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exception_Suppress
MD5: 62c37774cbcd5f167858d3b5268006aa
Listing 8: some_process.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Arrays; use Arrays;
3
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
This placement of the pragma will only suppress checks in the function body. However,
332 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/building_executable_programs_with_
gnat.html
that is where the exception would otherwise have been raised, leading to incorrect and
unpredictable execution. (Run the program more than once. If it prints the right answer
(42), or even the same value each time, it's just a coincidence.) As you can see, suppressing
checks negates the guarantee of errors being detected and addressed at run-time.
Another alternative is to leave checks enabled but not retain the dynamic call-chain prop-
agation. There are a couple of approaches available in this alternative.
The first approach is for the run-time library to invoke a global "last chance handler" (LCH)
when any exception is raised. Instead of the sequence of statements of an ordinary ex-
ception handler, the LCH is actually a procedure intended to perform "last-wishes" before
the program terminates. No exception handlers are allowed. In this scheme "propagation"
is simply a direct call to the LCH procedure. The default LCH implementation provided
by GNAT does nothing other than loop infinitely. Users may define their own replacement
implementation.
The availability of this approach depends on the run-time library. Typically, Zero Footprint
and Ravenscar SFP run-times will provide this mechanism because they are intended for
certification.
A user-defined LCH handler can be provided either in C or in Ada, with the following profiles:
[Ada]
[C]
We'll go into the details of the pragma Export in a further section on language interfacing.
For now, just know that the symbol __gnat_last_chance_handler is what the run-time
uses to branch immediately to the last-chance handler. Pragma Export associates that
symbol with this replacement procedure so it will be invoked instead of the default routine.
As a consequence, the actual procedure name in Ada is immaterial.
Here is an example implementation that simply blinks an LED forever on the target:
loop
Toggle (LCH_LED);
Next_Release := Next_Release + Period;
delay until Next_Release;
end loop;
end Last_Chance_Handler;
The LCH_LED is a constant referencing the LED used by the last-chance handler, declared
elsewhere. The infinite loop is necessary because a last-chance handler must never return
to the caller (hence the term "last-chance"). The LED changes state every half-second.
Unlike the approach in which there is only the last-chance handler routine, the other ap-
proach allows exception handlers, but in a specific, restricted manner. Whenever an ex-
ception is raised, the only handler that can apply is a matching handler located in the same
frame in which the exception is raised. Propagation in this context is simply an imme-
diate branch instruction issued by the compiler, going directly to the matching handler's
sequence of statements. If there is no matching local handler the last chance handler is
invoked. For example consider the body of function Value in the body of package Arrays:
Listing 9: arrays.ads
1 package Arrays is
2
7 end Arrays;
11 end Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exception_Return
MD5: 1f63b92739deb03529884ab0d25dadb8
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
In both procedure Some_Process and function Value we have an exception handler for
Constraint_Error. In this example the exception is raised in Value because the index
check fails there. A local handler for that exception is present so the handler applies and
the function returns zero, normally. Because the call to the function returns normally, the
execution of Some_Process prints zero and then completes normally.
Let's imagine, however, that function Value did not have a handler for Constraint_Error.
In the context of full exception propagation, the function call would return to the caller, i.e.,
Some_Process, and would be handled in that procedure's handler. But only local handlers
are allowed under the second alternative so the lack of a local handler in Value would
result in the last-chance handler being invoked. The handler for Constraint_Error in
Some_Process under this alternative approach.
So far we've only illustrated handling the Constraint_Error exception. It's possible to
handle other language-defined and user-defined exceptions as well, of course. It is even
possible to define a single handler for all other exceptions that might be encountered in
the handled sequence of statements, beyond those explicitly named. The "name" for this
otherwise anonymous exception is the Ada reserved word others. As in case statements,
it covers all other choices not explicitly mentioned, and so must come last. For example:
7 end Arrays;
13 end Arrays;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Exception_Return_Others
MD5: 7c2ed7efa23242f502a6cf4767da0192
4 procedure Some_Process is
5 L : constant List (1 .. 100) := (others => 42);
6 begin
7 Put_Line (Integer'Image (Value (L, 1, 10)));
8 exception
9 when Constraint_Error =>
10 Put_Line ("FAILURE");
11 end Some_Process;
In the code above, the Value function has a handler specifically for Constraint_Error
as before, but also now has a handler for all other exceptions. For any exception other
than Constraint_Error, function Value returns -1. If you remove the function's handler
for Constraint_Error (lines 7 and 8) then the other "anonymous" handler will catch the
exception and -1 will be returned instead of zero.
There are additional capabilities for exceptions, but for now you have a good basic under-
standing of how exceptions work, especially their dynamic nature at run-time.
So far, we have discussed language-defined checks inserted by the compiler for verification
at run-time, leading to exceptions being raised. We saw that these dynamic checks verified
semantic conditions ensuring proper execution, such as preventing writing past the end of
a buffer, or exceeding an application-specific integer range constraint, and so on. These
checks are defined by the language because they apply generally and can be expressed in
language-defined terms.
Developers can also define dynamic checks. These checks specify component-specific or
application-specific conditions, expressed in terms defined by the component or applica-
tion. We will refer to these checks as "user-defined" for convenience. (Be sure you under-
stand that we are not talking about user-defined exceptions here.)
Like the language-defined checks, user-defined checks must be true at run-time. All checks
consist of Boolean conditions, which is why we can refer to them as assertions: their con-
ditions are asserted to be true by the compiler or developer.
Assertions come in several forms, some relatively low-level, such as a simple pragma As-
sert, and some high-level, such as type invariants and contracts. These forms will be
presented in detail in a later section, but we will illustrate some of them here.
User-defined checks can be enabled at run-time in GNAT with the -gnata switch, as well as
with pragma Assertion_Policy. The switch enables all forms of these assertions, whereas
the pragma can be used to control specific forms. The switch is typically used but there are
reasonable use-cases in which some user-defined checks are enabled, and others, although
defined, are disabled.
By default in GNAT, language-defined checks are enabled but user-defined checks are dis-
abled. Here's an example of a simple program employing a low-level assertion. We can use
it to show the effects of the switches, including the defaults:
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Low_Level_Assertion
MD5: 2eb5e1879740cc3914acb8a362995b31
If we compiled this code we would get a warning about the assignment on line 8 after the
pragma Assert, but not one about the Assert itself on line 7.
gprbuild -q -P main.gpr
main.adb:8:11: warning: value not in range of type "Standard.Positive"
main.adb:8:11: warning: "Constraint_Error" will be raised at run time
No code is generated for the user-defined check expressed via pragma Assert but the
language-defined check is emitted. In this case the range constraint on X excludes zero
and negative numbers, but X * 5 = 50, X - 99 = -49. As a result, the check for the last
assignment would fail, raising Constraint_Error when the program runs. These results
are the expected behavior for the default switch settings.
But now let's enable user-defined checks and build it. Different compiler output will appear.
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Assert
MD5: 2eb5e1879740cc3914acb8a362995b31
Build output
Runtime output
Now we also get the compiler warning about the pragma Assert condition. When
run, the failure of pragma Assert on line 7 raises the exception Ada.Assertions.
Assertion_Error. According to the expression in the assertion, X is expected (incor-
rectly) to be above 99 after the multiplication. (The exception name in the error mes-
sage, SYSTEM.ASSERTIONS.ASSERT_FAILURE, is a GNAT-specific alias for Ada.Assertions.
Assertion_Error.)
It's interesting to see in the output that the compiler can detect some violations at compile-
time:
Generally speaking, a complete analysis is beyond the scope of compilers and they may
not find all errors prior to execution, even those we might detect ourselves by inspection.
More errors can be found by tools dedicated to that purpose, known as static analyzers. But
even an automated static analysis tool cannot guarantee it will find all potential problems.
A much more powerful alternative is formal proof, a form of static analysis that can (when
possible) give strong guarantees about the checks, for all possible conditions and all pos-
sible inputs. Proof can be applied to both language-defined and user-defined checks.
Be sure you understand that formal proof, as a form of static analysis, verifies conditions
prior to execution, even prior to compilation. That earliness provides significant cost ben-
efits. Removing bugs earlier is far less expensive than doing so later because the cost to
fix bugs increases exponentially over the phases of the project life cycle, especially after
deployment. Preventing bug introduction into the deployed system is the least expensive
approach of all. Furthermore, cost savings during the initial development will be possible
as well, for reasons specific to proof. We will revisit this topic later in this section.
Formal analysis for proof can be achieved through the SPARK subset of the Ada language
combined with the gnatprove verification tool. SPARK is a subset encompassing most of
the Ada language, except for features that preclude proof. As a disclaimer, this course is
not aimed at providing a full introduction to proof and the SPARK language, but rather to
present in a few examples what it is about and what it can do for us.
As it turns out, our procedure Main is already SPARK compliant so we can start verifying it.
3 procedure Main is
4 X : Positive := 10;
5 begin
6 X := X * 5;
7 pragma Assert (X > 99);
8 X := X - 99;
9 Put_Line (Integer'Image (X));
10 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Assert
MD5: 98cad2c7e7b7a12740db013727f01d45
Build output
Prover output
Runtime output
The "Prove" button invokes gnatprove on main.adb. You can ignore the parameters to the
invocation. For the purpose of this demonstration, the interesting output is this message:
main.adb:7:19: medium: assertion might fail, cannot prove X > 99 (e.g. when X = 50)
gnatprove can tell that the assertion X > 99 may have a problem. There's indeed a bug
here, and gnatprove even gives us the counterexample (when X is 50). As a result the code
is not proven and we know we have an error to correct.
Notice that the message says the assertion "might fail" even though clearly gnatprove has
an example for when failure is certain. That wording is a reflection of the fact that SPARK
gives strong guarantees when the assertions are proven to hold, but does not guarantee
that flagged problems are indeed problems. In other words, gnatprove does not give false
positives but false negatives are possible. The result is that if gnatprove does not indicate
a problem for the code under analysis we can be sure there is no problem, but if gnatprove
does indicate a problem the tool may be wrong.
An immediate benefit from having our code compatible with the SPARK subset is that we
can ask gnatprove to verify initialization and correct data flow, as indicated by the absence
of messages during SPARK "flow analysis." Flow analysis detects programming errors such
as reading uninitialized data, problematic aliasing between formal parameters, and data
races between concurrent tasks.
In addition, gnatprove checks unit specifications for the actual data read or written, and the
flow of information from inputs to outputs. As you can imagine, this verification provides
significant benefits, and it can be reached with comparatively low cost.
For example, the following illustrates an initialization failure:
4 procedure Main is
5 B : Integer;
6 begin
7 Increment (B);
8 Put_Line (B'Image);
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_0
MD5: 06d432a84d94635bb7bddafd9574a748
Prover output
Granted, Increment is a silly procedure as-is, but imagine it did useful things, and, as part
of that, incremented the argument. gnatprove tells us that the caller has not assigned a
value to the argument passed to Increment.
Consider this next routine, which contains a serious coding error. Flow analysis will find it
for us.
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_1
MD5: af7f16a9c83359c49fde44ed4796c8ec
Prover output
So far, we've seen assertions in a routine's sequence of statements, either through implicit
language-defined checks (is the index in the right range?) or explicit user-defined checks.
These checks are already useful by themselves but they have an important limitation: the
assertions are in the implementation, hidden from the callers of the routine. For example,
a call's success or failure may depend upon certain input values but the caller doesn't have
that information.
Generally speaking, Ada and SPARK put a lot of emphasis on strong, complete specifications
for the sake of abstraction and analysis. Callers need not examine the implementations to
determine whether the arguments passed to it are changed, for example. It is possible to go
beyond that, however, to specify implementation constraints and functional requirements.
We use contracts to do so.
At the language level, contracts are higher-level forms of assertions associated with speci-
fications and declarations rather than sequences of statements. Like other assertions they
can be activated or deactivated at run-time, and can be statically proven. We'll concen-
trate here on two kinds of contracts, both associated especially (but not exclusively) with
procedures and functions:
• Preconditions, those Boolean conditions required to be true prior to a call of the cor-
responding subprogram
• Postconditions, those Boolean conditions required to be true after a call, as a result of
the corresponding subprogram's execution
In particular, preconditions specify the initial conditions, if any, required for the called rou-
tine to correctly execute. Postconditions, on the other hand, specify what the called rou-
tine's execution must have done, at least, on normal completion. Therefore, preconditions
are obligations on callers (referred to as "clients") and postconditions are obligations on
implementers. By the same token, preconditions are guarantees to the implementers, and
postconditions are guarantees to clients.
Contract-based programming, then, is the specification and rigorous enforcement of these
obligations and guarantees. Enforcement is rigorous because it is not manual, but tool-
based: dynamically at run-time with exceptions, or, with SPARK, statically, prior to build.
Preconditions are specified via the "Pre" aspect. Postconditions are specified via the "Post"
aspect. Usually subprograms have separate declarations and these aspects appear with
those declarations, even though they are about the bodies. Placement on the declarations
allows the obligations and guarantees to be visible to all parties. For example:
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_2
MD5: 0fb78847a167d9318b00667c59a7038d
The precondition on line 2 specifies that, for any given call, the sum of the values passed
to parameters X and Y must not be zero. (Perhaps we're dividing by X + Y in the body.) The
declaration also provides a guarantee about the function call's result, via the postcondition
on line 3: for any given call, the value returned will be greater than the value passed to X.
Consider a client calling this function:
4 procedure Demo is
5 A, B, C : Integer;
6 begin
7 A := Mid (1, 2);
8 B := Mid (1, -1);
9 C := Mid (A, B);
10 Put_Line (C'Image);
11 end Demo;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_2
MD5: 3e0617d4b1c14b37a81377456bf73eb5
Prover output
gnatprove indicates that the assignment to B (line 8) might fail because of the precondition,
i.e., the sum of the inputs shouldn't be 0, yet -1 + 1 = 0. (We will address the other output
message elsewhere.)
Let's change the argument passed to Y in the second call (line 8). Instead of -1 we will pass
-2:
4 procedure Demo is
5 A, B, C : Integer;
6 begin
7 A := Mid (1, 2);
8 B := Mid (1, -2);
9 C := Mid (A, B);
10 Put_Line (C'Image);
11 end Demo;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_3
MD5: 496937d76e16ba524f98f5a94398e929
Prover output
The second call will no longer be flagged for the precondition. In addition, gnatprove will
know from the postcondition that A has to be greater than 1, as does B, because in both
calls 1 was passed to X. Therefore, gnatprove can deduce that the precondition will hold
for the third call C := Mid (A, B); because the sum of two numbers greater than 1 will
never be zero.
Postconditions can also compare the state prior to a call with the state after a call, using
the 'Old attribute. For example:
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Contracts_4
MD5: b879dcff91cb4fbce5501474b7f2e732
Prover output
The postcondition specifies that, on return, the argument passed to the parameter Value
will be one greater than it was immediately prior to the call (Value'Old).
10 end P;
10 V := Data (Index);
11 Index := Index + 1;
12 end Read;
13 end P;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Defensive
MD5: 4b4767100079b228f4f3c630d267ec53
Prover output
In addition to procedure Read we would also have a way to load the array components in
the first place, but we can ignore that for the purpose of this discussion.
Procedure Read is responsible for reading an element of the array and then incrementing
the index. What should it do in case of an invalid index? In this implementation there is
defensive code that returns a value arbitrarily chosen. We could also redesign the code to
return a status in this case, or — better — raise an exception.
An even more robust approach would be instead to ensure that this subprogram is only
called when Index is within the indexing boundaries of Data. We can express that require-
ment with a precondition (line 9).
11 end P;
9 end P;
Project: Courses.Ada_For_Embedded_C_Dev.SPARK.Defensive
MD5: 9646614c34d191be51b4522c972538aa
Prover output
Now we don't need the defensive code in the procedure body. That's safe because SPARK
will attempt to prove statically that the check will not fail at the point of each call.
Assuming that procedure Read is intended to be the only way to get values from the array,
in a real application (where the principles of software engineering apply) we would take ad-
vantage of the compile-time visibility controls that packages offer. Specifically, we would
move all the variables' declarations to the private part of the package, or even the pack-
age body, so that client code could not possibly access the array directly. Only procedure
Read would remain visible to clients, thus remaining the only means of accessing the ar-
ray. However, that change would entail others, and in this chapter we are only concerned
with introducing the capabilities of SPARK. Therefore, we keep the examples as simple as
possible.
Earlier we said that gnatprove will verify both language-defined and user-defined checks.
Proving that the language-defined checks will not raise exceptions at run-time is known as
proving "Absence of Run-Time Errors" or AoRTE for short. Successful proof of these checks
is highly significant in itself.
One of the major resulting benefits is that we can deploy the final executable with checks
disabled. That has obvious performance benefits, but it is also a safety issue. If we disable
the checks we also disable the run-time library support for them, but in that case the lan-
guage does not define what happens if indeed an exception is raised. Formally speaking,
anything could happen. We must have good reason for thinking that exceptions cannot be
raised.
This is such an important issue that proof of AoRTE can be used to comply with the objec-
tives of certification standards in various high-integrity domains (for example, DO-178B/C in
avionics, EN 50128 in railway, IEC 61508 in many safety-related industries, ECSS-Q-ST-80C
in space, IEC 60880 in nuclear, IEC 62304 in medical, and ISO 26262 in automotive).
As a result, the quality of the program can be guaranteed to achieve higher levels of in-
tegrity than would be possible in other programming languages.
However, successful proof of AoRTE may require additional assertions, especially precon-
ditions. We can see that with procedure Increment, the procedure that takes an Integer
argument and increments it by one. But of course, if the incoming value of the argument
is the largest possible positive value, the attempt to increment it would overflow, raising
Constraint_Error. (As you have likely already concluded, Constraint_Error is the most
common exception you will have to deal with.) We added a precondition to allow only the
integer values up to, but not including, the largest positive value:
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
Prove it, then comment-out the precondition and try proving it again. Not only will gnat-
prove tell us what is wrong, it will suggest a solution as well.
Without the precondition the check it provides would have to be implemented as defensive
code in the body. One or the other is critical here, but note that we should never need both.
The postcondition on Increment expresses what is, in fact, a unit-level requirement. Suc-
cessfully proving such requirements is another significant robustness and cost benefit. To-
gether with the proofs for initialization and AoRTE, these proofs ensure program integrity,
that is, the program executes within safe boundaries: the control flow of the program is cor-
rectly programmed and cannot be circumvented through run-time errors, and data cannot
be corrupted.
We can go even further. We can use contracts to express arbitrary abstract properties when
such exist. Safety and security properties, for instance, could be expressed as postcondi-
tions and then proven by gnatprove.
For example, imagine we have a procedure to move a train to a new position on the track,
and we want to do so safely, without leading to a collision with another train. Procedure
Move, therefore, takes two inputs: a train identifier specifying which train to move, and the
intended new position. The procedure's output is a value indicating a motion command to
be given to the train in order to go to that new position. If the train cannot go to that new
position safely the output command is to stop the train. Otherwise the command is for the
train to continue at an indicated speed:
type Move_Result is (Full_Speed, Slow_Down, Keep_Going, Stop);
procedure Move
(Train : in Train_Id;
New_Position : in Train_Position;
Result : out Move_Result)
(continues on next page)
The preconditions specify that, given a safe initial state and a valid move, the result of the
call will also be a safe state: there will be at most one train per track section and the track
signaling system will not allow any unsafe movements.
Make sure you understand that gnatprove does not attempt to prove the program correct
as a whole. It attempts to prove language-defined and user-defined assertions about parts
of the program, especially individual routines and calls to those routines. Furthermore,
gnatprove proves the routines correct only to the extent that the user-defined assertions
correctly and sufficiently describe and constrain the implementation of the corresponding
routines.
Although we are not proving whole program correctness, as you will have seen — and done
— we can prove properties than make our software far more robust and bug-free than is
possible otherwise. But in addition, consider what proving the unit-level requirements for
your procedures and functions would do for the cost of unit testing and system integration.
The tests would pass the first time.
However, within the scope of what SPARK can do, not everything can be proven. In some
cases that is because the software behavior is not amenable to expression as boolean
conditions (for example, a mouse driver). In other cases the source code is beyond the
capabilities of the analyzers that actually do the mathematical proof. In these cases the
combination of proof and actual test is appropriate, and still less expensive that testing
alone.
There is, of course, much more to be said about what can be done with SPARK and gnat-
prove. Those topics are reserved for the Introduction to SPARK (page 973) course.
SEVENTYONE
One question that may arise relatively soon when converting from C to Ada is the style of
source code presentation. The Ada language doesn't impose any particular style and for
many reasons, it may seem attractive to keep a C-like style — for example, camel casing
— to the Ada program.
However, the code in the Ada language standard, most third-party code, and the libraries
provided by GNAT follow a specific style for identifiers and reserved words. Using a different
style for the rest of the program leads to inconsistencies, thereby decreasing readability and
confusing automatic style checkers. For those reasons, it's usually advisable to adopt the
Ada style — in which each identifier starts with an upper case letter, followed by lower case
letters (or digits), with an underscore separating two "distinct" words within the identifier.
Acronyms within identifiers are in upper case. For example, there is a language-defined
package named Ada.Text_IO. Reserved words are all lower case.
Following this scheme doesn't preclude adding additional, project-specific rules.
Before even considering translating code from C to Ada, it's worthwhile to evaluate the
possibility of keeping a portion of the C code intact, and only translating selected modules
to Ada. This is a necessary evil when introducing Ada to an existing large C codebase,
where re-writing the entire code upfront is not practical nor cost-effective.
Fortunately, Ada has a dedicated set of features for interfacing with other languages. The
Interfaces package hierarchy and the pragmas Convention, Import, and Export allow
you to make inter-language calls while observing proper data representation for each lan-
guage.
Let's start with the following C code:
[C]
Listing 1: call.c
1 #include <stdio.h>
2
3 struct my_struct {
4 int A, B;
5 };
6
1475
Learning Ada
To call that function from Ada, the Ada compiler requires a description of the data struc-
ture to pass as well as a description of the function itself. To capture how the C struct
my_struct is represented, we can use the following record along with a pragma Conven-
tion. The pragma directs the compiler to lay out the data in memory the way a C compiler
would.
[Ada]
Listing 2: use_my_struct.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Interfaces.C;
3
4 procedure Use_My_Struct is
5
Runtime output
V = ( 1 2)
Describing a foreign subprogram call to Ada code is called binding and it is performed in
two stages. First, an Ada subprogram specification equivalent to the C function is coded.
A C function returning a value maps to an Ada function, and a void function maps to an
Ada procedure. Then, rather than implementing the subprogram using Ada code, we use a
pragma Import:
procedure Call (V : my_struct);
pragma Import (C, Call, "call"); -- Third argument optional
The Import pragma specifies that whenever Call is invoked by Ada code, it should invoke
the Call function with the C calling convention.
And that's all that's necessary. Here's an example of a call to Call:
[Ada]
Listing 3: use_my_struct.adb
1 with Interfaces.C;
2
Project: Courses.Ada_For_Embedded_C_Dev.Translation.My_Struct
MD5: 9b54edadd406c7f5a2b9f8b8f82a4a88
The easiest way to build an application using mixed C / Ada code is to create a simple
project file for gprbuild and specify C as an additional language. By default, when using
gprbuild we only compile Ada source files. To compile C code files as well, we use the
Languages attribute and specify c as an option, as in the following example of a project file
named default.gpr:
project Default is
end Default;
Then, we use this project file to build the application by simply calling gprbuild. Alterna-
tively, we can specify the project file on the command-line with the -P option — for example,
gprbuild -P default.gpr. In both cases, gprbuild compiles all C source-code file found
in the directory and links the corresponding object files to build the executable.
In order to include debug information, you can use gprbuild -cargs -g. This option adds
debug information based on both C and Ada code to the executable. Alternatively, you can
specify a Builder package in the project file and include global compilation switches for
each language using the Global_Compilation_Switches attribute. For example:
project Default is
package Builder is
for Global_Compilation_Switches ("Ada") use ("-g");
for Global_Compilation_Switches ("C") use ("-g");
end Builder;
end Default;
In this case, you can simply run gprbuild -P default.gpr to build the executable.
To debug the executable, you can use programs such as gdb or ddd, which are suitable for
debugging both C and Ada source-code. If you prefer a complete IDE, you may want to look
into GNAT Studio, which supports building and debugging an application within a single
environment, and remotely running applications loaded to various embedded devices. You
can find more information about gprbuild and GNAT Studio in the Introduction to GNAT
Toolchain (page 1681) course.
It may be useful to start interfacing Ada and C by using automatic binding generators. These
can be done either by invoking gcc -fdump-ada-spec option (to generate an Ada binding
to a C header file) or -gnatceg option (to generate a C binding to an Ada specification file).
For example:
The level of interfacing is very low level and typically requires either massaging (changing
the generated files) or wrapping (calling the generated files from a higher level interface).
For example, numbers bound from C to Ada are only standard numbers where user-defined
types may be desirable. C uses a lot of by-pointer parameters which may be better replaced
by other parameter modes, etc.
However, the automatic binding generator helps having a starting point which ensures
compatibility of the Ada and the C code.
It is relatively straightforward to pass an array from Ada to C. In particular, with the GNAT
compiler, passing an array is equivalent to passing a pointer to its first element. Of course,
as there's no notion of boundaries in C, the length of the array needs to be passed explicitly.
For example:
[C]
Listing 4: p.h
1 void p (int * a, int length);
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Arr_1
MD5: 123353e301a3d43016d2799855e6732a
[Ada]
Listing 5: main.adb
1 procedure Main is
2 type Arr is array (Integer range <>) of Integer;
3
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Arr_1
MD5: 9bfbc0f31da4554a1e1dea1ba2b1d305
The other way around — that is, retrieving an array that has been creating on the C side —
is more difficult. Because C doesn't explicitly carry boundaries, they need to be recreated
in some way.
The first option is to actually create an Ada array without boundaries. This is the most
flexible, but also the least safe option. It involves creating an array with indices over the
full range of Integer without ever creating it from Ada, but instead retrieving it as an access
from C. For example:
[C]
Listing 6: f.h
1 int * f ();
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Arr_2
MD5: 19e33efb6d7d46778b88baa2709111e5
[Ada]
Listing 7: main.adb
1 procedure Main is
2 type Arr is array (Integer) of Integer;
3 type Arr_A is access all Arr;
4
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Arr_2
MD5: b52213bcdd8db5e8abfcb8effabb84df
Note that Arr is a constrained type (it doesn't have the range <> notation for indices). For
that reason, as it would be for C, it's possible to iterate over the whole range of integer,
beyond the memory actually allocated for the array.
A somewhat safer way is to overlay an Ada array over the C one. This requires having
access to the length of the array. This time, let's consider two cases, one with an array
and its size accessible through functions, another one on global variables. This time, as
we're using an overlay, the function will be directly mapped to an Ada function returning
an address:
[C]
Listing 8: fg.h
1 int * f_arr (void);
2 int f_size (void);
3
4 int * g_arr;
5 int g_size;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Arr_3
MD5: b315ec2e5d9fdd297ba295ccbae910bc
[Ada]
Listing 9: fg.ads
1 with System;
2
3 package Fg is
4
15 G_Size : Integer;
16 pragma Import (C, G_Size, "g_size");
17
21 end Fg;
3 procedure Main is
4 begin
5 null;
6 end Main;
With all solutions though, importing an array from C is a relatively unsafe pattern, as there's
only so much information on the array as there would be on the C side in the first place.
These are good places for careful peer reviews.
When interfacing Ada and C, the rules of parameter passing are a bit different with regards
to what's a reference and what's a copy. Scalar types and pointers are passed by value,
whereas record and arrays are (almost) always passed by reference. However, there may
be cases where the C interface also passes values and not pointers to objects. Here's a
slightly modified version of a previous example to illustrate this point:
[C]
3 struct my_struct {
4 int A, B;
5 };
6
In Ada, a type can be modified so that parameters of this type can always be passed by
copy.
[Ada]
3 procedure Main is
4 type my_struct is record
5 A : Interfaces.C.int;
6 B : Interfaces.C.int;
7 end record
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Param_By_Value_Ada
MD5: 16e97033bdffb2bacc0cf3322c019a94
Note that this cannot be done at the subprogram declaration level, so if there is a mix of
by-copy and by-reference calls, two different types need to be used on the Ada side.
Because of the absence of namespaces, any global name in C tends to be very long. And
because of the absence of overloading, they can even encode type names in their type.
In Ada, the package is a namespace — two entities declared in two different packages are
clearly identified and can always be specifically designated. The C names are usually a
good indication of the names of the future packages and should be stripped — it is possible
to use the full name if useful. For example, here's how the following declaration and call
could be translated:
[C]
7 return 0;
8 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Namespaces
MD5: e8c25da648a2e8662d97a9a5b863a5bc
[Ada]
7 end Register_Interface;
3 procedure Main is
4 begin
5 Register_Interface.Initialize (15);
6 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Namespaces
MD5: 934edd7d3c74d058f862a786582a32c0
Note that in the above example, a use clause on Register_Interface could allow us to
omit the prefix.
71.8 Pointers
The first thing to ask when translating pointers from C to Ada is: are they needed in the
first place? In Ada, pointers (or access types) should only be used with complex structures
that cannot be allocated at run-time — think of a linked list or a graph for example. There
are many other situations that would need a pointer in C, but do not in Ada, in particular:
• Arrays, even when dynamically allocated
• Results of functions
• Passing large structures as parameters
• Access to registers
• ... others
This is not to say that pointers aren't used in these cases but, more often than not, the
pointer is hidden from the user and automatically handled by the code generated by the
compiler; thus avoiding possible mistakes from being made. Generally speaking, when
looking at C code, it's good practice to start by analyzing how many pointers are used and
to translate as many as possible into pointerless Ada structures.
Here are a few examples of such patterns — additional examples can be found throughout
this document.
Dynamically allocated arrays can be directly allocated on the stack:
[C]
3 int main() {
4 int *a = malloc(sizeof(int) * 10);
5
6 return 0;
7 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Array_Stack_Alloc_C
MD5: a922c3e163494339d6773c6ab1256549
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Array_Stack_Alloc_Ada
MD5: 2e4196c2a2016244a48de153beaa2b49
Build output
main.adb:3:04: warning: variable "A" is never read and never assigned [-gnatwv]
It's even possible to create a such an array within a structure, provided that the size of the
array is known when instantiating this object, using a type discriminant:
[C]
3 typedef struct {
4 int * a;
5 } S;
6
13 return 0;
14 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Struct_Array_Stack_Alloc_C
MD5: f8e5a877977387986b3e2353834a2989
[Ada]
8 V : S (9);
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Struct_Array_Stack_Alloc_Ada
MD5: 955c704bdbe4b2b788e4a790ade12df7
Build output
main.adb:8:04: warning: variable "V" is never read and never assigned [-gnatwv]
With regards to parameter passing, usage mode (input / output) should be preferred to
implementation mode (by copy or by reference). The Ada compiler will automatically pass
a reference when needed. This works also for smaller objects, so that the compiler will copy
in an out when needed. One of the advantages of this approach is that it clarifies the nature
of the object: in particular, it differentiates between arrays and scalars. For example:
[C]
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Array_In_Out_C
MD5: c2c936dd3afc4850c5869e4db73bb36b
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Array_In_Out_Ada
MD5: cf8e51391c9fd8608183c9dae2aa2802
Most of the time, access to registers end up in some specific structures being mapped onto
a specific location in memory. In Ada, this can be achieved through an Address clause
associated to a variable, for example:
[C]
5 return 0;
6 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Address_C
MD5: e810538d72d835a04736fcaf732f1930
[Ada]
3 procedure Test is
4 R : Integer with Address => System'To_Address (16#FFFF00A0#);
5 begin
6 null;
7 end Test;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Address_Ada
MD5: 1263f7289cec6673f19d88bffbeead48
These are some of the most common misuse of pointers in Ada. Previous sections of the
document deal with specifically using access types if absolutely necessary.
Bitwise operations such as masks and shifts in Ada should be relatively rarely needed, and,
when translating C code, it's good practice to consider alternatives. In a lot of cases, these
operations are used to insert several pieces of data into a larger structure. In Ada, this can
be done by describing the structure layout at the type level through representation clauses,
and then accessing this structure as any other.
Consider the case of using a C primitive type as a container for single bit boolean flags. In
C, this would be done through masks, e.g.:
[C]
12 return 0;
13 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Flags_C
MD5: cf903dee1fb1d78d74dc42b66adcdbd5
In Ada, the above can be represented through a Boolean array of enumerate values:
[Ada]
6 Value : Value_Array :=
7 (Flag_2 => True,
8 Flag_4 => True,
9 others => False);
10 begin
11 null;
12 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Flags_Ada
MD5: c92c8532763469f5e4d1027df2bd6a6b
Note the Pack directive for the array, which requests that the array takes as little space as
possible.
It is also possible to map records on memory when additional control over the representation
is needed or more complex data are used:
[C]
5 value = (2 << 1) | 1;
6
7 return 0;
8 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Rec_Map_C
MD5: 16606f11ab3e9c86d3e1d88ac9c3f37f
[Ada]
3 procedure Main is
4 type Value_Type is mod 2 ** 32;
5 pragma Provide_Shift_Operators (Value_Type);
6
7 Value : Value_Type;
8 begin
9 Value := Shift_Left (2, 1) or 1;
10 Put_Line ("Value = " & Value_Type'Image (Value));
11 end Main;
Runtime output
Value = 5
In the previous section, we've seen how to perform bitwise operations. In this section, we
look at how to interpret a data type as a bit-field and perform low-level operations on it.
In general, you can create a bit-field from any arbitrary data type. First, we declare a bit-
field type like this:
[Ada]
type Bit_Field is array (Natural range <>) of Boolean with Pack;
As we've seen previously, the Pack aspect declared at the end of the type declaration
indicates that the compiler should optimize for size. We must use this aspect to be able to
interpret data types as a bit-field.
Then, we can use the Size and the Address attributes of an object of any type to de-
clare a bit-field for this object. We've discussed the Size attribute earlier in this course
(page 1443).
The Address attribute indicates the address in memory of that object. For example, as-
suming we've declare a variable V, we can declare an actual bit-field object by referring to
the Address attribute of V and using it in the declaration of the bit-field, as shown here:
[Ada]
Note that, in this declaration, we're using the Address attribute of V for the Address aspect
of B.
This technique is called overlays for serialization. Now, any operation that we perform on
B will have a direct impact on V, since both are using the same memory location.
The approach that we use in this section relies on the Address aspect. Another approach
would be to use unchecked conversions, which we'll discuss in the next section (page 1502).
We should add the Volatile aspect to the declaration to cover the case when both objects
can still be changed independently — they need to be volatile, otherwise one change might
be missed. This is the updated declaration:
[Ada]
Using the Volatile aspect is important at high level of optimizations. You can find further
details about this aspect in the section about the Volatile and Atomic aspects (page 1439).
Another important aspect that should be added is Import. When used in the context of ob-
ject declarations, it'll avoid default initialization which could overwrite the existing content
while creating the overlay — see an example in the admonition below. The declaration now
becomes:
B : Bit_Field (0 .. V'Size - 1)
with
Address => V'Address, Import, Volatile;
3 procedure Simple_Bitfield is
4 type Bit_Field is array (Natural range <>) of Boolean with Pack;
5
6 V : Integer := 0;
7 B : Bit_Field (0 .. V'Size - 1)
8 with Address => V'Address, Import, Volatile;
9 begin
10 B (2) := True;
11 Put_Line ("V = " & Integer'Image (V));
12 end Simple_Bitfield;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Bitfield_Ada
MD5: 193a2db91619426a145cd267f873145f
Runtime output
V = 4
In this example, we first initialize V with zero. Then, we use the bit-field B and set the third
element (B (2)) to True. This automatically sets bit #3 of V to 1. Therefore, as expected,
the application displays the message V = 4, which corresponds to 22 = 4.
Note that, in the declaration of the bit-field type above, we could also have used a positive
range. For example:
type Bit_Field is array (Positive range <>) of Boolean with Pack;
B : Bit_Field (1 .. V'Size)
with Address => V'Address, Import, Volatile;
The only difference in this case is that the first bit is B (1) instead of B (0).
In C, we would rely on bit-shifting and masking to set that specific bit:
[C]
7 v = v | (1 << 2);
8
11 return 0;
12 }
Runtime output
v = 4
Important
Ada has the concept of default initialization. For example, you may set the default value of
record components:
[Ada]
3 procedure Main is
4
10 R : Rec;
11 begin
12 Put_Line ("R.X = " & Integer'Image (R.X));
13 Put_Line ("R.Y = " & Integer'Image (R.Y));
14 end Main;
Runtime output
R.X = 10
R.Y = 11
In the code above, we don't explicitly initialize the components of R, so they still have the
default values 10 and 11, which are displayed by the application.
Likewise, the Default_Value aspect can be used to specify the default value in other kinds
of type declarations. For example:
[Ada]
3 procedure Main is
4
8 P : Percentage;
9 begin
10 Put_Line ("P = " & Percentage'Image (P));
11 end Main;
Runtime output
P = 10
When declaring an object whose type has a default value, the object will automatically be
initialized with the default value. In the example above, P is automatically initialized with
10, which is the default value of the Percentage type.
Some types have an implicit default value. For example, access types have a default value
of null.
As we've just seen, when declaring objects for types with associated default values, auto-
matic initialization will happen. This can also happens when creating an overlay with the
Address aspect. The default value is then used to overwrite the content at the memory
location indicated by the address. However, in most situations, this isn't the behavior we
expect, since overlays are usually created to analyze and manipulate existing values. Let's
look at an example where this happens:
[Ada]
3 package body P is
4
16 end P;
3 with P; use P;
4
5 procedure Main is
6 V : Integer := 10;
7 begin
8 Put_Line ("V = " & Integer'Image (V));
9 Display_Bytes_Increment (V);
10 Put_Line ("V = " & Integer'Image (V));
11 end Main;
Build output
p.adb:7:14: warning: default initialization of "Bf" may modify "V" [enabled by␣
↪default]
p.adb:7:14: warning: use pragma Import for "Bf" to suppress initialization (RM B.
↪1(24)) [enabled by default]
Runtime output
V = 10
Byte = 0
Byte = 0
(continues on next page)
3 package body P is
4
16 end P;
3 with P; use P;
4
5 procedure Main is
6 V : Integer := 10;
7 begin
8 Put_Line ("V = " & Integer'Image (V));
9 Display_Bytes_Increment (V);
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Overlay_Default_Init_Import
MD5: e269d9d3c06c0f6c69ead16e7d2ba70b
Runtime output
V = 10
Byte = 10
Byte = 0
Byte = 0
Byte = 0
Now incrementing...
V = 11
This unwanted side-effect of the initialization by the Default_Value aspect that we've just
seen can also happen in these cases:
• when we set a default value for components of a record type declaration,
• when we use the Default_Component_Value aspect for array types, or
• when we set use the Initialize_Scalars pragma for a package.
Again, using the Import aspect when declaring the overlay eliminates this side-effect.
We can use this pattern for objects of more complex data types like arrays or records. For
example:
[Ada]
3 procedure Int_Array_Bitfield is
4 type Bit_Field is array (Natural range <>) of Boolean with Pack;
5
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Bitfield_Int_Array_Ada
MD5: 478ba4ce4f5886566556bddb58245eb9
Runtime output
A ( 1)= 4
A ( 2)= 0
In the Ada example above, we're using the bit-field to set bit #3 of the first element of the
array (A (1)). We could set bit #4 of the second element by using the size of the data type
(in this case, Integer'Size):
[Ada]
B (Integer'Size + 3) := True;
In C, we would select the specific array position and, again, rely on bit-shifting and masking
to set that specific bit:
[C]
15 return 0;
16 }
Runtime output
a[0] = 4
a[1] = 0
Since we can use this pattern for any arbitrary data type, this allows us to easily create a
subprogram to serialize data types and, for example, transmit complex data structures as
a bitstream. For example:
[Ada]
7 end Serializer;
15 begin
16 Put ("Bits: ");
17 for E of B loop
18 Show_Bit (E);
19 end loop;
20 New_Line;
21 end Transmit;
22
23 end Serializer;
8 end My_Recs;
4 procedure Main is
5 R : Rec := (5, "abc");
6 B : Bit_Field (0 .. R'Size - 1)
7 with Address => R'Address, Import, Volatile;
8 begin
9 Transmit (B);
10 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Bitfield_Serialization_ada
MD5: 5c9c2d18bab7c78456d1d795c6334cd9
Build output
main.adb:9:14: warning: volatile actual passed by copy (RM C.6(19)) [enabled by␣
↪default]
Runtime output
Bits: 1010000000000000000000000000000010000110010001101100011000000000
In this example, the Transmit procedure from Serializer package displays the individual
bits of a bit-field. We could have used this strategy to actually transmit the information as
a bitstream. In the main application, we call Transmit for the object R of record type Rec.
Since Transmit has the bit-field type as a parameter, we can use it for any type, as long
as we have a corresponding bit-field representation.
In C, we interpret the input pointer as an array of bytes, and then use shifting and masking
to access the bits of that byte. Here, we use the char type because it has a size of one byte
in most platforms.
[C]
3 #include <stdio.h>
4 #include <assert.h>
5
11 assert(sizeof(char) == 1);
12
13 printf("Bits: ");
14 for (i = 0; i < len / (sizeof(char) * 8); i++)
15 {
16 for (j = 0; j < sizeof(char) * 8; j++)
17 {
18 printf("%d", c[i] >> j & 1);
19 }
20 }
21 printf("\n");
22 }
3 #include "my_recs.h"
4 #include "serializer.h"
5
12 return 0;
13 }
Runtime output
Bits: 1010000000000000000000000000000010000110010001101100011000000000
Similarly, we can write a subprogram that converts a bit-field — which may have been
received as a bitstream — to a specific type. We can add a To_Rec subprogram to the
My_Recs package to convert a bit-field to the Rec type. This can be used to convert a
bitstream that we received into the actual data type representation.
As you know, we may write the To_Rec subprogram as a procedure or as a function. Since
we need to use slightly different strategies for the implementation, the following example
has both versions of To_Rec.
This is the updated code for the My_Recs package and the Main procedure:
[Ada]
7 end Serializer;
15 begin
16 Put ("Bits: ");
17 for E of B loop
18 Show_Bit (E);
19 end loop;
20 New_Line;
21 end Transmit;
22
23 end Serializer;
3 package My_Recs is
4
17 end My_Recs;
22 return R;
23 end To_Rec;
24
31 end My_Recs;
5 procedure Main is
6 R1 : Rec := (5, "abc");
7 R2 : Rec := (0, "zzz");
8
9 B1 : Bit_Field (0 .. R1'Size - 1)
10 with Address => R1'Address, Import, Volatile;
11 begin
12 Put ("R2 = ");
13 Display (R2);
14 New_Line;
15
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Bitfield_Deserialization_Ada
MD5: bf5cb5ef048ed1f95dba8e85275f6e32
Build output
main.adb:18:12: warning: volatile actual passed by copy (RM C.6(19)) [enabled by␣
↪default]
Runtime output
R2 = ( 0, zzz)
New bitstream received!
R2 = ( 5, abc)
In both versions of To_Rec, we declare the record object B_R as an overlay of the input
bit-field. In the procedure version of To_Rec, we then simply copy the data from B_R to the
output parameter R. In the function version of To_Rec, however, we need to declare a local
record object R, which we return after the assignment.
In C, we can interpret the input pointer as an array of bytes, and copy the individual bytes.
For example:
[C]
3 #include <stdio.h>
4 #include <assert.h>
5
9 printf("r2 = ");
10 display_r (&r2);
11 printf("\n");
12
20 return 0;
21 }
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Bitfield_Deserialization_C
MD5: 1c0fda773b0b681d0a4e9a57cf67d997
Runtime output
r2 = {0, zzz}
New bitstream received!
r2 = {5, abc}
Here, to_r casts both pointer parameters to pointers to char to get a byte-aligned pointer.
Then, it simply copies the data byte-by-byte.
Unchecked conversions are another way of converting between unrelated data types. This
conversion is done by instantiating the generic Unchecked_Conversions function for the
types you want to convert. Let's look at a simple example:
[Ada]
4 procedure Simple_Unchecked_Conversion is
5 type State is (Off, State_1, State_2)
6 with Size => Integer'Size;
7
8 for State use (Off => 0, State_1 => 32, State_2 => 64);
9
13 I : Integer;
14 begin
15 I := As_Integer (State_2);
16 Put_Line ("I = " & Integer'Image (I));
17 end Simple_Unchecked_Conversion;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Simple_Unchecked_Conversion
MD5: 1b6058ef1919879a7d2d86be41f3b269
Runtime output
I = 64
3 procedure Simple_Overlay is
4 type State is (Off, State_1, State_2)
5 with Size => Integer'Size;
6
7 for State use (Off => 0, State_1 => 32, State_2 => 64);
8
9 S : State;
10 I : Integer
11 with Address => S'Address, Import, Volatile;
12 begin
13 S := State_2;
14 Put_Line ("I = " & Integer'Image (I));
15 end Simple_Overlay;
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Simple_Overlay
MD5: 932135a47c36c406e70b22e075afeaf2
Runtime output
I = 64
Let's look at another example of converting between different numeric formats. In this
case, we want to convert between a 16-bit fixed-point and a 16-bit integer data type. This
is how we can do it using Unchecked_Conversion:
[Ada]
4 procedure Fixed_Int_Unchecked_Conversion is
5 Delta_16 : constant := 1.0 / 2.0 ** (16 - 1);
6 Max_16 : constant := 2 ** 15;
7
18 I : Int_16 := 0;
19 F : Fixed_16 := 0.0;
20 begin
21 F := Fixed_16'Last;
22 I := As_Int_16 (F);
23
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Fixed_Int_Unchecked_Conversion
MD5: 53b59ca56a5c25408d8b6e5fcb06f37a
Runtime output
F = 0.99997
I = 32767
Here, we instantiate Unchecked_Conversion for the Int_16 and Fixed_16 types, and we
call the instantiated functions explicitly. In this case, we call As_Int_16 to get the integer
value corresponding to Fixed_16'Last.
This is how we can rewrite the implementation above using overlays:
[Ada]
3 procedure Fixed_Int_Overlay is
4 Delta_16 : constant := 1.0 / 2.0 ** (16 - 1);
5 Max_16 : constant := 2 ** 15;
6
12 I : Int_16 := 0;
13 F : Fixed_16
14 with Address => I'Address, Import, Volatile;
15 begin
16 F := Fixed_16'Last;
17
Project: Courses.Ada_For_Embedded_C_Dev.Translation.Fixed_Int_Overlay
MD5: ee86e3d10266f8c8c96311595b6624ec
Runtime output
F = 0.99997
I = 32767
Here, the conversion to the integer value is implicit, so we don't need to call a conversion
function.
Using Unchecked_Conversion has the advantage of making it clear that a conversion is
happening, since the conversion is written explicitly in the code. With overlays, that con-
version is automatic and therefore implicit. In that sense, using an unchecked conversion
is a cleaner and safer approach. On the other hand, an unchecked conversion requires
a copy, so it's less efficient than overlays, where no copy is performed — because one
change in the source object is automatically reflected in the target object (and vice-versa).
In the end, the choice between unchecked conversions and overlays depends on the level
of performance that you want to achieve.
Also note that an unchecked conversion only has defined behavior when instantiated for
constrained types. For example, we shouldn't use this kind of conversion:
Ada.Unchecked_Conversion (Source => String,
Target => Integer);
Although this compiles, the behavior will only be well-defined in those cases when
Source'Size = Target'Size. Therefore, instead of using an unconstrained type for
Source, we should use a subtype that matches this expectation:
subtype Integer_String is String (1 .. Integer'Size / Character'Size);
Similarly, in order to rewrite the examples using bit-fields that we've seen in the previous
section, we cannot simply instantiate Unchecked_Conversion with the Target indicating
the unconstrained bit-field, such as:
Ada.Unchecked_Conversion (Source => Integer,
Target => Bit_Field);
Instead, we have to declare a subtype for the specific range we're interested in. This is how
we can rewrite one of the previous examples:
[Ada]
4 procedure Simple_Bitfield_Conversion is
5 type Bit_Field is array (Natural range <>) of Boolean with Pack;
6
7 V : Integer := 4;
8
23 B : Integer_Bit_Field;
24 begin
25 B := As_Bit_Field (V);
26
Runtime output
V = 4
In this example, we first declare the subtype Integer_Bit_Field as a bit-field with a length
that fits the V variable we want to convert to. Then, we can use that subtype in the instan-
tiation of Unchecked_Conversion.
SEVENTYTWO
72.2.1 Genericity
One usage of C macros involves the creation of functions that works regardless of the type
they're being called upon. For example, a swap macro may look like:
[C]
Listing 1: main.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
1507
Learning Ada
10 int main()
11 {
12 int a = 10;
13 int b = 42;
14
21 return 0;
22 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Swap_C
MD5: 96d0e8ce9ae985e4de9ed64a0f0961f5
Runtime output
a = 10, b = 42
a = 42, b = 10
Ada offers a way to declare this kind of functions as a generic, that is, a function that is
written after static arguments, such as a parameter:
[Ada]
Listing 2: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4
5 generic
6 type A_Type is private;
7 procedure Swap (Left, Right : in out A_Type);
8
18 A : Integer := 10;
19 B : Integer := 42;
20
21 begin
22 Put_Line ("A = "
23 & Integer'Image (A)
24 & ", B = "
25 & Integer'Image (B));
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Swap_Ada
MD5: 13f3527b4e3258ebd43be827ad0fcd14
Runtime output
A = 10, B = 42
A = 42, B = 10
There are a few key differences between the C and the Ada version here. In C, the macro
can be used directly and essentially get expanded by the preprocessor without any kind of
checks. In Ada, the generic will first be checked for internal consistency. It then needs to be
explicitly instantiated for a concrete type. From there, it's exactly as if there was an actual
version of this Swap function, which is going to be called as any other function. All rules for
parameter modes and control will apply to this instance.
In many respects, an Ada generic is a way to provide a safe specification and implementa-
tion of such macros, through both the validation of the generic itself and its usage.
Subprograms aren't the only entities that can me made generic. As a matter of fact, it's
much more common to render an entire package generic. In this case the instantiation
creates a new version of all the entities present in the generic, including global variables.
For example:
[Ada]
Listing 3: gen.ads
1 generic
2 type T is private;
3 package Gen is
4 type C is tagged record
5 V : T;
6 end record;
7
8 G : Integer;
9 end Gen;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Gen_Pkg_1
MD5: 721f9954561b7e0d2964ba0d226c748b
Listing 4: main.adb
1 with Gen;
2
3 procedure Main is
4 package I1 is new Gen (Integer);
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Gen_Pkg_1
MD5: ab0e99dedf40fff1bced048a96a0fbb6
Listing 5: sort.ads
1 generic
2 type Component is private;
3 type Index is (<>);
4 with function "<" (Left, Right : Component) return Boolean;
5 type Array_Type is array (Index range <>) of Component;
6 procedure Sort (A : in out Array_Type);
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Gen_Pkg_2
MD5: 5781f53f4fd4453ecc1313d05ab76f81
The declaration above states that we need a type (Component), a discrete type (Index),
a comparison subprogram ("<"), and an array definition (Array_Type). Given these, it's
possible to write an algorithm that can sort any Array_Type. Note the usage of the with
reserved word in front of the function name: it exists to differentiate between the generic
parameter and the beginning of the generic subprogram.
Here is a non-exhaustive overview of the kind of constraints that can be put on types:
For a more complete list please reference the Generic Formal Types in the Appendix of the
Introduction to Ada course (page 269).
Let's take a case where a codebase needs to handle small variations of a given device, or
maybe different generations of a device, depending on the platform it's running on. In this
example, we're assuming that each platform will lead to a different binary, so the code can
statically resolve which set of services are available. However, we want an easy way to
implement a new device based on a previous one, saying "this new device is the same as
this previous device, with these new services and these changes in existing services".
We can implement such patterns using Ada's simple derivation — as opposed to tagged
derivation, which is OOP-related and discussed in a later section.
Let's start from the following example:
[Ada]
Listing 6: drivers_1.ads
1 package Drivers_1 is
2
9 end Drivers_1;
Listing 7: drivers_1.adb
1 package body Drivers_1 is
2
17 end Drivers_1;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 4f9d7e29b64cda8664438a1d7eed9049
In the above example, Device_1 is an empty record type. It may also have some fields if
required, or be a different type such as a scalar. Then the four procedures Startup, Send,
Send_Fast and Receive are primitives of this type. A primitive is essentially a subprogram
that has a parameter or return type directly referencing this type and declared in the same
scope. At this stage, there's nothing special with this type: we're using it as we would use
any other type. For example:
[Ada]
Listing 8: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Drivers_1; use Drivers_1;
3
4 procedure Main is
5 D : Device_1;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 1b28f2c8ca92498cbcda582f092b9912
Runtime output
42
Let's now assume that we need to implement a new generation of device, Device_2. This
new device works exactly like the first one, except for the startup code that has to be
done differently. We can create a new type that operates exactly like the previous one, but
modifies only the behavior of Startup:
[Ada]
Listing 9: drivers_2.ads
1 with Drivers_1; use Drivers_1;
2
3 package Drivers_2 is
4
7 overriding
8 procedure Startup (Device : Device_2);
9
10 end Drivers_2;
3 overriding
4 procedure Startup (Device : Device_2) is null;
5
6 end Drivers_2;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 276c9da0b7c9ad61d679531e16fdd9cb
Here, Device_2 is derived from Device_1. It contains all the exact same properties and
primitives, in particular, Startup, Send, Send_Fast and Receive. However, here, we de-
cided to change the Startup function and to provide a different implementation. We over-
ride this function. The main subprogram doesn't change much, except for the fact that it
now relies on a different type:
[Ada]
4 procedure Main is
5 D : Device_2;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 31e7105a99771ce6c1602af117e2e8a6
Runtime output
42
We can continue with this approach and introduce a new generation of devices. This new
device doesn't implement the Send_Fast service so we want to remove it from the list
of available services. Furthermore, for the purpose of our example, let's assume that the
hardware team went back to the Device_1 way of implementing Startup. We can write
this new device the following way:
[Ada]
3 package Drivers_3 is
4
7 overriding
8 procedure Startup (Device : Device_3);
9
13 end Drivers_3;
3 overriding
4 procedure Startup (Device : Device_3) is null;
5
6 end Drivers_3;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 779579532c81b672d8a641c0b8594ed5
The is abstract definition makes illegal any call to a function, so calls to Send_Fast on
Device_3 will be flagged as being illegal. To then implement Startup of Device_3 as being
the same as the Startup of Device_1, we can convert the type in the implementation:
[Ada]
3 overriding
4 procedure Startup (Device : Device_3) is
5 begin
6 Drivers_1.Startup (Device_1 (Device));
7 end Startup;
8
9 end Drivers_3;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 5db9596c276a7a4521914f4108f61d28
4 procedure Main is
5 D : Device_3;
6 I : Integer;
7 begin
8 Startup (D);
9 Send_Fast (D, 999);
10 Receive (D, I);
11 Put_Line (Integer'Image (I));
12 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: 8b6af16d21c2f8a1f0e4866e6ddffd1f
Build output
[Ada]
7 end Drivers_1;
11 end Drivers_1;
3 package Drivers_2 is
4
9 end Drivers_2;
11 end Drivers_2;
4 procedure Main is
5 D : Transceiver;
6 I : Integer;
7 begin
8 Send (D, 999);
9 Receive (D, I);
10 Put_Line (Integer'Image (I));
11 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Derived_Drivers
MD5: e92590e4b91fef33f4fec23362a52873
Runtime output
42
In the above example, the whole code can rely on drivers.ads, instead of relying on the
specific driver. Here, Drivers is another name for Driver_1. In order to switch to Driver_2,
the project only has to replace that one drivers.ads file.
In the following section, we'll go one step further and demonstrate that this selection can
be done through a configuration switch selected at build time instead of a manual code
modification.
Configuration pragmas are a set of pragmas that modify the compilation of source-code
files. You may use them to either relax or strengthen requirements. For example:
In this example, we're suppressing the overflow check, thereby relaxing a requirement.
Normally, the following program would raise a constraint error due to a failed overflow
check:
[Ada]
4 procedure Main is
5 I : Integer := Integer'Last;
6 begin
7 I := Add_Max (I);
8 Put_Line ("I = " & Integer'Image (I));
9 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Constraint_Error_Detection
MD5: d6960fe8ae2af1d66b617bb92d3d47b6
Runtime output
When suppressing the overflow check, however, the program doesn't raise an exception,
and the value that Add_Max returns is -2, which is a wraparound of the sum of the maximum
integer values (Integer'Last + Integer'Last).
We could also strengthen requirements, as in this example:
Here, the restriction forbids the use of floating-point types and objects. The following pro-
gram would violate this restriction, so the compiler isn't able to compile the program when
the restriction is used:
procedure Main is
F : Float := 0.0;
-- Declaration is not possible with No_Floating_Point restriction.
begin
null;
end Main;
Restrictions are especially useful for high-integrity applications. In fact, the Ada Reference
Manual has a separate section for them333 .
When creating a project, it is practical to list all configuration pragmas in a separate file.
This is called a configuration pragma file, and it usually has an .adc file extension. If you use
GPRbuild for building Ada applications, you can specify the configuration pragma file in the
corresponding project file. For example, here we indicate that gnat.adc is the configuration
pragma file for our project:
project Default is
package Compiler is
for Local_Configuration_Pragmas use "gnat.adc";
end Compiler;
(continues on next page)
333 http://www.ada-auth.org/standards/12rm/html/RM-H-4.html
end Default;
In C, preprocessing flags are used to create blocks of code that are only compiled under
certain circumstances. For example, we could have a block that is only used for debugging:
[C]
4 int func(int x)
5 {
6 return x % 4;
7 }
8
9 int main()
10 {
11 int a, b;
12
13 a = 10;
14 b = func(a);
15
16 #ifdef DEBUG
17 printf("func(%d) => %d\n", a, b);
18 #endif
19
20 return 0;
21 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Debug_Code_C
MD5: 4daa8123f7112e7487ab54f16f80d34b
Here, the block indicated by the DEBUG flag is only included in the build if we define this
preprocessing flag, which is what we expect for a debug version of the build. In the release
version, however, we want to keep debug information out of the build, so we don't use this
flag during the build process.
Ada doesn't define a preprocessor as part of the language. Some Ada toolchains — like the
GNAT toolchain — do have a preprocessor that could create code similar to the one we've
just seen. When programming in Ada, however, the recommendation is to use configuration
packages to select code blocks that are meant to be included in the application.
When using a configuration package, the example above can be written as:
[Ada]
5 end Config;
5 procedure Main is
6 A, B : Integer;
7 begin
8 A := 10;
9 B := Func (A);
10
11 if Config.Debug then
12 Put_Line ("Func(" & Integer'Image (A) & ") => "
13 & Integer'Image (B));
14 end if;
15 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Debug_Code_Ada
MD5: b643b683098fa7ad5568a69c9f2c000f
In this example, Config is a configuration package. The version of Config we're seeing
here is the release version. The debug version of the Config package looks like this:
package Config is
end Config;
The compiler makes sure to remove dead code. In the case of the release version, since
Config.Debug is constant and set to False, the compiler is smart enough to remove the
call to Put_Line from the build.
As you can see, both versions of Config are very similar to each other. The general idea is
to create packages that declare the same constants, but using different values.
In C, we differentiate between the debug and release versions by selecting the appropriate
preprocessing flags, but in Ada, we select the appropriate configuration package during the
build process. Since the file name is usually the same (config.ads for the example above),
we may want to store them in distinct directories. For the example above, we could have:
• src/debug/config.ads for the debug version, and
• src/release/config.ads for the release version.
Then, we simply select the appropriate configuration package for each version of the build
by indicating the correct path to it. When using GPRbuild, we can select the appropriate
directory where the config.ads file is located. We can use scenario variables in our project,
which allow for creating different versions of a build. For example:
project Default is
end Default;
In this example, we're defining a scenario type called Mode_Type. Then, we're declaring
the scenario variable Mode and using it in the Source_Dirs declaration to complete the
path to the subdirectory containing the config.ads file. The expression "src/" & Mode
concatenates the user-specified mode to select the appropriate subdirectory.
We can then set the mode on the command-line. For example:
In addition to selecting code blocks for the build, we could also specify values that depend
on the target build. For our example above, we may want to create two versions of the
application, each one having a different version of a MOD_VALUE that is used in the imple-
mentation of func(). In C, we can achieve this by using preprocessing flags and defining
the corresponding version in APP_VERSION. Then, depending on the value of APP_VERSION,
we define the corresponding value of MOD_VALUE.
[C]
5 #if APP_VERSION == 1
6 #define MOD_VALUE 4
7 #endif
8
9 #if APP_VERSION == 2
10 #define MOD_VALUE 5
11 #endif
4 #include "defs.h"
5
6 int func(int x)
7 {
8 return x % MOD_VALUE;
9 }
10
11 int main()
12 {
13 int a, b;
(continues on next page)
15 a = 10;
16 b = func(a);
17
18 return 0;
19 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.App_Version_C
MD5: 9f204dcc65b70618324c48be0dbdffbe
If not defined outside, the code above will compile version #1 of the application. We can
change this by specifying a value for APP_VERSION during the build (e.g. as a Makefile
switch).
For the Ada version of this code, we can create two configuration packages for each version
of the application. For example:
[Ada]
3 package App_Defs is
4
7 end App_Defs;
3 procedure Main is
4 A, B : Integer;
5 begin
6 A := 10;
7 B := Func (A);
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.App_Version_Ada
MD5: 7c8e4280e74c04ab51073b25e8f53995
The code above shows the version #1 of the configuration package. The corresponding
-- ./src/app_2/app_defs.ads
package App_Defs is
end App_Defs;
Again, we just need to select the appropriate configuration package for each version of the
build, which we can easily do when using GPRbuild.
In basic terms, records with discriminants are records that include "parameters" in their
type definitions. This allows for adding more flexibility to the type definition. In the section
about pointers (page 1483), we've seen this example:
[Ada]
8 V : S (9);
9 begin
10 null;
11 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Rec_Disc_Ada
MD5: 02fa8fa7832a262b99aee139a1b5b7a6
Build output
main.adb:8:04: warning: variable "V" is never read and never assigned [-gnatwv]
Here, Last is the discriminant for type S. When declaring the variable V as S (9), we
specify the actual index of the last position of the array component A by setting the Last
discriminant to 9.
We can create an equivalent implementation in C by declaring a struct with a pointer to
an array:
[C]
4 typedef struct {
5 int * a;
6 const int last;
7 } S;
8
19 return 0;
20 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Rec_Disc_C
MD5: 8f8b53c38c2ef8c1624208a2d8fd13ef
Here, we need to explicitly allocate the a array of the S struct via a call to malloc(), which
allocates memory space on the heap. In the Ada version, in contrast, the array (V.A) is
allocated on the stack and we don't need to explicitly allocate it.
Note that the information that we provide as the discriminant to the record type (in the Ada
code) is constant, so we cannot assign a value to it. For example, we cannot write:
[Ada]
In the C version, we declare the last field constant to get the same behavior.
[C]
Note that the information provided as discriminants is visible. In the example above, we
could display Last by writing:
[Ada]
Also note that, even if a type is private, we can still access the information of the discrimi-
nants if they are visible in the public part of the type declaration. Let's rewrite the example
above:
[Ada]
6 private
(continues on next page)
11 end Array_Definition;
4 procedure Main is
5 V : S (9);
6 begin
7 Put_Line ("Last : " & Integer'Image (V.Last));
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Rec_Disc_Ada_Private
MD5: fa0158c3c61dd9ec7e4000416672f9e9
Build output
Runtime output
Last : 9
Even though the S type is now private, we can still display Last because this discriminant
is visible in the non-private part of package Array_Definition.
In simple terms, a variant record is a record with discriminants that allows for changing its
structure. Basically, it's a record containing a case. This is the general structure:
[Ada]
case V is
when Opt_1 => F1 : Type_1;
when Opt_2 => F2 : Type_2;
end case;
end record;
3 procedure Main is
4
24 begin
25 Display (F);
26 Display (I);
27 end Main;
Runtime output
Float value: 1.00000E+01
Integer value: 9
4 typedef struct {
5 int use_float;
6 union {
7 float f;
8 int i;
9 };
10 } float_int;
11
16 v.use_float = 1;
17 v.f = f;
18 return v;
(continues on next page)
25 v.use_float = 0;
26 v.i = i;
27 return v;
28 }
29
45 display (f);
46 display (i);
47
48 return 0;
49 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Var_Rec_C
MD5: ac0ad1e6ff7f2154e9dbb6838999a62e
Runtime output
Similar to the Ada code, we declare f containing a floating-point value, and i containing an
integer value. One difference is that we use the init_float() and init_int() functions
to initialize the float_int struct. These functions initialize the correct field of the union
and set the use_float field accordingly.
There is, however, a difference in accessibility between variant records in Ada and unions
in C. In C, we're allowed to access any field of the union regardless of the initialization:
[C]
This feature is useful to create overlays. In this specific example, however, the information
displayed to the user doesn't make sense, since the union was initialized with a floating-
point value (v.f) and, by accessing the integer field (v.i), we're displaying it as if it was
an integer value.
In Ada, accessing the wrong component would raise an exception at run-time ("discriminant
check failed"), since the component is checked before being accessed:
[Ada]
Using this method prevents wrong information being used in other parts of the program.
To get the same behavior in Ada as we do in C, we need to explicitly use the
Unchecked_Union aspect in the type declaration. This is the modified example:
[Ada]
3 procedure Main is
4
15 begin
16 Put_Line ("Integer value: " & Integer'Image (V.I));
17 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Unchecked_Union_Ada
MD5: f6c5eacbd96c23531d02bb47a9668ac5
Runtime output
Now, we can display the integer component (V.I) even though we initialized the floating-
point component (V.F). As expected, the information displayed by the test application in
this case doesn't make sense.
Note that, when using the Unchecked_Union aspect in the declaration of a variant record,
the reference discriminant is not available anymore, since it isn't stored as part of the
record. Therefore, we cannot access the Use_Float discriminant as in the following code:
[Ada]
Unchecked unions are particularly useful in Ada when creating bindings for C code.
Optional components
We can also use variant records to specify optional components of a record. For example:
[Ada]
3 procedure Main is
4 type Arr is array (Integer range <>) of Integer;
5
11 case Has_Extra_Info is
12 when No => null;
13 when Yes => B : Arr (0 .. Last);
14 end case;
15 end record;
16
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Var_Rec_Null_Ada
MD5: 548235fa8458302ba025c8fa49e61777
Build output
Runtime output
Here, in the declaration of S_Var, we don't have any component in case Has_Extra_Info
is false. The component is simply set to null in this case.
When running the example above, we see that the size of V1 is greater than the size of V2
due to the extra B component — which is only included when Has_Extra_Info is true.
We can use optional components to prevent subprograms from generating invalid informa-
tion that could be misused by the caller. Consider the following example:
[C]
40 return 0;
41 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Non_Opt_C
MD5: 56f8a72782c4a54d8a6026aa39ce421a
Runtime output
Calculation error!
Value = 0.500000
In this code, we're using the output parameter success of the calculate() function to
indicate whether the calculation was successful or not. This approach has a major problem:
there's no way to prevent that the invalid value returned by calculate() in case of an error
is misused in another computation. For example:
[C]
return 0;
}
We cannot prevent access to the returned value or, at least, force the caller to evaluate
success before using the returned value.
This is the corresponding code in Ada:
[Ada]
3 procedure Main is
4
26 F : Float;
27 Success : Boolean;
28 begin
29 F := Calculate (1.0, 0.5, Success);
30 Display (F, Success);
31
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Non_Opt_Ada
MD5: bb27fd31660ad604487f908934a3d3cb
Runtime output
Calculation error!
Value = 5.00000E-01
The Ada code above suffers from the same drawbacks as the C code. Again, there's no way
to prevent misuse of the invalid value returned by Calculate in case of errors.
However, in Ada, we can use variant records to make the component unavailable and there-
fore prevent misuse of this information. Let's rewrite the original example and wrap the
returned value in a variant record:
[Ada]
3 procedure Main is
4
30 begin
31 Display (Calculate (1.0, 0.5));
32 Display (Calculate (0.5, 1.0));
33 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Opt_Ada
MD5: 8b70cd16d5ff13611567fa71059d6891
Runtime output
Calculation error!
Value = 5.00000E-01
In this example, we can determine whether the calculation was successful or not by eval-
uating the Success component of the Opt_Float. If the calculation wasn't successful, we
won't be able to access the F component of the Opt_Float. As mentioned before, trying to
access the component in this case would raise an exception. Therefore, in case of errors,
we can ensure that no information is misused after the call to Calculate.
In the previous section (page 1522), we've seen that we can add variability to records by
using discriminants. Another approach is to use tagged records, which are the base for
object-oriented programming in Ada.
Type extension
A tagged record type is declared by adding the tagged keyword. For example:
[Ada]
11 R1 : Rec;
12 R2 : Tagged_Rec;
13
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Tagged_Type_Decl
MD5: 53810d3bb5aa7e7b1483270d974eb025
In this simple example, there isn't much difference between the Rec and Tagged_Rec type.
However, tagged types can be derived and extended. For example:
[Ada]
23 R1 : Rec;
24 R2 : Tagged_Rec;
25 R3 : Ext_Tagged_Rec;
26
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Tagged_Type_Extension_Decl
MD5: 707a3e6b220357f50f6792190b000c91
As indicated in the example, a type derived from an untagged type cannot have an exten-
sion. The compiler indicates this error if you uncomment the declaration of the Ext_Rec
type above. In contrast, we can extend a tagged type, as we did in the declaration of
Ext_Tagged_Rec. In this case, Ext_Tagged_Rec has all the components of the Tagged_Rec
type (V, in this case) plus the additional components from its own type declaration (V2, in
this case).
Overriding subprograms
Previously, we've seen that subprograms can be overriden. For example, if we had im-
plemented a Reset and a Display procedure for the Rec type that we declared above,
these procedures would be available for an Ext_Rec type derived from Rec. Also, we could
override these procedures for the Ext_Rec type. In Ada, we don't need object-oriented
programming features to do that: simple (untagged) records can be used to derive types,
inherit operations and override them. However, in applications where the actual subpro-
gram to be called is determined dynamically at run-time, we need dispatching calls. In this
case, we must use tagged types to implement this.
Let's discuss the similarities and differences between untagged and tagged types based on
this example:
[Ada]
30 end P;
3 package body P is
4
62 end P;
4 procedure Main is
5 X_Rec : Rec;
6 X_New_Rec : New_Rec;
7
22 --
23 -- Use new operations when available
24 --
25 New_Op (X_New_Rec);
26 X_Ext_Tagged_Rec.New_Op;
27
28 --
29 -- Display all objects
30 --
31 Display (X_Rec);
32 Display (X_New_Rec);
33 X_Tagged_Rec.Display; -- we could write "Display (X_Tagged_Rec)" as well
34 X_Ext_Tagged_Rec.Display;
35
36 --
37 -- Resetting and display objects of Tagged_Rec'Class
38 --
39 Put_Line ("Operations on Tagged_Rec'Class");
40 Put_Line ("------------------------------");
41 for E of X_Tagged_Rec_Array loop
42 E.Reset;
43 E.Display;
44 end loop;
45 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Tagged_Type_Extension_Decl
MD5: 29412b74db6680f0a0986b62e5284cf7
Runtime output
TYPE: REC
Rec.V = 0
TYPE: NEW_REC
New_Rec.V = 1
TYPE: P.TAGGED_REC
Tagged_Rec.V = 0
TYPE: P.EXT_TAGGED_REC
Ext_Tagged_Rec.V = 1
Ext_Tagged_Rec.V2 = 0
Operations on Tagged_Rec'Class
------------------------------
TYPE: P.TAGGED_REC
Tagged_Rec.V = 0
TYPE: P.EXT_TAGGED_REC
Ext_Tagged_Rec.V = 0
Ext_Tagged_Rec.V2 = 0
Dispatching calls
Let's look more closely at the dispatching calls implemented above. First, we declare the
X_Tagged_Rec_Array array and initialize it with the access to objects of both parent and
derived tagged types:
[Ada]
Here, we use the aliased keyword to be able to get access to the objects (via the 'Access
attribute).
Then, we loop over this array and call the Reset and Display procedures:
[Ada]
Since we're using dispatching calls, the actual procedure that is selected depends on the
type of the object. For the first element (X_Tagged_Rec_Array (1)), this is Tagged_Rec,
while for the second element (X_Tagged_Rec_Array (2)), this is Ext_Tagged_Rec.
Dispatching calls are only possible for a type class — for example, the Tagged_Rec'Class.
When the type of an object is known at compile time, the calls won't dispatch at
runtime. For example, the call to the Reset procedure of the X_Ext_Tagged_Rec
object (X_Ext_Tagged_Rec.Reset) will always take the overriden Reset procedure of
the Ext_Tagged_Rec type. Similarly, if we perform a view conversion by writing
Tagged_Rec (A_Ext_Tagged_Rec).Display, we're instructing the compiler to interpret
A_Ext_Tagged_Rec as an object of type Tagged_Rec, so that the compiler selects the Dis-
play procedure of the Tagged_Rec type.
Interfaces
Another useful feature of object-oriented programming is the use of interfaces. In this case,
we can define abstract operations, and implement them in the derived tagged types. We
declare an interface by simply writing type T is interface. For example:
[Ada]
All operations on an interface type are abstract, so we need to write is abstract in the
signature — as we did in the declaration of Op above. Also, since interfaces are abstract
types and don't have an actual implementation, we cannot declare objects for it.
We can derive tagged types from an interface and implement the actual operations of that
interface:
[Ada]
Note that we're not using the tagged keyword in the declaration because any type derived
from an interface is automatically tagged.
Let's look at an example with an interface and two derived tagged types:
[Ada]
12 end P;
3 package body P is
4
17 end P;
3 procedure Main is
4 D_Small : Small_Display_Type;
5 D_Big : Big_Display_Type;
6
12 begin
13 Dispatching_Display (D_Small);
14 Dispatching_Display (D_Big);
15 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Interfaces_1
MD5: 564eba158b2f8fc3efea9e892a21caa9
Runtime output
Using Small_Display_Type
Using Big_Display_Type
In this example, we have an interface type Display_Interface and two tagged types that
are derived from Display_Interface: Small_Display_Type and Big_Display_Type.
Both types (Small_Display_Type and Big_Display_Type) implement the interface by
overriding the Display procedure. Then, in the inner procedure Dispatching_Display
of the Main procedure, we perform a dispatching call depending on the actual type of D.
We may derive a type from multiple interfaces by simply writing type Derived_T is new
T1 and T2 with null record. For example:
[Ada]
17 end Transceivers;
17 end Transceivers;
3 procedure Main is
4 D : Transceiver;
5 begin
6 D.Send;
7 D.Receive;
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Multiple_Interfaces
MD5: c81813941bd3458eaf7b1fd39b010a03
Runtime output
Sending data...
Receiving data...
We may also declare abstract tagged types. Note that, because the type is abstract, we
cannot use it to declare objects for it — this is the same as for interfaces. We can only
use it to derive other types. Let's look at the abstract tagged type declared in the Ab-
stract_Transceivers package:
[Ada]
3 package Abstract_Transceivers is
4
11 end Abstract_Transceivers;
11 end Abstract_Transceivers;
3 procedure Main is
4 D : Abstract_Transceiver;
5 begin
6 D.Send;
7 D.Receive;
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Multiple_Interfaces
MD5: c2b0b3aab1ffc9c3b9a0749bf6721088
Build output
In this example, we declare the abstract tagged type Abstract_Transceiver. Here, we're
only partially implementing the interfaces from which this type is derived: we're imple-
menting Send, but we're skipping the implementation of Receive. Therefore, Receive is
an abstract operation of Abstract_Transceiver. Since any tagged type that has abstract
operations is abstract, we must indicate this by adding the abstract keyword in type dec-
laration.
Also, when compiling this example, we get an error because we're trying to declare an object
of Abstract_Transceiver (in the Main procedure), which is not possible. Naturally, if we
derive another type from Abstract_Transceiver and implement Receive as well, then we
can declare objects of this derived type. This is what we do in the Full_Transceivers
below:
[Ada]
3 package Full_Transceivers is
4
8 end Full_Transceivers;
11 end Full_Transceivers;
3 procedure Main is
4 D : Full_Transceiver;
5 begin
6 D.Send;
7 D.Receive;
8 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Multiple_Interfaces
MD5: 77a86a6d917547d306a89422e7522111
Runtime output
Sending data...
Receiving data...
Here, we implement the Receive procedure for the Full_Transceiver. Therefore, the type
doesn't have any abstract operation, so we can use it to declare objects.
In the section about simple derivation (page 1511), we've seen an example where the actual
selection was done at implementation time by renaming one of the packages:
[Ada]
with Drivers_1;
Although this approach is useful in many cases, there might be situations where we need
to select the actual driver dynamically at runtime. Let's look at how we could rewrite that
example using interfaces, tagged types and dispatching calls:
[Ada]
9 end Drivers_Base;
3 package Drivers_1 is
4
11 end Drivers_1;
19 end Drivers_1;
3 package Drivers_2 is
4
11 end Drivers_2;
19 end Drivers_2;
3 with Drivers_Base;
4 with Drivers_1;
5 with Drivers_2;
6
7 procedure Main is
8 D1 : aliased Drivers_1.Transceiver;
9 D2 : aliased Drivers_2.Transceiver;
10 D : access Drivers_Base.Transceiver'Class;
11
12 I : Integer;
13
26 begin
27 Select_Driver (1);
28 D.Send (999);
29 D.Receive (I);
30 Put_Line (Integer'Image (I));
31
32 Select_Driver (2);
33 D.Send (999);
34 D.Receive (I);
35 Put_Line (Integer'Image (I));
36 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Tagged_Drivers
MD5: d823b7231f1adf003fb6f545cb482308
Runtime output
Using Drivers_1
42
Using Drivers_2
7
In this example, we declare the Transceiver interface in the Drivers_Base package. This
interface is then used to derive the tagged types Transceiver from both Drivers_1 and
Drivers_2 packages.
In the Main procedure, we use the access to Transceiver'Class — from the interface
declared in the Drivers_Base package — to declare D. This object D contains the access to
the actual driver loaded at any specific time. We select the driver at runtime in the inner
Select_Driver procedure, which initializes D (with the access to the selected driver). Then,
any operation on D triggers a dispatching call to the selected driver.
Further resources
14 int main()
15 {
16 int selection = 1;
17 void (*current_show_msg) (char *);
18
19 switch (selection)
20 {
21 case 1: current_show_msg = &show_msg_v1; break;
22 case 2: current_show_msg = &show_msg_v2; break;
23 default: current_show_msg = NULL; break;
24 }
25
26 if (current_show_msg != NULL)
27 {
28 current_show_msg ("Hello there!");
29 }
30 else
31 {
32 printf("ERROR: no version of show_msg() selected!\n");
33 }
34
35 return 0;
36 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Selecting_Subprogram_C
MD5: 414c99fca2490611d20d031f8549ff59
Runtime output
The example above contains two versions of the show_msg() function: show_msg_v1()
and show_msg_v2(). The function is selected depending on the value of selection, which
initializes the function pointer current_show_msg. If there's no corresponding value, cur-
rent_show_msg is set to null — alternatively, we could have selected a default version of
show_msg() function. By calling current_show_msg ("Hello there!"), we're calling the
function that current_show_msg is pointing to.
This is the corresponding implementation in Ada:
[Ada]
3 procedure Show_Subprogram_Selection is
4
18 Current_Show_Msg : Show_Msg_Proc;
19 Selection : Natural;
20
21 begin
22 Selection := 1;
23
24 case Selection is
25 when 1 => Current_Show_Msg := Show_Msg_V1'Access;
26 when 2 => Current_Show_Msg := Show_Msg_V2'Access;
27 when others => Current_Show_Msg := null;
28 end case;
29
36 end Show_Subprogram_Selection;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Selecting_Subprogram_Ada
MD5: ee41e042e3b879b4a2671bfe6d8072aa
Runtime output
The structure of the code above is very similar to the one used in the C code. Again, we
have two version of Show_Msg: Show_Msg_V1 and Show_Msg_V2. We set Current_Show_Msg
according to the value of Selection. Here, we use 'Access to get access to the corre-
sponding procedure. If no version of Show_Msg is available, we set Current_Show_Msg to
null.
Pointers to subprograms are also typically used as callback functions. This approach is
extensively used in systems that process events, for example. Here, we could have a two-
layered system:
• A layer of the system (an event manager) triggers events depending on information
from sensors.
– For each event, callback functions can be registered.
– The event manager calls registered callback functions when an event is triggered.
• Another layer of the system registers callback functions for specific events and decides
what to do when those events are triggered.
This approach promotes information hiding and component decoupling because:
• the layer of the system responsible for managing events doesn't need to know what
the callback function actually does, while
• the layer of the system that implements callback functions remains agnostic to imple-
mentation details of the event manager — for example, how events are implemented
in the event manager.
Let's see an example in C where we have a process_values() function that calls a callback
function (process_one) to process a list of values:
[C]
3 #include <assert.h>
4 #include <stdio.h>
5
4 #include "process_values.h"
5
11 # define LEN_VALUES 5
12
13 int main()
(continues on next page)
16 int values[LEN_VALUES] = { 1, 2, 3, 4, 5 };
17 int i;
18
26 return 0;
27 }
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Callback_C
MD5: ff5c8611d0901f40b6c4a9effeb0a323
Runtime output
Value [0] = 11
Value [1] = 12
Value [2] = 13
Value [3] = 14
Value [4] = 15
As mentioned previously, process_values() doesn't have any knowledge about what pro-
cess_one() does with the integer value it receives as a parameter. Also, we could re-
place proc_10() by another function without having to change the implementation of pro-
cess_values().
Note that process_values() calls an assert() for the function pointer to compare it
against null. Here, instead of checking the validity of the function pointer, we're expecting
the caller of process_values() to provide a valid pointer.
This is the corresponding implementation in Ada:
[Ada]
11 end Values_Processing;
11 end Values_Processing;
6 procedure Show_Callback is
7 Values : Integer_Array := (1, 2, 3, 4, 5);
8 begin
9 Process_Values (Values, Proc_10'Access);
10
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.Callback_Ada
MD5: f49c54f0d14193d305c0e962a392ab67
Runtime output
Value [ 1] = 11
Value [ 2] = 12
Value [ 3] = 13
Value [ 4] = 14
Value [ 5] = 15
In the previous sections, we have shown how to use packages to create separate com-
ponents of a system. As we know, when designing a complex system, it is advisable to
separate concerns into distinct units, so we can use Ada packages to represent each unit
of a system. In this section, we go one step further and create separate dynamic libraries
for each component, which we'll then link to the main application.
Let's suppose we have a main system (Main_System) and a component A (Component_A)
that we want to use in the main system. For example:
[Ada]
10 end Component_A;
15 end Component_A;
8 procedure Main_System is
9 Values : constant Float_Array := (10.0, 11.0, 12.0, 13.0);
10 Average_Value : Float;
11 begin
12 Average_Value := Average (Values);
13 Put_Line ("Average = " & Float'Image (Average_Value));
14 end Main_System;
Project: Courses.Ada_For_Embedded_C_Dev.Reusability.System_For_Dyn_Lib
MD5: d759132b787e636d4bcd5f8cd6393f2a
Runtime output
Average = 1.15000E+01
Note that, in the source-code example above, we're indicating the name of each file. We'll
now see how to organize those files in a structure that is suitable for the GNAT build system
(GPRbuild).
In order to discuss how to create dynamic libraries, we need to dig into some details about
the build system. With GNAT, we can use project files for GPRbuild to easily design dynamic
libraries. Let's say we use the following directory structure for the code above:
|- component_a
| | component_a.gpr
| |- src
| | | component_a.adb
| | | component_a.ads
|- main_system
| | main_system.gpr
| |- src
| | | main_system.adb
Here, we have two directories: component_a and main_system. Each directory contains a
project file (with the .gpr file extension) and a source-code directory (src).
In the source-code example above, we've seen the content of files component_a.ads,
component_a.adb and main_system.adb. Now, let's discuss how to write the project file for
Component_A (component_a.gpr), which will build the dynamic library for this component:
end Component_A;
The project is defined as a library project instead of project. This tells GPRbuild to build
a library instead of an executable binary. We then specify the library name using the Li-
brary_Name attribute, which is required, so it must appear in a library project. The next
two library-related attributes are optional, but important for our use-case. We use:
• Library_Kind to specify that we want to create a dynamic library — by default, this
attribute is set to static;
• Library_Dir to specify the directory where the library is stored.
In the project file of our main system (main_system.gpr), we just need to reference the
project of Component_A using a with clause and indicating the correct path to that project
file:
with "../component_a/component_a.gpr";
project Main_System is
for Source_Dirs use ("src");
for Object_Dir use "obj";
(continues on next page)
GPRbuild takes care of selecting the correct settings to link the dynamic library created for
Component_A with the main application (Main_System) and build an executable.
We can use the same strategy to create a Component_B and dynamically link to it in the
Main_System. We just need to create the separate structure for this component — with the
appropriate Ada packages and project file — and include it in the project file of the main
system using a with clause:
with "../component_a/component_a.gpr";
with "../component_b/component_b.gpr";
...
Again, GPRbuild takes care of selecting the correct settings to link both dynamic libraries
together with the main application.
You can find more details and special setting for library projects in the GPRbuild documen-
tation334 .
334 https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug/gnat_project_manager.html#library-projects
SEVENTYTHREE
PERFORMANCE CONSIDERATIONS
All in all, there should not be significant performance differences between code written
in Ada and code written in C, provided that they are semantically equivalent. Taking the
current GNAT implementation and its GCC C counterpart for example, most of the code
generation and optimization phases are shared between C and Ada — so there's not one
compiler more efficient than the other. Furthermore, the two languages are fairly similar
in the way they implement imperative semantics, in particular with regards to memory
management or control flow. They should be equivalent on average.
When comparing the performance of C and Ada code, differences might be observed. This
usually comes from the fact that, while the two piece appear semantically equivalent, they
happen to be actually quite different; C code semantics do not implicitly apply the same
run-time checks that Ada does. This section will present common ways for improving Ada
code performance.
Clever use of compilation switches might optimize the performance of an application sig-
nificantly. In this section, we'll briefly look into some of the switches available in the GNAT
toolchain.
Optimization levels can be found in many compilers for multiple languages. On the low-
est level, the GNAT compiler doesn't optimize the code at all, while at the higher levels,
the compiler analyses the code and optimizes it by removing unnecessary operations and
making the most use of the target processor's capabilities.
By being part of GCC, GNAT offers the same -O_ switches as GCC:
SwitchDescription
-O0 No optimization: the generated code is completely unoptimized. This is the default
optimization level.
-O1 Moderate optimization.
-O2 Full optimization.
-O3 Same optimization level as for -O2. In addition, further optimization strategies,
such as aggressive automatic inlining and vectorization.
1555
Learning Ada
Note that the higher the level, the longer the compilation time. For fast compilation dur-
ing development phase, unless you're working on benchmarking algorithms, using -O0 is
probably a good idea.
In addition to the levels presented above, GNAT also has the -Os switch, which allows for
optimizing code and data usage.
73.2.2 Inlining
As we've seen in the previous section, automatic inlining depends on the optimization level.
The highest optimization level (-O3), for example, performs aggressive automatic inlining.
This could mean that this level inlines too much rather than not enough. As a result, the
cache may become an issue and the overall performance may be worse than the one we
would achieve by compiling the same code with optimization level 2 (-O2). Therefore, the
general recommendation is to not just select -O3 for the optimized version of an application,
but instead compare it the optimized version built with -O2.
In some cases, it's better to reduce the optimization level and perform manual inlining in-
stead of automatic inlining. We do that by using the Inline aspect. Let's reuse an example
from a previous chapter and inline the Average function:
[Ada]
Listing 1: float_arrays.ads
1 package Float_Arrays is
2
8 end Float_Arrays;
Listing 2: float_arrays.adb
1 package body Float_Arrays is
2
12 end Float_Arrays;
Listing 3: compute_average.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Compute_Average is
6 Values : constant Float_Array := (10.0, 11.0, 12.0, 13.0);
7 Average_Value : Float;
8 begin
9 Average_Value := Average (Values);
(continues on next page)
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Inlining
MD5: faf9d0d8cd5aefd7a48bcd950b1256fa
Runtime output
Average = 1.15000E+01
When compiling this example, GNAT will inline Average in the Compute_Average procedure.
In order to effectively use this aspect, however, we need to set the optimization level to
at least -O1 and use the -gnatn switch, which instructs the compiler to take the Inline
aspect into account.
Note, however, that the Inline aspect is just a recommendation to the compiler. Some-
times, the compiler might not be able to follow this recommendation, so it won't inline the
subprogram. In this case, we get a compilation warning from GNAT.
These are some examples of situations where the compiler might not be able to inline a
subprogram:
• when the code is too large,
• when it's too complicated — for example, when it involves exception handling —, or
• when it contains tasks, etc.
In addition to the Inline aspect, we also have the Inline_Always aspect. In contrast to the
former aspect, however, the Inline_Always aspect isn't primarily related to performance.
Instead, it should be used when the functionality would be incorrect if inlining was not per-
formed by the compiler. Examples of this are procedures that insert Assembly instructions
that only make sense when the procedure is inlined, such as memory barriers.
Similar to the Inline aspect, there might be situations where a subprogram has the In-
line_Always aspect, but the compiler is unable to inline it. In this case, we get a compila-
tion error from GNAT.
73.3.1 Checks
Ada provides many runtime checks to ensure that the implementation is working as ex-
pected. For example, when accessing an array, we would like to make sure that we're not
accessing a memory position that is not allocated for that array. This is achieved by an
index check.
Another example of runtime check is the verification of valid ranges. For example, when
adding two integer numbers, we would like to ensure that the result is still in the valid
range — that the value is neither too large nor too small. This is achieved by an range
check. Likewise, arithmetic operations shouldn't overflow or underflow. This is achieved by
an overflow check.
Although runtime checks are very useful and should be used as much as possible, they can
also increase the overhead of implementations at certain hot-spots. For example, checking
the index of an array in a sorting algorithm may significantly decrease its performance. In
those cases, suppressing the check may be an option. We can achieve this suppression by
using pragma Suppress (Index_Check). For example:
[Ada]
We can also deactivate overflow checks for integer types using the -gnato switch when
compiling a source-code file with GNAT. In this case, overflow checks in the whole file are
deactivated.
It is also possible to suppress all checks at once using pragma Suppress (All_Checks).
In addition, GNAT offers a compilation switch called -gnatp, which has the same effect on
the whole file.
Note, however, that this kind of suppression is just a recommendation to the compiler.
There's no guarantee that the compiler will actually suppress any of the checks because
the compiler may not be able to do so — typically because the hardware happens to do it.
For example, if the machine traps on any access via address zero, requesting the removal of
null access value checks in the generated code won't prevent the checks from happening.
It is important to differentiate between required and redundant checks. Let's consider the
following example in C:
[C]
Listing 4: main.c
1 #include <stdio.h>
2
7 res = a / b;
8
12 return 0;
13 }
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Division_By_Zero_C
MD5: c8d95cbdd76618108119886c27ce7eb6
Because C doesn't have language-defined checks, as soon as the application tries to divide
a value by zero in res = a / b, it'll break — on Linux, for example, you may get the
following error message by the operating system: Floating point exception (core
dumped). Therefore, we need to manually introduce a check for zero before this operation.
For example:
[C]
Listing 5: main.c
1 #include <stdio.h>
2
7 if (b != 0) {
8 res = a / b;
9
19 return 0;
20 }
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Division_By_Zero_Check_C
MD5: 67ea0140d8248674b4aac06825c7cdbe
Runtime output
Listing 6: show_division_by_zero.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Division_By_Zero is
4 A : Integer := 8;
5 B : Integer := 0;
6 Res : Integer;
7 begin
8 Res := A / B;
9
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Division_By_Zero_Ada
MD5: 2af6690eb977203ef7ce2178d15255af
Build output
Runtime output
Similar to the first version of the C code, we're not explicitly checking for a potential division
by zero here. In Ada, however, this check is automatically inserted by the language itself.
When running the application above, an exception is raised when the application tries to
divide the value in A by zero. We could introduce exception handling in our example, so
that we get the same message as we did in the second version of the C code:
[Ada]
Listing 7: show_division_by_zero.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Show_Division_By_Zero is
4 A : Integer := 8;
5 B : Integer := 0;
6 Res : Integer;
7 begin
8 Res := A / B;
9
Build output
show_division_by_zero.adb:8:15: warning: division by zero [enabled by default]
show_division_by_zero.adb:8:15: warning: Constraint_Error will be raised at run␣
↪time [enabled by default]
Runtime output
Error: cannot calculate value (division by zero)
This example demonstrates that the division check for Res := A / B is required and
shouldn't be suppressed. In contrast, a check is redundant — and therefore not required —
when we know that the condition that leads to a failure can never happen. In many cases,
the compiler itself detects redundant checks and eliminates them (for higher optimization
levels). Therefore, when improving the performance of your application, you should:
1. keep all checks active for most parts of the application;
2. identify the hot-spots of your application;
3. identify which checks haven't been eliminated by the optimizer on these hot-spots;
4. identify which of those checks are redundant;
5. only suppress those checks that are redundant, and keep the required ones.
73.3.2 Assertions
We've already discussed assertions in this section of the SPARK chapter (page 1462). As-
sertions are user-defined checks that you can add to your code using the pragma Assert.
For example:
[Ada]
return Res;
end Sort;
Assertions that are specified with pragma Assert are not enabled by default. You can
enable them by setting the assertion policy to check — using pragma Assertion_Policy
(Check) — or by using the -gnata switch when compiling with GNAT.
Similar to the checks discussed previously, assertions can generate significant overhead
when used at hot-spots. Restricting those assertions to development (e.g. debug version)
and turning them off on the release version may be an option. In this case, formal proof —
as discussed in the SPARK chapter (page 1455) — can help you. By formally proving that
assertions will never fail at run-time, you can safely deactivate them.
Ada generally speaking provides more ways than C or C++ to write simple dynamic struc-
tures, that is to say structures that have constraints computed after variables. For example,
it's quite typical to have initial values in record types:
[Ada]
type R is record
F : Some_Field := Call_To_Some_Function;
end record;
However, the consequences of the above is that any declaration of a instance of this type
without an explicit value for F will issue a call to Call_To_Some_Function. More subtle
issue may arise with elaboration. For example, it's possible to write:
Listing 8: some_functions.ads
1 package Some_Functions is
2
7 end Some_Functions;
Listing 9: values.ads
1 with Some_Functions; use Some_Functions;
2
3 package Values is
4 A_Start : Integer := Some_Function_Call;
5 A_End : Integer := Some_Other_Function_Call;
6 end Values;
3 package Arr_Def is
4 type Arr is array (Integer range A_Start .. A_End) of Integer;
5 end Arr_Def;
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Dynamic_Array
MD5: 0c97cecb64d27e935724c8b5f941fb4f
It may indeed be appealing to be able to change the values of A_Start and A_End at startup
so as to align a series of arrays dynamically. The consequence, however, is that these values
will not be known statically, so any code that needs to access to boundaries of the array
will need to read data from memory. While it's perfectly fine most of the time, there may be
situations where performances are so critical that static values for array boundaries must
be enforced.
Here's a last case which may also be surprising:
[Ada]
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Record_With_Arrays
MD5: e7b2656433279d36db87506276b68398
In the code above, R contains two arrays, F1 and F2, respectively constrained by the dis-
criminant D1 and D2. The consequence is, however, that to access F2, the run-time needs to
know how large F1 is, which is dynamically constrained when creating an instance. There-
fore, accessing to F2 requires a computation involving D1 which is slower than, let's say,
two pointers in an C array that would point to two different arrays.
Generally speaking, when values are used in data structures, it's useful to always consider
where they're coming from, and if their value is static (computed by the compiler) or dy-
namic (only known at run-time). There's nothing fundamentally wrong with dynamically
constrained types, unless they appear in performance-critical pieces of the application.
In the section about pointers (page 1483), we mentioned that the Ada compiler will auto-
matically pass parameters by reference when needed. Let's look into what "when needed"
means. The fundamental point to understand is that the parameter types determine how
the parameters are passed in and/or out. The parameter modes do not control how param-
eters are passed.
Specifically, the language standards specifies that scalar types are always passed by value,
and that some other types are always passed by reference. It would not make sense to make
a copy of a task when passing it as a parameter, for example. So parameters that can be
passed reasonably by value will be, and those that must be passed by reference will be.
That's the safest approach.
But the language also specifies that when the parameter is an array type or a record type,
and the record/array components are all by-value types, then the compiler decides: it can
pass the parameter using either mechanism. The critical case is when such a parameter
is large, e.g., a large matrix. We don't want the compiler to pass it by value because that
would entail a large copy, and indeed the compiler will not do so. But if the array or record
parameter is small, say the same size as an address, then it doesn't matter how it is passed
and by copy is just as fast as by reference. That's why the language gives the choice to
the compiler. Although the language does not mandate that large parameters be passed
by reference, any reasonable compiler will do the right thing.
The modes do have an effect, but not in determining how the parameters are passed. Their
effect, for parameters passed by value, is to determine how many times the value is copied.
For mode in and mode out there is just one copy. For mode in out there will be two copies,
one in each direction.
Therefore, unlike C, you don't have to use access types in Ada to get better performance
when passing arrays or records to subprograms. The compiler will almost certainly do the
right thing for you.
Let's look at this example:
[C]
3 struct Data {
4 int prev, curr;
5 };
6
27 return 0;
28 }
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Passing_Rec_By_Reference_C
MD5: 9087e26168e49d095b5e0776d6330d69
Runtime output
Prev : 1
Curr : 3
In this C code example, we're using pointers to pass D1 as a reference to update and dis-
play. In contrast, the equivalent code in Ada simply uses the parameter modes to specify
the data flow directions. The mechanisms used to pass the values do not appear in the
source code.
[Ada]
3 procedure Update_Record is
4
25 begin
26 Update (D1, 3);
27 Display (D1);
28 end Update_Record;
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Passing_Rec_By_Reference_Ada
MD5: 6c64fb73e2cf490c0a129f0cd73c190b
Runtime output
Prev: 1
Curr: 3
In the calls to Update and Display, D1 is always be passed by reference. Because no extra
copy takes place, we get a performance that is equivalent to the C version. If we had used
arrays in the example above, D1 would have been passed by reference as well:
[Ada]
3 procedure Update_Array is
4
23 begin
24 Update (D1, 3);
25 Display (D1);
26 end Update_Array;
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Passing_Array_By_Reference_Ada
MD5: 5fb27811f34543fc4150eb4fddbe7034
Runtime output
Prev: 1
Curr: 3
Again, no extra copy is performed in the calls to Update and Display, which gives us optimal
performance when dealing with arrays and avoids the need to use access types to optimize
the code.
Previously, we've discussed the cost of passing complex records as arguments to subpro-
grams. We've seen that we don't have to use explicit access type parameters to get better
performance in Ada. In this section, we'll briefly discuss the cost of function returns.
In general, we can use either procedures or functions to initialize a data structure. Let's
look at this example in C:
[C]
3 struct Data {
4 int prev, curr;
5 };
6
17 return d;
18 }
19
24 D1 = get_init_data();
25
26 init_data(&D1);
27
28 return 0;
29 }
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Init_Rec_Proc_And_Func_C
MD5: 0586636d5e25c0d6bec2257af75ae998
This code example contains two subprograms that initialize the Data structure:
• init_data(), which receives the data structure as a reference (using a pointer) and
initializes it, and
• get_init_data(), which returns the initialized structure.
In C, we generally avoid implementing functions such as get_init_data() because of the
extra copy that is needed for the function return.
This is the corresponding implementation in Ada:
[Ada]
19 D1 : Data;
20
25 Init (D1);
26 end Init_Record;
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Init_Rec_Proc_And_Func_Ada
MD5: 0f930eea432a82d78840b72c0714b283
Build output
In this example, we have two versions of Init: one using a procedural form, and the other
one using a functional form. Note that, because of Ada's support for subprogram overload-
ing, we can use the same name for both subprograms.
The issue is that assignment of a function result entails a copy, just as if we assigned
one variable to another. For example, when assigning a function result to a constant, the
function result is copied into the memory for the constant. That's what is happening in the
above examples for the initialized variables.
Therefore, in terms of performance, the same recommendations apply: for large types we
should avoid writing functions like the Init function above. Instead, we should use the
procedural form of Init. The reason is that the compiler necessarily generates a copy for
the Init function, while the Init procedure uses a reference for the output parameter, so
that the actual record initialization is performed in place in the caller's argument.
An exception to this is when we use functions returning values of limited types, which by
definition do not allow assignment. Here, to avoid allowing something that would otherwise
look suspiciously like an assignment, the compiler generates the function body so that it
builds the result directly into the object being assigned. No copy takes place.
We could, for example, rewrite the example above using limited types:
[Ada]
16 D1 : Data := Init;
17
Project: Courses.Ada_For_Embedded_C_Dev.Performance.Init_Lim_Rec_Proc_And_Func_Ada
MD5: 57fc1b3f69b42dd4633b0c67e252c2d2
In this example, D1 : Data := Init; has the same cost as the call to the procedural form
— Init (D1); — that we've seen in the previous example. This is because the assignment
is done in place.
Note that limited types require the use of the extended return statements (return ... do
... end return) in function implementations. Also note that, because the Data type is
limited, we can only use the Init function in the declaration of D1; a statement in the code
such as D1 := Init; is therefore forbidden.
SEVENTYFOUR
The technical benefits of a migration from C to Ada are usually relatively straightforward
to demonstrate. Hopefully, this course provides a good basis for it. However, when faced
with an actual business decision to make, additional considerations need to be taken into
account, such as return on investment, perennity of the solution, tool support, etc. This
section will cover a number of usual questions and provide elements of answers.
Switching from one technology to another is a cost, may that be in terms of training, tran-
sition of the existing environment or acquisition of new tools. This investment needs to be
matched with an expected return on investment, or ROI, to be consistent. Of course, it's
incredibly difficult to provide a firm answer to how much money can be saved by transi-
tioning, as this is highly dependent on specific project objectives and constraints. We're
going to provide qualitative and quantitative arguments here, from the perspective of a
project that has to reach a relatively high level of integrity, that is to say a system where
the occurrence of a software failure is a relatively costly event.
From a qualitative standpoint, there are various times in the software development life cycle
where defects can be found:
1. on the developer's desk
2. during component testing
3. during integration testing
4. after deployment
5. during maintenance
Numbers from studies vary greatly on the relative costs of defects found at each of these
phases, but there's a clear ordering between them. For example, a defect found while de-
veloping is orders of magnitude less expensive to fix than a defect found e.g. at integration
time, which may involve costly debugging sessions and slow down the entire system accep-
tance. The whole purpose of Ada and SPARK is to push defect detection to the developer's
desk as much as possible; at least for all of these defects that can be identified at that
level. While the strict act of writing software may be taking more effort because of all of
the additional safeguards, this should have a significant and positive impact down the line
and help to control costs overall. The exact value this may translate into is highly business
dependent.
From a quantitative standpoint, two studies have been done almost 25 years apart and
provide similar insights:
• Rational Software in 1995 found that the cost of developing software in Ada was overall
half as much as the cost of developing software in C.
1569
Learning Ada
• VDC ran a study in 2018, finding that the cost savings of developing with Ada over C
ranged from 6% to 38% in savings.
From a qualitative standpoint, in particular with regards to Ada and C from a formal proof
perspective, an interesting presentation was made in 2017 by two researchers. They tried
to apply formal proof on the same piece of code, developed in Ada/SPARK on one end and
C/Frama-C on the other. Their results indicate that the Ada/SPARK technology is indeed
more conducive to formal proof methodologies.
Although all of these studies have their own biases, they provide a good idea of what to
expect in terms of savings once the initial investment in switching to Ada is made. This is
assuming everything else is equal, in particular that the level of integrity is the same. In
many situations, the migration to Ada is justified by an increase in terms of integrity expec-
tations, in which case it's expected that development costs will rise (it's more expensive to
develop better software) and Ada is viewed as a means to mitigate this rise in development
costs.
That being said, the point of this argument is not to say that it's not possible to write very
safe and secure software with languages different than Ada. With the right expertise, the
right processes and the right tools, it's done every day. The point is that Ada overall reduces
the level of processes, expertise and tools necessary and will allow to reach the same target
at a lower cost.
Ada was initially born as a DoD project, and thus got its initial customer base in aerospace
and defence (A&D). At the time these lines are written and from the perspective of AdaCore,
A&D is still the largest consumer of Ada today and covers about 70% of the market. This
creates a consistent and long lasting set of established users as these project last often for
decades, using the same codebase migrating from platform to platform.
More recently however, there has been an emerging interest for Ada in new communities of
users such as automotive, medical device, industrial automation and overall cyber-security.
This can probably be explained by a rise of safety, reliability and cyber-security require-
ments. The market is moving relatively rapidly today and we're anticipating an increase
of the Ada footprint in these domains, while still remaining a technology of choice for the
development of mission critical software.
The first piece of the answer lies in the user base of the Ada language, as seen in the
previous question. Projects using Ada in the aerospace and defence domain maintain source
code over decades, providing healthy funding foundation for Ada-based technologies.
AdaCore being the author of this course, it's difficult for us to be fair in our description
of other Ada compilation technologies. We will leave to the readers the responsibility of
forging their own opinion. If they present a credible alternative to the GNAT compiler, then
this whole section can be considered as void.
Assuming GNAT is the only option available, and acknowledging that this is an argument
that we're hearing from a number of Ada adopters, let's discuss the "sole source" issue.
First of all, it's worth noting that industries are using a lot of software that is provided by
only one source, so while non-ideal, these situations are also quite common.
In the case of the GNAT compiler however, while AdaCore is the main maintainer, this main-
tenance is done as part of an open-source community. This means that nothing prevents
a third party to start selling a competing set of products based on the same compiler, pro-
vided that it too adopts the open-source approach. Our job is to be more cost-effective
than the alternative, and indeed for the vast part this has prevented a competing offering
to emerge. However, should AdaCore disappear or switch focus, Ada users would not be
prevented from carrying on using its software (there is no lock) and a third party could take
over maintenance. This is not a theoretical case, this has been done in the past either by
companies looking at supporting their own version of GNAT, vendors occupying a specific
niche that was left uncovered , or hobbyists developing their own builds.
With that in mind, it's clear that the "sole source" provider issue is a circumstantial —
nothing is preventing other vendors from emerging if the conditions are met.
A language by itself is of little use for the development of safety-critical software. Instead,
a complete toolset is needed to accompany the development process, in particular tools
for edition, testing, static analysis, etc.
AdaCore provides a number of these tools either in through its core or add-on package.
These include (as of 2019):
• An IDE (GNAT Studio)
• An Eclipse plug-in (GNATbench)
• A debugger (GDB)
• A testing tool (GNATtest)
• A structural code coverage tool (GNATcoverage)
• A metric computation tool (GNATmetric)
• A coding standard checker (GNATcheck)
• Static analysis tools (CodePeer, SPARK Pro)
• A Simulink code generator (QGen)
• An Ada parser to develop custom tools (libadalang)
Ada is, however, an internationally standardized language, and many companies are pro-
viding third party solutions to complete the toolset. Overall, the language can be and is
used with tools on par with their equivalent C counterparts.
A common question from teams on the verge of selecting Ada and SPARK is how to manage
the developer team growth and turnover. While Ada and SPARK are taught by a growing
number of universities worldwide, it may still be challenging to hire new staff with prior Ada
experience.
Fortunately, Ada's base semantics are very close to those of C/C++, so that a good embed-
ded software developer should be able to learn it relatively easily. This course is definitely
a resource available to get started. Online training material is also available, together with
on-site in person training.
In general, getting an engineer operational in Ada and SPARK shouldn't take more than a
few weeks worth of time.
The most common scenario when introducing Ada and SPARK to a project or a team is to do
it within a pre-existing C codebase, which can already spread over hundreds of thousands if
not millions lines of code. Re-writing this software to Ada or SPARK is of course not practical
and counterproductive.
Most teams select either a small piece of existing code which deserves particular attention,
or new modules to develop, and concentrate on this. Developing this module or part of
the application will also help in developing the coding patterns to be used for the particular
project and company. This typically concentrates an effort of a few people on a few thou-
sands lines of code. The resulting code can be linked to the rest of the C application. From
there, the newly established practices and their benefit can slowly spread through the rest
of the environment.
Establishing this initial core in Ada and SPARK is critical, and while learning the language
isn't a particularly difficult task, applying it to its full capacity may require some expertise.
One possibility to accelerate this initial process is to use AdaCore mentorship services.
SEVENTYFIVE
CONCLUSION
Although Ada's syntax might seem peculiar to C developers at first glance, it was designed
to increase readability and maintainability, rather than making it faster to write in a con-
densed manner — as it is often the case in C.
Especially in the embedded domain, C developers are used to working at a very low level,
which includes mathematical operations on pointers, complex bit shifts, and logical bitwise
operations. C is well designed for such operations because it was designed to replace
Assembly language for faster, more efficient programming.
Ada can be used to describe high level semantics and architectures. The beauty of the
language, however, is that it can be used all the way down to the lowest levels of the
development, including embedded Assembly code or bit-level data management. How-
ever, although Ada supports bitwise operations such as masks and shifts, they should be
relatively rarely needed. When translating C code to Ada, it's good practice to consider
alternatives. In a lot of cases, these operations are used to insert several pieces of data
into a larger structure. In Ada, this can be done by describing the structure layout at the
type level through representation clauses, and then accessing this structure as any other.
For example, we can interpret an arbitrary data type as a bit-field and perform low-level
operations on it.
Because Ada is a strongly typed language, it doesn't define any implicit type conversions
like C. If we try to compile Ada code that contains type mismatches, we'll get a compilation
error. Because the compiler prevents mixing variables of different types without explicit
type conversion, we can't accidentally end up in a situation where we assume something
will happen implicitly when, in fact, our assumption is incorrect. In this sense, Ada's type
system encourages programmers to think about data at a high level of abstraction. Ada
supports overlays and unchecked conversions as a way of converting between unrelated
data type, which are typically used for interfacing with low-level elements such as registers.
In Ada, arrays aren't interchangeable with operations on pointers like in C. Also, array types
are considered first-class citizens and have dedicated semantics such as the availability of
the array's boundaries at run-time. Therefore, unhandled array overflows are impossible
unless checks are suppressed. Any discrete type can serve as an array index, and we can
specify both the starting and ending bounds. In addition, Ada offers high-level operations
for copying, slicing, and assigning values to arrays.
Although Ada supports pointers, most situations that would require a pointer in C do not in
Ada. In the vast majority of the cases, indirect memory management can be hidden from
the developer and thus prevent many potential errors. In C, pointers are typically used to
pass references to subprograms, for example. In contrast, Ada parameter modes indicate
the flow of information to the reader, leaving the means of passing that information to the
compiler.
When translating pointers from C code to Ada, we need to assess whether they are needed
in the first place. Ada pointers (access types) should only be used with complex structures
that cannot be allocated at run-time. There are many situations that would require a pointer
in C, but do not in Ada. For example, arrays — even when dynamically allocated —, results
of functions, passing of large structures as parameters, access to registers, etc.
1573
Learning Ada
Because of the absence of namespaces, global names in C tend to be very long. Also,
because of the absence of overloading, they can even encode type names in their name.
In Ada, a package is a namespace. Also, we can use the private part of a package to declare
private types and private subprograms. In fact, private types are useful for preventing the
users of those types from depending on the implementation details. Another use-case is
the prevention of package users from accessing the package state/data arbitrarily.
Ada has a dedicated set of features for interfacing with other languages, so we can easily
interface with our existing C code before translating it to Ada. Also, GNAT includes auto-
matic binding generators. Therefore, instead of re-writing the entire C code upfront, which
isn't practical or cost-effective, we can selectively translate modules from C to Ada.
When it comes to implementing concurrency and real time, Ada offers several options. Ada
provides high level constructs such as tasks and protected objects to express concurrency
and synchronization, which can be used when running on top of an operating system such
as Linux. On more constrained systems, such as bare metal or some real-time operating
systems, a subset of the Ada tasking capabilities — known as the Ravenscar and Jorvik
profiles — is available. Though restricted, this subset also has nice properties, in particular
the absence of deadlock,the absence of priority inversion, schedulability and very small
footprint. On bare metal systems, this also essentially means that Ada comes with its own
real-time kernel. The advantage of using the full Ada tasking model or the restricted profiles
is to enhance portability.
Ada includes many features typically used for embedded programming:
• Built-in support for handling interrupts, so we can process interrupts by attaching a
handler — as a protected procedure — to it.
• Built-in support for handling both volatile and atomic data.
• Support for register overlays, which we can use to create a structure that facilitates
manipulating bits from registers.
• Support for creating data streams for serialization of arbitrary information and trans-
mission over a communication channel, such as a serial port.
• Built-in support for fixed-point arithmetic, which is an option when our target device
doesn't have a floating-point unit or the result of calculations needs to be bit-exact.
Also, Ada compilers such as GNAT have built-in support for directly mixing Ada and Assembly
code.
Ada also supports contracts, which can be associated with types and variables to refine
values and define valid and invalid values. The most common kind of contract is a range
constraint — using the range reserved word. Ada also supports contract-based program-
ming in the form of preconditions and postconditions. One typical benefit of contract-based
programming is the removal of defensive code in subprogram implementations.
It is common to see embedded software being used in a variety of configurations that re-
quire small changes to the code for each instance. In C, variability is usually achieved
through macros and function pointers, the former being tied to static variability and the
latter to dynamic variability. Ada offers many alternatives for both techniques, which aim
at structuring possible variations of the software. Examples of static variability in Ada are:
genericity, simple derivation, configuration pragma files, and configuration packages. Ex-
amples of dynamic variability in Ada are: records with discriminants, variant records —
which may include the use of unions —, object orientation, pointers to subprograms, and
design by components using dynamic libraries.
There shouldn't be significant performance differences between code written in Ada and
code written in C — provided that they are semantically equivalent. One reason is that
the two languages are fairly similar in the way they implement imperative semantics, in
particular with regards to memory management or control flow. Therefore, they should be
equivalent on average. However, when a piece of code in Ada is significantly slower than its
counterpart in C, this usually comes from the fact that, while the two pieces of code appear
1575
Learning Ada
SEVENTYSIX
The goal of this appendix is to present a hands-on view on how to translate a system from
C to Ada and improve it with object-oriented programming.
Let's start with an overview of a simple system that we'll implement and use below. The
main system is called AB and it combines two systems A and B. System AB is not supposed
to do anything useful. However, it can serve as a good model for the hands-on we're about
to start.
This is a list of requirements for the individual systems A and B, and the combined system
AB:
• System A:
– The system can be activated and deactivated.
∗ During activation, the system's values are reset.
– Its current value (in floating-point) can be retrieved.
∗ This value is the average of the two internal floating-point values.
– Its current state (activated or deactivated) can be retrieved.
• System B:
– The system can be activated and deactivated.
∗ During activation, the system's value is reset.
– Its current value (in floating-point) can be retrieved.
– Its current state (activated or deactivated) can be retrieved.
• System AB
– The system contains an instance of system A and an instance of system B.
– The system can be activated and deactivated.
∗ System AB activates both systems A and B during its own activation.
∗ System AB deactivates both systems A and B during its own deactivation.
– Its current value (in floating-point) can be retrieved.
∗ This value is the average of the current values of systems A and B.
– Its current state (activated or deactivated) can be retrieved.
∗ AB is only considered activated when both systems A and B are activated.
1577
Learning Ada
In this section, we look into implementations (in both C and Ada) of system AB that don't
make use of object-oriented programming.
Listing 1: system_a.h
1 typedef struct {
2 float val[2];
3 int active;
4 } A;
5
Listing 2: system_a.c
1 #include "system_a.h"
2
Listing 3: system_b.h
1 typedef struct {
2 float val;
3 int active;
4 } B;
5
Listing 4: system_b.c
1 #include "system_b.h"
2
Listing 5: system_ab.h
1 #include "system_a.h"
2 #include "system_b.h"
3
4 typedef struct {
5 A a;
6 B b;
7 } AB;
8
Listing 6: system_ab.c
1 #include <math.h>
2 #include "system_ab.h"
3
Listing 7: main.c
1 #include <stdio.h>
2 #include "system_ab.h"
3
20 int main()
21 {
22 AB s;
23
27 display_active (&s);
28 display_check (&s);
29
33 display_active (&s);
34 }
Runtime output
Activating system AB...
System AB is active.
System AB check: PASSED.
Deactivating system AB...
System AB is not active.
Here, each system is implemented in a separate set of header and source-code files. For
example, the API of system AB is in system_ab.h and its implementation in system_ab.c.
In the main application, we instantiate system AB and activate it. Then, we proceed to dis-
play the activation state and the result of the system's health check. Finally, we deactivate
the system and display the activation state again.
Listing 8: system_a.ads
1 package System_A is
2
5 type A is record
6 Val : Val_Array (1 .. 2);
7 Active : Boolean;
8 end record;
9
18 end System_A;
Listing 9: system_a.adb
1 package body System_A is
2
24 end System_A;
3 type B is record
4 Val : Float;
5 Active : Boolean;
6 end record;
7
16 end System_B;
24 end System_B;
4 package System_AB is
5
6 type AB is record
7 SA : A;
8 SB : B;
9 end record;
10
21 end System_AB;
31 end System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 AB_Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.HandsOnOOP.System_AB_Ada
MD5: f2e3df0b3874e5edc5ea90c01961cf64
Runtime output
As you can see, this is a direct translation that doesn't change much of the structure of the
original C code. Here, the goal was to simply translate the system from one language to
another and make sure that the behavior remains the same.
3 type A is private;
4
13 private
14
17 type A is record
18 Val : Val_Array (1 .. 2);
19 Active : Boolean;
20 end record;
21
22 end Simple.System_A;
22 end Simple.System_A;
3 type B is private;
4
13 private
14
15 type B is record
16 Val : Float;
17 Active : Boolean;
18 end record;
19
20 end Simple.System_B;
22 end Simple.System_B;
4 package Simple.System_AB is
5
6 type AB is private;
7
18 private
19
20 type AB is record
21 SA : A;
22 SB : B;
23 end record;
24
25 end Simple.System_AB;
27 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.HandsOnOOP.System_AB_Ada_Enhanced
MD5: 5019a7088ab4160f5e3b33c73db2b03b
Runtime output
Until now, we haven't used any of the object-oriented programming features of the Ada
language. So we can start by analyzing the API of systems A and B and deciding how to
best abstract some of its elements using object-oriented programming.
76.3.1 Interfaces
The first thing we may notice is that we actually have two distinct sets of APIs there:
• one API for activating and deactivating the system.
• one API for retrieving the value of the system.
We can use this distinction to declare two interface types:
• Activation_IF for the Activate and Deactivate procedures and the Is_Active func-
tion;
• Value_Retrieval_IF for the Value function.
This is how the declaration could look like:
Note that, because we are declaring interface types, all operations on those types must be
abstract or, in the case of procedures, they can also be declared null. For example, we
could change the declaration of the procedures above to this:
When an operation is declared abstract, we must override it for the type that derives from
the interface. When a procedure is declared null, it acts as a do-nothing default. In this
case, overriding the operation is optional for the type that derives from this interface.
Since the original system needs both interfaces we've just described, we have to declare
another type that combines those interfaces. We can do this by declaring the interface type
Sys_Base, which serves as the base type for systems A and B. This is the declaration:
Since the system activation functionality is common for both systems A and B, we could
implement it as part of Sys_Base. That would require changing the declaration from a
simple interface to an abstract record:
Now, we can add the Boolean component to the record (as a private component) and over-
ride the subprograms of the Activation_IF interface. This is the adapted declaration:
private
In the declaration of the Sys_Base type we've just seen, we're not overriding the Value
function — from the Value_Retrieval_IF interface — for the Sys_Base type, so it remains
an abstract function for Sys_Base. Therefore, the Sys_Base type itself remains abstract
and needs be explicitly declared as such.
We use this strategy to ensure that all types derived from Sys_Base need to implement
their own version of the Value function. For example:
Here, the A type is derived from the Sys_Base and it includes its own version of the Value
function by overriding it. Therefore, A is not an abstract type anymore and can be used to
declare objects:
procedure Main is
Obj : A;
V : Float;
begin
Obj.Activate;
V := Obj.Value;
end Main;
Important
Note that the use of the overriding keyword in the subprogram declaration is not strictly
necessary. In fact, we could leave this keyword out, and the code would still compile.
However, if provided, the compiler will check whether the information is correct.
Using the overriding keyword can help to avoid bad surprises — when you may think that
you're overriding a subprogram, but you're actually not. Similarly, you can also write not
overriding to be explicit about subprograms that are new primitives of a derived type. For
example:
We also need to declare the values that are used internally in systems A and B. For system
A, this is the declaration:
private
In the previous implementation, we've seen that the A_Activate and B_Activate proce-
dures perform the following steps:
• initialize internal values;
• indicate that the system is active (by setting the Active flag to True).
In the implementation of the Activate procedure for the Sys_Base type, however, we're
only dealing with the second step. Therefore, we need to override the Activate procedure
and make sure that we initialize internal values as well. First, we need to declare this
procedure for type A:
In the implementation of Activate, we should call the Activate procedure from the parent
(Sys_Base) to ensure that whatever was performed for the parent will be performed in the
derived type as well. For example:
Here, by writing Sys_Base (E), we're performing a view conversion. Basically, we're telling
the compiler to view E not as an object of type A, but of type Sys_Base. When we do this,
any operation performed on this object will be done as if it was an object of Sys_Base type,
which includes calling the Activate procedure of the Sys_Base type.
Important
If we write T (Obj).Proc, we're telling the compiler to call the Proc procedure of type T
and apply it on Obj.
If we write T'Class (Obj).Proc, however, we're telling the compiler to dispatch the call.
For example, if Obj is of derived type T2 and there's an overridden Proc procedure for type
T2, then this procedure will be called instead of the Proc procedure for type T.
76.3.5 Type AB
While the implementation of systems A and B is almost straightforward, it gets more in-
teresting in the case of system AB. Here, we have a similar API, but we don't need the
activation mechanism implemented in the abstract type Sys_Base. Therefore, deriving
from Sys_Base is not the best option. Instead, when declaring the AB type, we can simply
use the same interfaces as we did for Sys_Base, but keep it independent from Sys_Base.
For example:
private
Naturally, we still need to override all the subprograms that are part of the Activation_IF
and Value_Retrieval_IF interfaces. Also, we need to implement the additional Check
function that was originally only available on system AB. Therefore, we declare these sub-
programs:
20 private
21
27 end Simple;
16 end Simple;
9 private
10
17 end Simple.System_A;
15 end Simple.System_A;
9 private
10
15 end Simple.System_B;
12 end Simple.System_B;
4 package Simple.System_AB is
5
16 private
17
23 end Simple.System_AB;
27 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
28 Activate (S);
29
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Runtime output
Activating system AB...
System AB is active
System AB check: PASSED
Deactivating system AB...
System AB is not active
When analyzing the complete source-code, we see that there are at least two areas that
we could still improve.
The first issue concerns the implementation of the Activate procedure for types derived
from Sys_Base. For those derived types, we're expecting that the Activate procedure of
the parent must be called in the implementation of the overriding Activate procedure. For
example:
package body Simple.System_A is
If a developer forgets to call that specific Activate procedure, however, the system won't
work as expected. A better strategy could be the following:
• Declare a new Activation_Reset procedure for Sys_Base type.
• Make a dispatching call to the Activation_Reset procedure in the body of the Acti-
vate procedure (of the Sys_Base type).
• Let the derived types implement their own version of the Activation_Reset proce-
dure.
This is a simplified view of the implementation using the points described above:
package Simple is
end Simple;
E.Active := True;
end Activate;
end Simple;
package Simple.System_A is
private
end Simple.System_A;
end Simple.System_A;
The next area that we could improve is in the declaration of the system AB. In the previous
implementation, we were explicitly describing the two components of that system, namely
a component of type A and a component of type B:
Of course, this declaration matches the system requirements that we presented in the
beginning. However, we could use strategies that make it easier to incorporate requirement
changes later on. For example, we could hide this information about systems A and B by
simply declaring an array of components of type access Sys_Base'Class and allocate
them dynamically in the body of the package. Naturally, this approach might not be suitable
for certain platforms. However, the advantage would be that, if we wanted to replace the
component of type B by a new component of type C, for example, we wouldn't need to
change the interface. This is how the updated declaration could look like:
Important
Note that we're now using the limited keyword in the declaration of type AB. That is nec-
essary because we want to prevent objects of type AB being copied by assignment, which
would lead to two objects having the same (dynamically allocated) subsystems A and B
internally. This change requires that both Activation_IF and Value_Retrieval_IF are
declared limited as well.
Another approach that we could use to implement the dynamic allocation of systems A and
B is to declare AB as a limited controlled type — based on the Limited_Controlled type of
the Ada.Finalization package.
The Limited_Controlled type includes the following operations:
• Initialize, which is called when objects of a type derived from the Lim-
ited_Controlled type are being created — by declaring an object of the derived
type, for example —, and
• Finalize, which is called when objects are being destroyed — for example, when an
object gets out of scope at the end of a subprogram where it was created.
In this case, we must override those procedures, so we can use them for dynamic memory
allocation. This is a simplified view of the update implementation:
package Simple.System_AB is
end Simple.System_AB;
end Simple.System_AB;
22 private
23
29 end Simple;
9 E.Active := True;
10 end Activate;
11
20 end Simple;
7 private
8
17 end Simple.System_A;
14 end Simple.System_A;
7 private
8
15 end Simple.System_B;
11 end Simple.System_B;
3 package Simple.System_AB is
4
16 private
17
29 end Simple.System_AB;
48 end Simple.System_AB;
5 procedure Main is
6
25 S : AB;
26 begin
27 Put_Line ("Activating system AB...");
(continues on next page)
30 Display_Active (S);
31 Display_Check (S);
32
36 Display_Active (S);
37 end Main;
Project: Courses.Ada_For_Embedded_C_Dev.HandsOnOOP.System_AB_Ada_OOP_2
MD5: f8d0d4a07aaa045cb30bddc88db2215a
Runtime output
Naturally, this is by no means the best possible implementation of system AB. By applying
other software design strategies that we haven't covered here, we could most probably
think of different ways to use object-oriented programming to improve this implementa-
tion. Also, in comparison to the original implementation (page 1581), we recognize that
the amount of source-code has grown. On the other hand, we now have a system that is
factored nicely, and also more extensible.
1605
Learning Ada
This book presents the SPARK technology — the SPARK subset of Ada and its supporting
static analysis tools — through an example-driven comparison with the rules in the widely
known MISRA C subset of the C language.
This document was prepared by Yannick Moy, with contributions and review from Ben Bros-
gol.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
Note: Each code example from this book has an associated "code block metadata", which
contains the name of the "project" and an MD5 hash value. This information is used to
identify a single code example.
You can find all code examples in a zip file, which you can download from the learn web-
site336 . The directory structure in the zip file is based on the code block metadata. For
example, if you're searching for a code example with this metadata:
• Project: Courses.Intro_To_Ada.Imperative_Language.Greet
• MD5: cba89a34b87c9dfa71533d982d05e6ab
you will find it in this directory:
projects/Courses/Intro_To_Ada/Imperative_Language/Greet/
cba89a34b87c9dfa71533d982d05e6ab/
In order to use this code example, just follow these steps:
1. Unpack the zip file;
2. Go to target directory;
3. Start GNAT Studio on this directory;
4. Build (or compile) the project;
5. Run the application (if a main procedure is available in the project).
335 http://creativecommons.org/licenses/by-sa/4.0
336 https://learn.adacore.com/zip/learning-ada_code.zip
1607
Learning Ada
1608
CHAPTER
SEVENTYSEVEN
PREFACE
1609
Learning Ada
a deviation of rule 10.6 "The value of a composite expression shall not be assigned to an
object with wider essential type":
Here, the multiplication of two unsigned 16-bit values and assignment of the result to an
unsigned 32-bit variable constitutes a violation of the aforementioned rule, which gets justi-
fied for efficiency reasons. What the authors seem to have missed is that the multiplication
is then performed with the signed integer type int instead of the target unsigned type
uint32_t. Thus the multiplication of two unsigned 16-bit values may lead to an overflow
of the 32-bit intermediate signed result, which is an occurrence of an undefined behavior.
In such a case, a compiler is free to assume that the value of prod cannot exceed 231 - 1
(the maximal value of a signed 32-bit integer) as otherwise an undefined behavior would
have been triggered. For example, the undefined behavior with values 65535 for qty and
time_step is reported when running the code compiled by either the GCC or LLVM compiler
with option -fsanitize=undefined.
The MISRA C checkers that detect violations of undecidable rules are either unsound tools
that can detect only some of the violations, or sound tools that guarantee to detect all
such violations at the cost of possibly many false reports of violations. This is a direct
consequence of undecidability. However, static analysis technology is available that can
achieve soundness without inundating users with false alarms. One example is the SPARK
toolset developed by AdaCore, Altran and Inria, which is based on four principles:
• The base language Ada provides a solid foundation for static analysis through a well-
defined language standard, strong typing and rich specification features.
• The SPARK subset of Ada restricts the base language in essential ways to support static
analysis, by controlling sources of ambiguity such as side-effects and aliasing.
• The static analysis tools work mostly at the granularity of an individual function, mak-
ing the analysis more precise and minimizing the possibility of false alarms.
• The static analysis tools are interactive, allowing users to guide the analysis if neces-
sary or desired.
In this document, we show how SPARK can be used to achieve high code quality with guar-
antees that go beyond what would be feasible with MISRA C.
An on-line and interactive version of this document is available at AdaCore's
learn.adacore.com site341 .
341 https://learn.adacore.com/courses/SPARK_for_the_MISRA_C_Developer
SEVENTYEIGHT
Many consistency properties that are taken for granted in other languages are not enforced
in C. The basic property that all uses of a variable or function are consistent with its type is
not enforced by the language and is also very difficult to enforce by a tool. Three features
of C contribute to that situation:
• the textual-based inclusion of files means that every included declaration is subject to
a possibly different reinterpretation depending on context.
• the lack of consistency requirements across translation units means that type incon-
sistencies can only be detected at link time, something linkers are ill-equipped to do.
• the default of making a declaration externally visible means that declarations that
should be local will be visible to the rest of the program, increasing the chances for
inconsistencies.
MISRA C contains guidelines on all three fronts to enforce basic program consistency.
The text-based inclusion of files is one of the dated idiosyncracies of the C programming
language that was inherited by C++ and that is known to cause quality problems, especially
during maintenance. Although multiple inclusion of a file in the same translation unit can
be used to emulate template programming, it is generally undesirable. Indeed, MISRA C
defines Directive 4.10 precisely to forbid it for header files: "Precautions shall be taken in
order to prevent the contents of a header file being included more than once".
The subsequent section on "Preprocessing Directives" contains 14 rules restricting the use
of text-based inclusion through preprocessing. Among other things these rules forbid the
use of the #undef directive (which works around conflicts in macro definitions introduced by
text-based inclusion) and enforces the well-known practice of enclosing macro arguments
in parentheses (to avoid syntactic reinterpretations in the context of the macro use).
SPARK (and more generally Ada) does not suffer from these problems, as it relies on se-
mantic inclusion of context instead of textual inclusion of content, using with clauses:
Listing 1: hello_world.adb
1 with Ada.Text_IO;
2
3 procedure Hello_World is
4 begin
5 Ada.Text_IO.Put_Line ("hello, world!");
6 end Hello_World;
1611
Learning Ada
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hello_World
MD5: 5ed9609dd61bbcee252bb8529a6d3479
Runtime output
hello, world!
Note that with clauses are only allowed at the beginning of files; the compiler issues an
error if they are used elsewhere:
Listing 2: hello_world.adb
1 procedure Hello_World is
2 with Ada.Text_IO; -- Illegal
3 begin
4 Ada.Text_IO.Put_Line ("hello, world!");
5 end Hello_World;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hello_World
MD5: afa19e8e2c114a5832b49e9efcbe675e
Listing 3: hello_world.adb
1 with Ada.Text_IO;
2 with Ada.Text_IO; -- Legal but useless
3
4 procedure Hello_World is
5 begin
6 Ada.Text_IO.Put_Line ("hello, world!");
7 end Hello_World;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hello_World
MD5: 270928968d7beb4809af9e62df530722
Runtime output
hello, world!
The order in which units are imported is irrelevant. All orders are valid and have the same
semantics.
No conflict arises from importing multiple units, even if the same name is defined in several,
since each unit serves as namespace for the entities which it defines. So we can define our
own version of Put_Line in some Helper unit and import it together with the standard
version defined in Ada.Text_IO:
Listing 4: helper.ads
1 package Helper is
2 procedure Put_Line (S : String);
3 end Helper;
Listing 5: helper.adb
1 with Ada.Text_IO;
2
Listing 6: hello_world.adb
1 with Ada.Text_IO;
2 with Helper;
3
4 procedure Hello_World is
5 begin
6 Ada.Text_IO.Put_Line ("hello, world!");
7 Helper.Put_Line ("hello, world!");
8 end Hello_World;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hello_World
MD5: 5fa012cc996e24e3b1f604e35bbba44f
Runtime output
hello, world!
Start helper version
hello, world!
End helper version
The only way a conflict can arise is if we want to be able to reference Put_Line directly,
without using the qualified name Ada.Text_IO.Put_Line or Helper.Put_Line. The use
clause makes public declarations from a unit available directly:
Listing 7: helper.ads
1 package Helper is
2 procedure Put_Line (S : String);
3 end Helper;
Listing 8: helper.adb
1 with Ada.Text_IO;
2
Listing 9: hello_world.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Helper; use Helper;
3
4 procedure Hello_World is
5 begin
6 Ada.Text_IO.Put_Line ("hello, world!");
7 Helper.Put_Line ("hello, world!");
8 Put_Line ("hello, world!"); -- ERROR
9 end Hello_World;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hello_World
MD5: 405e138d78e0dc869e8a340681d87e61
Build output
Here, both units Ada.Text_IO and Helper define a procedure Put_Line taking a String as
argument, so the compiler cannot disambiguate the direct call to Put_Line and issues an
error.
Note that it helpfully points to candidate declarations, so that the user can decide which
qualified name to use as in the previous two calls.
Issues arising in C as a result of text-based inclusion of files are thus completely prevented
in SPARK (and Ada) thanks to semantic import of units. Note that the C++ committee
identified this weakness some time ago and has approved342 the addition of modules to
C++20, which provide a mechanism for semantic import of units.
An issue related to text-based inclusion of files is that there is no single source for declaring
the type of a variable or function. If a file origin.c defines a variable var and functions
fun and print:
3 int var = 0;
4 int fun() {
5 return 1;
6 }
7 void print() {
8 printf("var = %d\n", var);
9 }
342 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4720.pdf
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Origin
MD5: 3395f1e43408d5bc5c1e6b8431c959d6
and the corresponding header file origin.h declares var, fun and print as having external
linkage:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Origin
MD5: e8e880a16f5099dc1e0a75ffeeeb9468
then client code can include origin.h with declarations for var and fun:
3 int main() {
4 var = fun();
5 print();
6 return 0;
7 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Origin
MD5: 3d4582d3956897b657778ae355d0ef1b
Runtime output
var = 1
5 int main() {
6 var = fun();
7 print();
8 return 0;
9 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Origin
MD5: 4b25aa011b580f92f2831a48008fbef6
Runtime output
var = 1
Then, if an inconsistency is introduced in the type of var of fun between these alternative
declarations and their actual type, the compiler cannot detect it. Only the linker, which has
access to the set of object files for a program, can detect such inconsistencies. However, a
linker's main task is to link, not to detect inconsistencies, and so inconsistencies in the type
of variables and functions in most cases cannot be detected. For example, most linkers
cannot detect if the type of var or the return type of fun is changed to float in the decla-
rations above. With the declaration of var changed to float, the above program compiles
and runs without errors, producing the erroneous output var = 1065353216 instead of var
=1. With the return type of fun changed to float instead, the program still compiles and
runs without errors, producing this time the erroneous output var = 0.
The inconsistency just discussed is prevented by MISRA C Rule 8.3 "All declarations of an ob-
ject or function shall use the same names and type qualifiers". This is a decidable rule, but
it must be enforced at system level, looking at all translation units of the complete program.
MISRA C Rule 8.6 also requires a unique definition for a given identifier across translation
units, and Rule 8.5 requires that an external declaration shared between translation units
comes from the same file. There is even a specific section on "Identifiers" containing 9 rules
requiring uniqueness of various categories of identifiers.
SPARK (and more generally Ada) does not suffer from these problems, as it relies on se-
mantic inclusion of context using with clauses to provide a unique declaration for each
entity.
Many problems in C stem from the lack of encapsulation. There is no notion of namespace
that would allow a file to make its declarations available without risking a conflict with
other files. Thus MISRA C has a number of guidelines that discourage the use of external
declarations:
• Directive 4.8 encourages hiding the definition of structures and unions in implemen-
tation files (.c files) when possible: "If a pointer to a structure or union is never deref-
erenced within a translation unit, then the implementation of the object should be
hidden."
• Rule 8.7 forbids the use of external declarations when not needed: "Functions and
objects should not be defined with external linkage if they are referenced in only one
translation unit."
• Rule 8.8 forces the explicit use of keyword static when appropriate: "The static stor-
age class specifier shall be used in all declarations of objects and functions that have
internal linkage."
The basic unit of modularization in SPARK, as in Ada, is the package. A package always has
a spec (in an .ads file), which defines the interface to other units. It generally also has a
body (in an .adb file), which completes the spec with an implementation. Only declarations
from the package spec are visible from other units when they import (with) the package. In
fact, only declarations from what is called the "visible part" of the spec (before the keyword
private) are visible from units that with the package.
3 procedure Hello_World is
4 begin
5 Public_Put_Line ("hello, world!");
6 Private_Put_Line ("hello, world!"); -- ERROR
7 Body_Put_Line ("hello, world!"); -- ERROR
8 end Hello_World;
Build output
hello_world.adb:6:04: error: "Private_Put_Line" is not visible
hello_world.adb:6:04: error: non-visible (private) declaration at helper.ads:4
hello_world.adb:7:04: error: "Body_Put_Line" is undefined
gprbuild: *** compilation phase failed
Note the different errors on the calls to the private and body versions of Put_Line. In
the first case the compiler can locate the candidate procedure but it is illegal to call
it from Hello_World, in the second case the compiler does not even know about any
Body_Put_Line when compiling Hello_World since it only looks at the spec and not the
body.
SPARK (and Ada) also allow defining a type in the private part of a package spec while
simply declaring the type name in the public ("visible") part of the spec. This way, client
code — i.e., code that with's the package — can use the type, typically through a public
API, but have no access to how the type is implemented:
3 package Information_System is
4 Archive : Vault.Data;
5 end Information_System;
4 procedure Hacker is
5 V : Integer := Vault.Get (Information_System.Archive);
6 begin
7 Vault.Set (Information_System.Archive, V + 1);
8 Information_System.Archive.Val := 0; -- ERROR
9 end Hacker;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Program_Consistency.Hacker
MD5: 065ed34dc727e2eb0bdc50a667cb1f78
Build output
SEVENTYNINE
C's syntax is concise but also very permissive, which makes it easy to write programs whose
effect is not what was intended. MISRA C contains guidelines to:
• clearly distinguish code from comments
• specially handle function parameters and result
• ensure that control structures are not abused
The problem arises from block comments in C, starting with /* and ending with */. These
comments do not nest with other block comments or with line comments. For example,
consider a block comment surrounding three lines that each increase variable a by one:
/*
++a;
++a;
++a; */
Now consider what happens if the first line is commented out using a block comment and
the third line is commented out using a line comment (also known as a C++ style comment,
allowed in C since C99):
/*
/* ++a; */
++a;
// ++a; */
The result of commenting out code that was already commented out is that the second line
of code becomes live! Of course, the above example is simplified, but similar situations
do arise in practice, which is the reason for MISRA C Directive 4.1 "Sections of code should
not be 'commented out'". This is reinforced with Rules 3.1 and 3.2 from the section on
"Comments" that forbid in particular the use of /* inside a comment like we did above.
These situations cannot arise in SPARK (or in Ada), as only line comments are permitted,
using --:
-- A := A + 1;
-- A := A + 1;
-- A := A + 1;
So commenting again the first and third lines does not change the effect:
-- -- A := A + 1;
-- A := A + 1;
-- -- A := A + 1;
1619
Learning Ada
It is possible in C to ignore the result of a function call, either implicitly or else explicitly by
converting the result to void:
f();
(void)f();
This is particularly dangerous when the function returns an error status, as the caller is then
ignoring the possibility of errors in the callee. Thus the MISRA C Directive 4.7: "If a function
returns error information, then that error information shall be tested". In the general case
of a function returning a result which is not an error status, MISRA C Rule 17.7 states that
"The value returned by a function having non-void return type shall be used", where an
explicit conversion to void counts as a use.
In SPARK, as in Ada, the result of a function call must be used, for example by assigning it to
a variable or by passing it as a parameter, in contrast with procedures (which are equivalent
to void-returning functions in C). SPARK analysis also checks that the result of the function
is actually used to influence an output of the calling subprogram. For example, the first two
calls to F in the following are detected as unused, even though the result of the function
call is assigned to a variable, which is itself used in the second case:
Listing 1: fun.ads
1 package Fun is
2 function F return Integer is (1);
3 end Fun;
Listing 2: use_f.ads
1 procedure Use_F (Z : out Integer);
Listing 3: use_f.adb
1 with Fun; use Fun;
2
8 Y := F;
9 X := Y;
10
11 Z := F;
12 end Use_F;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Func_Return
MD5: 4fc78b4136677d6338984ab8ccfa5cd1
Prover output
Only the result of the third call is used to influence the value of an output of Use_F, here
the output parameter Z of the procedure.
In C, function parameters are treated as local variables of the function. They can be mod-
ified, but these modifications won't be visible outside the function. This is an opportunity
for mistakes. For example, the following code, which appears to swap the values of its
parameters, has in reality no effect:
MISRA C Rule 17.8 prevents such mistakes by stating that "A function parameter should not
be modified".
No such rule is needed in SPARK, since function parameters are only inputs so cannot be
modified, and procedure parameters have a mode defining whether they can be modified or
not. Only parameters of mode out or ada:in out can be modified — and these are prohibited
from functions in SPARK — and their modification is visible at the calling site. For example,
assigning to a parameter of mode in (the default parameter mode if omitted) results in
compilation errors:
Listing 4: swap.ads
1 procedure Swap (X, Y : Integer);
Listing 5: swap.adb
1 procedure Swap (X, Y : Integer) is
2 Tmp : Integer := X;
3 begin
4 X := Y; -- ERROR
5 Y := Tmp; -- ERROR
6 end Swap;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Swap
MD5: 187927c610e202f2e1eee6a602fda25e
Build output
5. Y := Tmp; -- ERROR
|
>>> assignment to "in" mode parameter not allowed
6. end Swap;
Listing 6: swap.ads
1 procedure Swap (X, Y : in out Integer);
Listing 7: swap.adb
1 procedure Swap (X, Y : in out Integer) is
2 Tmp : constant Integer := X;
3 begin
4 X := Y;
5 Y := Tmp;
6 end Swap;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Swap
MD5: c983a229fc5a69db5dbb85f49a91b325
Prover output
The previous issue (ignoring the result of a function call) is an example of a control structure
being abused, due to the permissive syntax of C. There are many such examples, and MISRA
C contains a number of guidelines to prevent such abuse.
Because a semicolon can act as a statement, and because an if-statement and a loop accept
a simple statement (possibly only a semicolon) as body, inserting a single semicolon can
completely change the behavior of the code:
int func() {
if (0)
return 1;
while (1)
return 0;
return 0;
}
As written, the code above returns with status 0. If a semicolon is added after the first line
(if (0);), then the code returns with status 1. If a semicolon is added instead after the
third line (while (1);), then the code does not return. To prevent such surprises, MISRA C
Rule 15.6 states that "The body of an iteration-statement or a selection-statement shall be
a compound statement" so that the code above must be written:
int func() {
if (0) {
return 1;
}
while (1) {
return 0;
}
return 0;
}
Note that adding a semicolon after the test of the if or while statement has the same
effect as before! But doing so would violate MISRA C Rule 15.6.
In SPARK, the semicolon is not a statement by itself, but rather a marker that terminates
a statement. The null statement is an explicit null;, and all blocks of statements have
explicit begin and end markers, which prevents mistakes that are possible in C. The SPARK
(also Ada) version of the above C code is as follows:
Listing 8: func.ads
1 function Func return Integer;
Listing 9: func.adb
1 function Func return Integer is
2 begin
3 if False then
4 return 1;
5 end if;
6 while True loop
7 return 0;
8 end loop;
9 return 0;
10 end Func;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Semicolon
MD5: 34fc5967c41d337aada17429ee5f44e9
Prover output
Switch statements are well-known for being easily misused. Control can jump to any case
section in the body of the switch, which in C can be before any statement contained in the
body of the switch. At the end of the sequence of statements associated with a case, exe-
cution continues with the code that follows unless a break is encountered. This is a recipe
for mistakes, and MISRA C enforces a simpler well-formed syntax for switch statements
defined in Rule 16.1: "All switch statements shall be well-formed".
The other rules in the section on "Switch statements" go on detailing individual conse-
quences of Rule 16.1. For example Rule 16.3 forbids the fall-through from one case to the
next: "An unconditional break statement shall terminate every switch-clause". As another
example, Rule 16.4 mandates the presence of a default case to handle cases not taken into
account explicitly: "Every switch statement shall have a default label".
The analog of the C switch statements in SPARK (and in Ada) is the case statement. This
statement has a simpler and more robust structure than the C switch, with control auto-
matically exiting after one of the case alternatives is executed, and the compiler checking
that the alternatives are disjoint (like in C) and complete (unlike in C). So the following code
is rejected by the compiler:
18 end Sign_Domain;
12 end Sign_Domain;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Case_Statement
MD5: d345a4d23b5b2402f8bd103e5e550a3b
Build output
sign_domain.adb:7:15: error: the choice "others" must appear alone and last
sign_domain.ads:6:07: error: missing case value: "Zero"
sign_domain.ads:14:15: error: duplication of choice value: "Positive" at line 13
gprbuild: *** compilation phase failed
The error in function Opposite is that the when choices do not cover all values of the target
expression. Here, A is of the enumeration type Sign, so all three values of the enumeration
must be covered.
The error in function Multiply is that Positive is covered twice, in the second and the
third alternatives. This is not allowed.
The error in procedure Get_Sign is that the others choice (the equivalent of C default
case) must come last. Note that an others choice would be useless in Opposite and Mul-
tiply, as all Sign values are covered.
Here is a correct version of the same code:
19 end Sign_Domain;
12 end Sign_Domain;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Case_Statement
MD5: 1c99fc53d2d2c0dddbea5e5b0a6c5746
Prover output
Similarly to C switches, for-loops in C can become unreadable. MISRA C thus enforces a sim-
pler well-formed syntax for for-loops, defined in Rule 14.2: "A for loop shall be well-formed".
The main effect of this simplification is that for-loops in C look like for-loops in SPARK (and
in Ada), with a loop counter that is incremented or decremented at each iteration. Section
8.14 defines precisely what a loop counter is:
1. It has a scalar type;
2. Its value varies monotonically on each loop iteration; and
3. It is used in a decision to exit the loop.
In particular, Rule 14.2 forbids any modification of the loop counter inside the loop body.
Here's the example used in MISRA C:2012 to illustrate this rule:
The equivalent SPARK (and Ada) code does not compile, because of the attempt to modify
the value of the loop counter:
7 if C then
8 Flag := True;
9 end if;
10
11 I := I + 3; -- ERROR
12 end loop;
13 end Well_Formed_Loop;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Well_Formed_Loop
MD5: 842564c961aa018e03e03f81439995ec
Build output
Removing the problematic line leads to a valid program. Note that the additional condition
being tested in the C for-loop has been moved to a separate exit statement at the start of
the loop body.
SPARK (and Ada) loops can increase (or, with explicit syntax, decrease) the loop counter by
1 at each iteration.
SPARK loops can iterate over any discrete type; i.e., integers as above or enumerations:
C does not provide a closing symbol for an if-statement. This makes it possible to write the
following code, which appears to try to return the absolute value of its argument, while it
actually does the opposite:
13 int main() {
14 printf("absval(5) = %d\n", absval(5));
15 printf("absval(0) = %d\n", absval(0));
16 printf("absval(-10) = %d\n", absval(-10));
17 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Dangling_Else_C
MD5: c180a948dd8bed4e3b97efde1522214c
Runtime output
absval(5) = -5
absval(0) = 0
absval(-10) = -10
The warning issued by GCC or LLVM with option -Wdangling-else (implied by -Wall) gives
a clue about the problem: although the else branch is written as though it completes the
outer if-statement, in fact it completes the inner if-statement.
MISRA C Rule 15.6 avoids the problem: "The body of an iteration-statement or a selection-
statement shall be a compound statement". That's the same rule as the one shown earlier
for Preventing the Semicolon Mistake (page 1622). So the code for absval must be written:
15 int main() {
16 printf("absval(5) = %d\n", absval(5));
17 printf("absval(0) = %d\n", absval(0));
18 printf("absval(-10) = %d\n", absval(-10));
19 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Dangling_Else_
↪MISRA_C
MD5: 2b76377aca52ff45ed6b19fa1f367473
Runtime output
absval(5) = 5
absval(0) = 0
absval(-10) = 10
Project: Courses.SPARK_For_The_MISRA_C_Dev.Syntactic_Guarantees.Dangling_Else_Ada
MD5: e867b6354ef7bdd89bae1673e888153a
Prover output
Interestingly, SPARK analysis detects here that the negation operation on line 9 might over-
flow. That's an example of runtime error detection which will be covered in the chapter on
Detecting Undefined Behavior (page 1665).
EIGHTY
Pointers in C provide a low-level view of the addressable memory as a set of integer ad-
dresses. To write at address 42, just go through a pointer:
Listing 1: main.c
1 int main() {
2 int *p = 42;
3 *p = 0;
4 return 0;
5 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Pointers_C
MD5: 005183ada50cb6642f38a3640d77efff
1631
Learning Ada
In an attempt to rule out issues that come from direct addressing of memory with pointers,
MISRA C states in Rule 11.4 that "A conversion should not be performed between a pointer
to object and an integer type". As this rule is classified as only Advisory, MISRA C completes
it with two Required rules:
• Rule 11.6: "A cast shall not be performed between pointer to void and an arithmetic
type"
• Rule 11.7: "A cast shall not be performed between pointer to object and a non-integer
arithmetic type"
In Ada, pointers are not addresses, and addresses are not integers. An opaque standard
type System.Address is used for addresses, and conversions to/from integers are provided
in a standard package System.Storage_Elements. The previous C code can be written as
follows in Ada:
Listing 2: pointer.adb
1 with System;
2 with System.Storage_Elements;
3
4 procedure Pointer is
5 A : constant System.Address := System.Storage_Elements.To_Address (42);
6 M : aliased Integer with Address => A;
7 P : constant access Integer := M'Access;
8 begin
9 P.all := 0;
10 end Pointer;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Pointers_Ada
MD5: 32ac91ade61a39d3505d155d7b97a8a5
Passing parameters by reference is critical for efficient programs, but the absence of refer-
ences distinct from pointers in C incurs a serious risk. Any parameter of a pointer type can
be copied freely to a variable whose lifetime is longer than the object pointed to, a problem
known as "dangling pointers". MISRA C forbids such uses in Rule 18.6: "The address of
an object with automatic storage shall not be copied to another object that persists after
the first object has ceased to exist". Unfortunately, enforcing this rule is difficult, as it is
undecidable.
In SPARK, parameters can be passed by reference, but no pointer to the parameter can be
stored past the return point of the function, which completely solves this issue. In fact, the
decision to pass a parameter by copy or by reference rests in many cases with the compiler,
but such compiler dependency has no effect on the functional behavior of a SPARK program.
In the example below, the compiler may decide to pass parameter P of procedure Rotate_X
either by copy or by reference, but regardless of the choice the postcondition of Rotate_X
will hold: the final value of P will be modified by rotation around the X axis.
Listing 3: geometry.ads
1 package Geometry is
2
10 end Geometry;
Listing 4: geometry.adb
1 package body Geometry is
2
10 end Geometry;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Geometry
MD5: d3801cf1413887ffd5fff8b6b86b7742
Prover output
SPARK's analysis tool can mathematically prove that the postcondition is true.
The greatest source of vulnerabilities regarding pointers is their use as substitutes for ar-
rays. Although the C language has a syntax for declaring and accessing arrays, this is just
a thin syntactic layer on top of pointers. Thus:
• Array access is just pointer arithmetic;
• If a function is to manipulate an array then the array's length must be separately
passed as a parameter; and
• The program is susceptible to the various vulnerabilities originating from the confusion
of pointers and arrays, such as buffer overflow.
Consider a function that counts the number of times a value is present in an array. In C,
this could be written:
Listing 5: main.c
1 #include <stdio.h>
2
13 int main() {
14 int p[5] = {0, 3, 9, 3, 3};
15 int c = count(p, 5, 3);
16 printf("value 3 is seen %d times in p\n", c);
17 return 0;
18 }
Runtime output
value 3 is seen 3 times in p
Function count has no control over the range of addresses accessed from pointer p. The
critical property that the len parameter is a valid length for an array of integers pointed to
by parameter p rests completely with the caller of count, and count has no way to check
that this is true.
To mitigate the risks associated with pointers being used for arrays, MISRA C contains eight
rules in a section on "Pointers and arrays". These rules forbid pointer arithmetic (Rule 18.4)
or, if this Advisory rule is not followed, require pointer arithmetic to stay within bounds (Rule
18.1). But, even if we rewrite the loop in count to respect all decidable MISRA C rules, the
program's correctness still depends on the caller of count passing a correct value of len:
Listing 6: main.c
1 #include <stdio.h>
2
13 int main() {
14 int p[5] = {0, 3, 9, 3, 3};
15 int c = count(p, 5, 3);
16 printf("value 3 is seen %d times in p\n", c);
17 return 0;
18 }
Runtime output
value 3 is seen 3 times in p
The resulting code is more readable, but still vulnerable to incorrect values of parameter
len passed by the caller of count, which violates undecidable MISRA C Rules 18.1 (pointer
arithmetic should stay within bounds) and 1.3 (no undefined behavior). Contrast this with
the same function in SPARK (and Ada):
Listing 7: types.ads
1 package Types is
2 type Int_Array is array (Positive range <>) of Integer;
3 end Types;
Listing 8: count.ads
1 with Types; use Types;
2
Listing 9: count.adb
1 function Count (P : Int_Array; V : Integer) return Natural is
2 Count : Natural := 0;
3 begin
4 for I in P'Range loop
5 if P (I) = V then
6 Count := Count + 1;
7 end if;
8 end loop;
9 return Count;
10 end Count;
5 procedure Test_Count is
6 P : constant Int_Array := (0, 3, 9, 3, 3);
7 C : constant Integer := Count (P, 3);
8 begin
9 Put_Line ("value 3 is seen" & C'Img & " times in p");
10 end Test_Count;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Arrays_Ada
MD5: 82e9d18d4b8ad8aa87ca8520bd7b830c
Runtime output
The array parameter P is not simply a homogeneous sequence of Integer values. The com-
piler must represent P so that its lower and upper bounds (P'First and P'Last) and thus
also its length (P'Length) can be retrieved. Function Count can simply loop over the range
of valid array indexes P'First .. P'Last (or P'Range for short). As a result, function
Count can be verified in isolation to be free of vulnerabilities such as buffer overflow, as
it does not depend on the values of parameters passed by its callers. In fact, we can go
further in SPARK and show that the value returned by Count is no greater than the length
of parameter P by stating this property in the postcondition of Count and asking the SPARK
analysis tool to prove it:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Arrays_Ada
MD5: 4c9a34614d53c4d268cbff787c9b73e6
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
count.adb:6:30: info: loop invariant preservation proved
count.adb:6:30: info: loop invariant initialization proved
count.adb:6:41: info: overflow check proved
count.adb:8:25: info: overflow check proved
count.ads:4:11: info: postcondition proved
count.ads:4:28: info: range check proved
The only help that SPARK analysis required from the programmer, in order to prove the
postcondition, is a loop invariant (a special kind of assertion) that reflects the value of
Count at each iteration.
The C language defines a special pointer type void* that corresponds to an untyped pointer.
It is legal to convert any pointer type to and from void*, which makes it a convenient
way to simulate C++ style templates. Consider the following code which indirectly applies
assign_int to integer i and assign_float to floating-point f by calling assign on both:
17 int main() {
18 int i;
19 float f;
20 assign((assign_fun)&assign_int, &i);
21 assign((assign_fun)&assign_float, &f);
22 printf("i = %d; f = %f\n", i, f);
23 }
Runtime output
i = 42; f = 42.000000
The references to the variables i and f are implicitly converted to the void* type as a
way to apply assign to any second parameter p whose type matches the argument type
of its first argument fun. The use of an untyped argument means that the responsibility
for the correct typing rests completely with the programmer. Swap i and f in the calls to
assign and you still get a compilable program without warnings, that runs and produces
completely bogus output:
i = 1109917696; f = 0.000000
i = 42; f = 42.000000
Generics in SPARK (and Ada) can implement the desired functionality in a fully typed way,
with any errors caught at compile time, where procedure Assign applies its parameter
procedure Initialize to its parameter V:
4 procedure Apply_Assign is
5 procedure Assign_Int (V : out Integer) is
6 begin
7 V := 42;
8 end Assign_Int;
9
18 I : Integer;
19 F : Float;
20 begin
21 Assign_I (I);
22 Assign_F (F);
23 Put_Line ("I =" & I'Img & "; F =" & F'Img);
24 end Apply_Assign;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Typed_Pointers_Ada
MD5: af23d6f8a742676139aac38a385c7bf7
Runtime output
I = 42; F = 4.20000E+01
The generic procedure Assign must be instantiated with a specific type for T and a specific
procedure (taking a single out parameter of this type) for Initialize. The procedure
resulting from the instantiation applies to a variable of this type. So switching I and F
above would result in an error detected by the compiler. Likewise, an instantiation such as
the following would also be a compile-time error:
In C, all scalar types can be converted both implicitly and explicitly to any other scalar type.
The semantics is defined by rules of promotion and conversion, which can confuse even
experts. One example was noted earlier, in the Preface (page 1609). Another example
appears in an article introducing a safe library for manipulating scalars343 by Microsoft
expert David LeBlanc. In its conclusion, the author acknowledges the inherent difficulty in
understanding scalar type conversions in C, by showing an early buggy version of the code
to produce the minimum signed integer:
The issue here is that the literal 1 on the left-hand side of the shift is an int, so on a 64-bit
machine with 32-bit int and 64-bit type T, the above is shifting 32-bit value 1 by 63 bits.
This is a case of undefined behavior, producing an unexpected output with the Microsoft
compiler. The correction is to convert the first literal 1 to T before the shift:
Although he'd asked some expert programmers to review the code, no one found this prob-
lem.
To avoid these issues as much as possible, MISRA C defines its own type system on top of
C types, in the section on "The essential type model" (eight rules). These can be seen as
additional typing rules, since all rules in this section are decidable, and can be enforced at
the level of a single translation unit. These rules forbid in particular the confusing cases
mentioned above. They can be divided into three sets of rules:
• restricting operations on types
• restricting explicit conversions
• restricting implicit conversions
Apart from the application of some operations to floating-point arguments (the bitwise,
mod and array access operations) which are invalid and reported by the compiler, all oper-
ations apply to all scalar types in C. MISRA C Rule 10.1 constrains the types on which each
operation is possible as follows.
343 https://msdn.microsoft.com/en-us/library/ms972705.aspx
Adding two Boolean values, or an Apple and an Orange, might sound like a bad idea, but it
is easily done in C:
4 int main() {
5 bool b1 = true;
6 bool b2 = false;
7 bool b3 = b1 + b2;
8
16 return 0;
17 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Pointer_Arith_C
MD5: 30e28b34f616f8e6d35233a4ce698c23
Runtime output
b3 = 1; f3 = 1
No error from the compiler here. In fact, there is no undefined behavior in the above code.
Variables b3 and f3 both end up with value 1. Of course it makes no sense to add Boolean or
enumerated values, and thus MISRA C Rule 18.1 forbids the use of all arithmetic operations
on Boolean and enumerated values, while also forbidding most arithmetic operations on
characters. That leaves the use of arithmetic operations for signed or unsigned integers
as well as floating-point types and the use of modulo operation % for signed or unsigned
integers.
Here's an attempt to simulate the above C code in SPARK (and Ada):
12 end Bad_Arith;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Pointer_Arith_Ada
MD5: 984381fdcf1a682e1998f7881c0532f9
Build output
3 procedure Ok_Arith is
4
14 begin
15 pragma Assert (B1 = B3);
16 pragma Assert (F1 = F3);
17
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Pointer_Arith_Ada
MD5: 6ad400913a48fd815845b6a99d90ec2d
Runtime output
FALSE
TRUE
APPLE
ORANGE
4 int main() {
5 typedef enum {Ape, Bee, Cat} Animal;
6 bool answer = (2 * Bee) || ! (2 * Bee);
7 printf("two bee or not two bee? %d\n", answer);
8 return 0;
9 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Boolean_C
MD5: a9d4886827c983df51c9285fe3fd6c77
Runtime output
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Boolean_Ada
MD5: 9089114f9cc6495dabd6957b54b33bd2
Build output
As expected, the compiler rejects this code. There is no available * operation that works
on an enumeration type, and likewise no available or or not operation.
Here's a genetic engineering example that combines a Bee with a Dog to produce a Cat,
by manipulating the atomic structure (the bits in its representation):
4 int main() {
5 typedef enum {Ape, Bee, Cat, Dog} Animal;
6 Animal mutant = Bee ^ Dog;
7 assert (mutant == Cat);
8 return 0;
9 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Bitwise_C
MD5: 645b0b6155f1cb17d02c7bcbb976993c
This algorithm works by accessing the underlying bitwise representation of Bee and Dog
(0x01 and 0x03, respectively) and, by applying the exclusive-or operator ^, transforming it
into the underlying bitwise representation of a Cat (0x02). While powerful, manipulating the
bits in the representation of values is best reserved for unsigned integers as illustrated in the
book Hacker's Delight344 . MISRA C Rule 18.1 thus forbids the use of all bitwise operations
on anything but unsigned integers.
Below is an attempt to do the same in SPARK (and Ada). The bitwise operators are
and, or, xor, and not, and the related bitwise functions are Shift_Left, Shift_Right,
Shift_Right_Arithmetic, Rotate_Left and Rotate_Right:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Bitwise_Ada
MD5: 3f7c3dd616f065016590d574200cf1db
Build output
The declaration of Mutant is illegal, since the xor operator is only available for Boolean
and unsigned integer (modular) values; it is not available for Animal. The same restriction
applies to the other bitwise operators listed above. If we really wanted to achieve the effect
of the above code in legal SPARK (or Ada), then the following approach will work (the type
Unsigned_8 is an 8-bit modular type declared in the predefined package Interfaces).
344 http://www.hackersdelight.org/
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Bitwise_Ada_2
MD5: 359439d40740fe2d99e6f334ed3500f9
Prover output
Note that and, or, not and xor are used both as logical operators and as bitwise operators,
but there is no possible confusion between these two uses. Indeed the use of such operators
on values from modular types is a natural generalization of their uses on Boolean, since
values from modular types are often interpreted as arrays of Booleans.
A simple way to bypass the restrictions of Rule 10.1 is to explicitly convert the arguments
of an operation to a type that the rule allows. While it can often be useful to cast a value
from one type to another, many casts that are allowed in C are either downright errors or
poor replacements for clearer syntax.
One example is to cast from a scalar type to Boolean. A better way to express (bool)x is
to compare x to the zero value of its type: x != 0 for integers, x != 0.0 for floats, x !=
'0' for characters, x != Enum where Enum is the first enumerated value of the type. Thus,
MISRA C Rule 10.5 advises avoiding casting non-Boolean values to Boolean.
Rule 10.5 also advises avoiding other casts that are, at best, obscure:
• from a Boolean to any other scalar type
• from a floating-point value to an enumeration or a character
• from any scalar type to an enumeration
The rules are not symmetric, so although a float should not be cast to an enum, casting an
enum to a float is allowed. Similarly, although it is advised to not cast a character to an
enum, casting an enum to a character is allowed.
The rules in SPARK are simpler. There are no conversions between numeric types (integers,
fixed-point and floating-point) and non-numeric types (such as Boolean, Character, and
other enumeration types). Conversions between different non-numeric types are limited
to those that make semantic sense, for example between a derived type and its parent
type. Any numeric type can be converted to any other numeric type, with precise rules for
rounding/truncating values when needed and run-time checking that the converted value
is in the range associated with the target type.
Rules 10.1 and 10.5 restrict operations on types and explicit conversions. That's not enough
to avoid problematic C programs; a program violating one of these rules can be expressed
using only implicit type conversions. For example, the Shakespearian code in section
Boolean Operations on Boolean (page 1642) can be reformulated to satisfy both Rules 10.1
and 10.5:
4 int main() {
5 typedef enum {Ape, Bee, Cat} Animal;
6 int b = Bee;
7 bool t = 2 * b;
8 bool answer = t || ! t;
9 printf("two bee or not two bee? %d\n", answer);
10 return 0;
11 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Strong_Typing.Implicit_Conversion_C
MD5: a157dd05c5fe8926886361b533305e14
Runtime output
Here, we're implicitly converting the enumerated value Bee to an int, and then implicitly
converting the integer value 2 * b to a Boolean. This does not violate 10.1 or 10.5, but it
is prohibited by MISRA C Rule 10.3: "The value of an expression shall not be assigned to an
object with a narrower essential type or of a different essential type category".
Rule 10.1 also does not prevent arguments of an operation from being inconsistent, for
example comparing a floating-point value and an enumerated value. But MISRA C Rule
10.4 handles this situation: "Both operands of an operator in which the usual arithmetic
conversions are performed shall have the same essential type category".
In addition, three rules in the "Composite operators and expressions" section avoid common
mistakes related to the combination of explicit/implicit conversions and operations.
The rules in SPARK (and Ada) are far simpler: there are no implicit conversions! This applies
both between types of a different essential type category as MISRA C puts it, as well as
between types that are structurally the same but declared as different types.
MD5: f10b50048595df0b4ed77c06a7508412
Build output
bad_conversions.adb:12:09: error: expected type "Standard.Float"
bad_conversions.adb:12:09: error: found type "Standard.Integer"
bad_conversions.adb:13:09: error: expected type "Standard.Integer"
bad_conversions.adb:13:09: error: found type "Animal" defined at line 5
bad_conversions.adb:14:09: error: expected type "Animal" defined at line 5
bad_conversions.adb:14:09: error: found type "Standard.Boolean"
bad_conversions.adb:15:09: error: expected type "My_Animal" defined at line 6
bad_conversions.adb:15:09: error: found type "Animal" defined at line 5
bad_conversions.adb:16:09: error: expected type "Standard.Boolean"
bad_conversions.adb:16:09: error: found type "Standard.Character"
bad_conversions.adb:17:09: error: expected type "Standard.Character"
bad_conversions.adb:17:09: error: found type "Standard.Float"
gprbuild: *** compilation phase failed
The compiler reports a mismatch on every statement in the above procedure (the declara-
tions are all legal).
Adding explicit conversions makes the assignments to F and M valid, since SPARK (and Ada)
allow conversions between numeric types and between a derived type and its parent type,
but all other conversions are illegal:
MD5: 4d3f6a8629d51f27b6628dae5fc7b680
Build output
Although an enumeration value cannot be converted to an integer (or vice versa) either
implicitly or explicitly, SPARK (and Ada) provide functions to obtain the effect of a type
conversion. For any enumeration type T, the function T'Pos(e) takes an enumeration
value from type T and returns its relative position as an integer, starting at 0. For example,
Animal'Pos(Bee) is 1, and Boolean'Pos(False) is 0. In the other direction, T'Val(n),
where n is an integer, returns the enumeration value in type T at relative position n. If n is
negative or greater then T'Pos(T'Last) then a run-time exception is raised.
Hence, the following is valid SPARK (and Ada) code; Character is defined as an enumeration
type:
EIGHTYONE
As with most programming languages, C does not require that variables be initialized at
their declaration, which makes it possible to unintentionally read uninitialized data. This is
a case of undefined behavior, which can sometimes be used to attack the program.
Listing 1: f.h
1 #include <stdint.h>
2
Listing 2: f.c
1 #include "f.h"
2
Listing 3: g.c
1 #include <stdint.h>
2 #include "f.h"
3
8 f ( 0, &u );
9
10 if ( u == 3U )
(continues on next page)
1649
Learning Ada
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Read_Uninitialized_Data_C
MD5: f36430141f48b34810d53a43294c7d74
Detecting the violation of Rule 9.1 can be arbitrarily complex, as the program points corre-
sponding to a variable's initialization and read can be separated by many calls and condi-
tions. This is one of the undecidable rules, for which most MISRA C checkers won't detect
all violations.
In SPARK, the guarantee that all reads are to initialized data is enforced by the SPARK
analysis tool, GNATprove, through what is referred to as flow analysis. Every subprogram
is analyzed separately to check that it cannot read uninitialized data. To make this modular
analysis possible, SPARK programs need to respect the following constraints:
• all inputs of a subprogram should be initialized on subprogram entry
• all outputs of a subprogram should be initialized on subprogram return
Hence, given the following code translated from C, GNATprove reports that function F might
not always initialize output parameter P:
Listing 4: init.ads
1 with Interfaces; use Interfaces;
2
3 package Init is
4 procedure F (B : Boolean; P : out Unsigned_16);
5 procedure G;
6 end Init;
Listing 5: init.adb
1 package body Init is
2
10 procedure G is
11 U : Unsigned_16;
12 begin
13 F (False, U);
14
15 if U = 3 then
16 null;
17 end if;
18 end G;
19
20 end Init;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Read_Uninitialized_Data_
↪Ada
MD5: d54bc9901b3bff4f0cfea9942a795156
Prover output
We can correct the program by initializing P to value 0 when condition B is not satisfied:
Listing 6: init.ads
1 with Interfaces; use Interfaces;
2
3 package Init is
4 procedure F (B : Boolean; P : out Unsigned_16);
5 procedure G;
6 end Init;
Listing 7: init.adb
1 package body Init is
2
12 procedure G is
13 U : Unsigned_16;
14 begin
15 F (False, U);
16
17 if U = 3 then
18 null;
19 end if;
20 end G;
21
22 end Init;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Read_Uninitialized_Data_
↪Ada
MD5: 481787c333014d56814a7205720f72bc
Prover output
GNATprove now does not report any possible reads of uninitialized data. On the contrary,
it confirms that all reads are made from initialized data.
In contrast with C, SPARK does not guarantee that global data (called library-level data in
SPARK and Ada) is zero-initialized at program startup. Instead, GNATprove checks that all
global data is explicitly initialized (at declaration or elsewhere) before it is read. Hence it
goes beyond the MISRA C Rule 9.1, which considers global data as always initialized even if
the default value of all-zeros might not be valid data for the application. Here's a variation
of the above code where variable U is now global:
Listing 8: init.ads
1 with Interfaces; use Interfaces;
2
3 package Init is
4 U : Unsigned_16;
5 procedure F (B : Boolean);
6 procedure G;
7 end Init;
Listing 9: init.adb
1 package body Init is
2
3 procedure F (B : Boolean) is
4 begin
5 if B then
6 U := 3;
7 end if;
8 end F;
9
10 procedure G is
11 begin
12 F (False);
13
14 if U = 3 then
15 null;
16 end if;
17 end G;
18
19 end Init;
3 procedure Call_Init is
4 begin
5 Init.G;
6 end Call_Init;
MD5: a85cde45a658727975367b041a1a5dc3
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
call_init.adb:5:08: medium: "U" might not be initialized after elaboration of main␣
(continues on next page)
GNATprove reports here that variable U might not be initialized at program startup, which
is indeed the case here. It reports this issue on the main program Call_Init because its
analysis showed that F needs to take U as an initialized input (since F is not initializing U on
all paths, U keeps its value on the other path, which needs to be an initialized value), which
means that G which calls F also needs to take U as an initialized input, which in turn means
that Call_Init which calls G also needs to take U as an initialized input. At this point,
we've reached the main program, so the initialization phase (referred to as elaboration in
SPARK and Ada) should have taken care of initializing U. This is not the case here, hence
the message from GNATprove.
It is possible in SPARK to specify that G should initialize variable U; this is done with a data
dependency contract introduced with aspect Global following the declaration of procedure
G:
3 package Init is
4 U : Unsigned_16;
5 procedure F (B : Boolean);
6 procedure G with Global => (Output => U);
7 end Init;
3 procedure F (B : Boolean) is
4 begin
5 if B then
6 U := 3;
7 end if;
8 end F;
9
10 procedure G is
11 begin
12 F (False);
13
14 if U = 3 then
15 null;
16 end if;
17 end G;
18
19 end Init;
3 procedure Call_Init is
4 begin
5 Init.G;
6 end Call_Init;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Read_Uninitialized_Data_
↪Ada
MD5: 100122ca3c8c60c134822a85d564a60a
Prover output
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: analysis of data and information flow ...
init.adb:12:07: high: "U" is not initialized
init.adb:12:07: high: "U" is not an input in the Global contract of subprogram "G"␣
↪at init.ads:6
init.adb:12:07: high: either make "U" an input in the Global contract or␣
↪initialize it before use
GNATprove reports the error on the call to F in G, as it knows at this point that F needs U
to be initialized but the calling context in G cannot provide that guarantee. If we provide
the same data dependency contract for F, then GNATprove reports the error on F itself,
similarly to what we saw for an output parameter U.
The other rules in the section on "Initialization" deal with common errors in initializing ag-
gregates and designated initializers in C99 to initialize a structure or array at declaration.
These rules attempt to patch holes created by the lax syntax and rules in C standard. For
example, here are five valid initializations of an array of 10 elements in C:
Only a is fully initialized to all-zeros in the above code snippet. MISRA C Rule 9.3 thus forbids
all other declarations by stating that "Arrays shall not be partially initialized". In addition,
MISRA C Rule 9.4 forbids the declaration of e by stating that "An element of an object shall
not be initialised more than once" (in e's declaration, the element at index 8 is initialized
twice).
The same holds for initialization of structures. Here is an equivalent set of declarations with
the same potential issues:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Redundant_Init
MD5: e562ef70b8c8a170d2bd09281cf2a075
Here only a, d and e are fully initialized. MISRA C Rule 9.3 thus forbids the declarations of
b and c. In addition, MISRA C Rule 9.4 forbids the declaration of e.
In SPARK and Ada, the aggregate used to initialize an array or a record must fully cover the
components of the array or record. Violations lead to compilation errors, both for records:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Init_Record_1
MD5: 6b28bffe6270c5ea5055123c5b89c508
Build output
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Init_Array_1
MD5: 81aa6363ba770ded10bef8d3d8776914
Build output
init_array.ads:3:15: warning: too few elements for type "Arr" defined at line 2␣
↪[enabled by default]
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Init_Record_2
MD5: 07d3f790009be97cef2daaf08b2f7afd
Build output
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Init_Array_2
MD5: 12f5fa4615abccde43f63f72340fd4a0
Build output
init_array.ads:3:43: error: 7
gprbuild: *** compilation phase failed
Finally, while it is legal in Ada to leave uninitialized parts in a record or array aggregate
by using the box notation (meaning that the default initialization of the type is used, which
may be no initialization at all), SPARK analysis rejects such use when it leads to components
not being initialized, both for records:
Project: Courses.SPARK_For_The_MISRA_C_Dev.Initialization.Init_Record_3
MD5: a7736f2b563c39fb4ab10007e927ad97
Prover output
↪Record_3/a7736f2b563c39fb4ab10007e927ad97/main_spark.adc:12
↪Record_3/a7736f2b563c39fb4ab10007e927ad97/main_spark.adc:12
EIGHTYTWO
As with most programming languages, C allows side effects in expressions. This leads to
subtle issues about conflicting side effects, when subexpressions of the same expression
read/write the same variable.
Conflicting side effects are a kind of undefined behavior; the C Standard (section 6.5) de-
fines the concept as follows:
"Between two sequence points, an object is modified more than once, or is mod-
ified and the prior value is read other than to determine the value to be stored"
This legalistic wording is somewhat opaque, but the notion of sequence points is summa-
rized in Annex C of the C90 and C99 standards. MISRA C repeats these conditions in the
Amplification of Rule 13.2, including the read of a volatile variable as a side effect similar
to writing a variable.
This rule is undecidable, so MISRA C completes it with two rules that provide simpler restric-
tions preventing some side effects in expressions, thus reducing the potential for undefined
behavior:
• Rule 13.3: "A full expression containing an increment (++) or decrement (--) operator
should have no other potential side effects other than that caused by the increment
or decrement operator".
• Rule 13.4: "The result of an assignment operator should not be used".
In practice, conflicting side effects usually manifest themselves as portability issues, since
the result of the evaluation of an expression depends on the order in which a compiler
decides to evaluate its subexpressions. So changing the compiler version or the target
platform might lead to a different behavior of the application.
To reduce the dependency on evaluation order, MISRA C Rule 13.1 states: "Initializer lists
shall not contain persistent side effects". This case is theoretically different from the previ-
ously mentioned conflicting side effects, because initializers that comprise an initializer list
are separated by sequence points, so there is no risk of undefined behavior if two initializ-
ers have conflicting side effects. But given that initializers are executed in an unspecified
order, the result of a conflict is potentially as damaging for the application.
1659
Learning Ada
Even in cases with no undefined or unspecified behavior, expressions with multiple side
effects can be confusing to programmers reading or maintaining the code. This problem
arises in particular with C's increment and decrement operators that can be applied prior
to or after the expression evaluation, and with the assignment operator = in C since it can
easily be mistaken for equality. Thus MISRA C forbids the use of the increment / decrement
(Rule 13.3) and assignment (Rule 13.4) operators in expressions that have other potential
side effects.
In other cases, the presence of expressions with side effects might be confusing, if the
programmer wrongly thinks that the side effects are guaranteed to occur. Consider the
function decrease_until_one_is_null below, which decreases both arguments until one
is null:
Listing 1: main.c
1 #include <stdio.h>
2
12 int main() {
13 int x = 42, y = 42;
14 decrease_until_one_is_null (&x, &y);
15 printf("x = %d, y = %d\n", x, y);
16 return 0;
17 }
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.Side_Effect_C
MD5: a3e991881894bc3fb25a5f49a083fd2e
Runtime output
x = 0, y = 1
x = 0, y = 1
I.e., starting from the same value 42 for both x and y, only x has reached the value zero
after decrease_until_one_is_null returns. The reason is that the side effect on y is
performed only conditionally. To avoid such surprises, MISRA C Rule 13.5 states: "The right
hand operand of a logical && or || operator shall not contain persistent side effects"; this
rule forbids the code above.
MISRA C Rule 13.6 similarly states: "The operand of the sizeof operator shall not contain any
expression which has potential side effects". Indeed, the operand of sizeof is evaluated
only in rare situations, and only according to C99 rules, which makes any side effect in such
an operand a likely mistake.
In SPARK, expressions cannot have side effects; only statements can. In particular, there
are no increment/decrement operators, and no assignment operator. There is instead an
assignment statement, whose syntax using := clearly distinguishes it from equality (using
=). And in any event an expression is not allowed as a statement and this a construct such
as X = Y; would be illegal. Here is how a variable X can be assigned, incremented and
decremented:
X := 1;
X := X + 1;
X := X - 1;
Listing 2: volatile_read.ads
1 package Volatile_Read is
2 X : Integer with Volatile;
3 procedure P (Y : out Integer);
4 end Volatile_Read;
Listing 3: volatile_read.adb
1 package body Volatile_Read is
2 procedure P (Y : out Integer) is
3 begin
4 Y := X - X; -- ERROR
5 end P;
6 end Volatile_Read;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.Volatile_Read_1
MD5: 7ec58b4d1432d03d60b5ea6019cc031e
Prover output
Instead, every read of a volatile variable must occur immediately before being assigned to
another variable, as follows:
Listing 4: volatile_read.ads
1 package Volatile_Read is
2 X : Integer with Volatile;
3 procedure P (Y : out Integer);
4 end Volatile_Read;
Listing 5: volatile_read.adb
1 package body Volatile_Read is
2 procedure P (Y : out Integer) is
3 X1 : constant Integer := X;
4 X2 : constant Integer := X;
5 begin
6 Y := X1 - X2;
7 end P;
8 end Volatile_Read;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.Volatile_Read_2
MD5: 1224af597a12a8ca77b96976c76b422f
Prover output
Note here that the order of capture of the volatile value of X might be significant. For
example, X might denote a quantity which only increases, like clock time, so that the above
expression X1 - X2 would always be negative or zero.
Even more significantly, functions in SPARK cannot have side effects; only procedures can.
The only effect of a SPARK function is the computation of a result from its inputs, which
may be passed as parameters or as global variables. In particular, SPARK functions cannot
have out or in out parameters:
Listing 6: bad_function.ads
1 function Bad_Function (X, Y : Integer; Sum, Max : out Integer) return Boolean;
2 -- ERROR, since "out" parameters are not allowed
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.Function_With_Out_Param
MD5: 204dd22df61fe15208ae34ebc3828974
Prover output
↪With_Out_Param/204dd22df61fe15208ae34ebc3828974/main_spark.adc:12
More generally, SPARK does not allow functions that have a side effect in addition to return-
ing their result, as is typical of many idioms in other languages, for example when setting
a new value and returning the previous one:
Listing 7: bad_functions.ads
1 package Bad_Functions is
2 function Set (V : Integer) return Integer;
3 function Get return Integer;
4 end Bad_Functions;
Listing 8: bad_functions.adb
1 package body Bad_Functions is
2
3 Value : Integer := 0;
4
14 end Bad_Functions;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.Side_Effect_Ada
MD5: 3337b6025c4996e7fa8c7e27b4df42c1
Prover output
GNATprove detects that function Set has a side effect on global variable Value and issues
an error. The correct idiom in SPARK for such a case is to use a procedure with an out
parameter to return the desired result:
Listing 9: ok_subprograms.ads
1 package Ok_Subprograms is
2 procedure Set (V : Integer; Prev : out Integer);
3 function Get return Integer;
4 end Ok_Subprograms;
3 Value : Integer := 0;
4
13 end Ok_Subprograms;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Side_Effect.No_Side_Effect_Ada
MD5: 04e2235b8b6a01706434d35f6636674c
Prover output
With the above restrictions in SPARK, none of the conflicts of side effects that can occur in
C can occur in SPARK, and this is guaranteed by flow analysis.
EIGHTYTHREE
Undefined behavior (and critical unspecified behavior, which we'll treat as undefined behav-
ior) are the plague of C programs. Many rules in MISRA C are designed to avoid undefined
behavior, as evidenced by the twenty occurrences of "undefined" in the MISRA C:2012 doc-
ument.
MISRA C Rule 1.3 is the overarching rule, stating very simply:
"There shall be no occurrence of undefined or critical unspecified behaviour."
The deceptive simplicity of this rule rests on the definition of undefined or critical unspec-
ified behaviour. Appendix H of MISRA:C 2012 lists hundreds of cases of undefined and
critical unspecified behavior in the C programming language standard, a majority of which
are not individually decidable.
It is therefore not surprising that a majority of MISRA C checkers do not make a serious
attempt to verify compliance with MISRA C Rule 1.3.
Since SPARK is a subset of the Ada programming language, SPARK programs may exhibit
two types of undefined behaviors that can occur in Ada:
• bounded error: when the program enters a state not defined by the language seman-
tics, but the consequences are bounded in various ways. For example, reading unini-
tialized data can lead to a bounded error, when the value read does not correspond to
a valid value for the type of the object. In this specific case, the Ada Reference Manual
states that either a predefined exception is raised or execution continues using the
invalid representation.
• erroneous execution: when when the program enters a state not defined by the lan-
guage semantics, but the consequences are not bounded by the Ada Reference Man-
ual. This is the closest to an undefined behavior in C. For example, concurrently writing
through different tasks to the same unprotected variable is a case of erroneous exe-
cution.
Many cases of undefined behavior in C would in fact raise exceptions in SPARK. For example,
accessing an array beyond its bounds raises the exception Constraint_Error while reach-
ing the end of a function without returning a value raises the exception Program_Error.
The SPARK Reference Manual defines the SPARK subset through a combination of legality
rules (checked by the compiler, or the compiler-like phase preceding analysis) and verifica-
tion rules (checked by the formal analysis tool GNATprove). Bounded errors and erroneous
execution are prevented by a combination of legality rules and the flow analysis part of
GNATprove, which in particular detects potential reads of uninitialized data, as described
in Detecting Reads of Uninitialized Data (page 1649). The following discussion focuses on
how SPARK can verify that no exceptions can be raised.
1665
Learning Ada
The most common run-time errors are related to misuse of arithmetic (division by zero, over-
flows, exceeding the range of allowed values), arrays (accessing beyond an array bounds,
assigning between arrays of different lengths), and structures (accessing components that
are not defined for a given variant).
Arithmetic run-time errors can occur with signed integers, unsigned integers, fixed-point
and floating-point (although with IEEE 754 floating-point arithmetic, errors are manifest as
special run-time values such as NaN and infinities rather than as exceptions that are raised).
These errors can occur when applying arithmetic operations or when converting between
numeric types (if the value of the expression being converted is outside the range of the
type to which it is being converted).
Operations on enumeration values can also lead to run-time errors; e.g., T'Pred(T'First)
or T'Succ(T'Last) for an enumeration type T, or T'Val(N) where N is an integer value
that is outside the range 0 .. T'Pos(T'Last).
The Update procedure below contains what appears to be a simple assignment statement,
which sets the value of array element A(I+J) to P/Q.
Listing 1: show_runtime_errors.ads
1 package Show_Runtime_Errors is
2
8 end Show_Runtime_Errors;
Listing 2: show_runtime_errors.adb
1 package body Show_Runtime_Errors is
2
8 end Show_Runtime_Errors;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Undefined_Behavior.Runtime_Errors
MD5: 8ad4488974ab9e49ac17bf094ae33eac
Prover output
↪bits machine integer] [possible fix: add precondition (if J >= 0 then I <=␣
↪errors.ads:6]
↪add precondition (if J >= 0 then I <= A'Last - J else I >= A'First - J) to␣
↪subprogram at show_runtime_errors.ads:6]
↪subprogram at show_runtime_errors.ads:6]
↪subprogram at show_runtime_errors.ads:6]
• The division P / Q may overflow in the special case where P is Integer'First and Q
is -1, because of the asymmetric range of signed integer types.
A (I + J) := Integer'First / -1;
• Since the array can only contain non-negative numbers (the element subtype is Nat-
ural), it is also an error to store a negative value in it.
A (I + J) := 1 / -1;
For each of these potential run-time errors, the compiler will generate checks in the exe-
cutable code, raising an exception if any of the checks fail:
A (Integer'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : overflow check failed
A (A'Last + 1) := P / Q;
-- raised CONSTRAINT_ERROR : index check failed
A (I + J) := Integer'First / (-1);
-- raised CONSTRAINT_ERROR : overflow check failed
A (I + J) := 1 / (-1);
-- raised CONSTRAINT_ERROR : range check failed
A (I + J) := P / 0;
-- raised CONSTRAINT_ERROR : divide by zero
These run-time checks incur an overhead in program size and execution time. Therefore it
may be appropriate to remove them if we are confident that they are not needed.
The traditional way to obtain the needed confidence is through testing, but it is well known
that this can never be complete, at least for non-trivial programs. Much better is to guaran-
tee the absence of run-time errors through sound static analysis, and that's where SPARK
and GNATprove can help.
More precisely, GNATprove logically interprets the meaning of every instruction in the pro-
gram, taking into account both control flow and data/information dependencies. It uses
this analysis to generate a logical formula called a verification condition for each possible
check.
A (Integer'Last + 1) := P / Q;
-- medium: overflow check might fail
A (A'Last + 1) := P / Q;
-- medium: array index check might fail
A (I + J) := Integer'First / (-1);
-- medium: overflow check might fail
A (I + J) := 1 / (-1);
-- medium: range check might fail
A (I + J) := P / 0;
-- medium: divide by zero might fail
The verification conditions are then given to an automatic prover. If every verification con-
dition can be proved, then no run-time errors will occur.
GNATprove's analysis is sound — it will detect all possible instances of run-time exceptions
being raised — while also having high precision (i.e., not producing a cascade of "false
alarms").
The way to program in SPARK so that GNATprove can guarantee the absence of run-time
errors entails:
• declaring variables with precise constraints, and in particular to specify precise ranges
for scalars; and
• defining preconditions and postconditions on subprograms, to specify respectively the
constraints that callers should respect and the guarantees that the subprogram should
provide on exit.
For example, here is a revised version of the previous example, which can guarantee
through proof that no possible run-time error can be raised:
Listing 3: no_runtime_errors.ads
1 package No_Runtime_Errors is
2
13 end No_Runtime_Errors;
Listing 4: no_runtime_errors.adb
1 package body No_Runtime_Errors is
2
10 end No_Runtime_Errors;
EIGHTYFOUR
MISRA C defines unreachable code as code that cannot be executed, and it defines dead
code as code that can be executed but has no effect on the functional behavior of the
program. (These definitions differ from traditional terminology, which refers to the first
category as "dead code" and the second category as "useless code".) Regardless of the
terminology, however, both types are actively harmful, as they might confuse programmers
and lead to errors during maintenance.
The "Unused code" section of MISRA C contains seven rules that deal with detecting both
unreachable code and dead code. The two most important rules are:
• Rule 2.1: "A project shall not contain unreachable code", and
• Rule 2.2: "There shall not be dead code".
Other rules in the same section prohibit unused entities of various kinds (type declarations,
tag declarations, macro declarations, label declarations, function parameters).
While some simple cases of unreachable code can be detected by static analysis (typically
if a condition in an if statement can be determined to be always true or false), most cases
of unreachable code can only be detected by performing coverage analysis in testing, with
the caveat that code reported as not being executed is not necessarily unreachable (it
could simply reflect gaps in the test suite). Note that statement coverage, rather than the
more comprehensive decision coverage or modified condition / decision coverage (MC/DC)
as defined in the DO-178C standard for airborne software, is sufficient to detect poten-
tial unreachable statements, corresponding to code that is not covered during the testing
campaign.
The presence of dead code is much harder to detect, both statically and dynamically, as it
requires creating a complete dependency graph linking statements in the code and their
effect on visible behavior of the program.
SPARK can detect some cases of both unreachable and dead code through its precise con-
struction of a dependency graph linking a subprogram's statements to all its inputs and
outputs. This analysis might not be able to detect complex cases, but it goes well beyond
what other analyses do in general.
Listing 1: much_ado_about_little.ads
1 procedure Much_Ado_About_Little (X, Y, Z : Integer; Success : out Boolean);
Listing 2: much_ado_about_little.adb
1 procedure Much_Ado_About_Little (X, Y, Z : Integer; Success : out Boolean) is
2
3 procedure Ok is
4 begin
5 Success := True;
6 end Ok;
7
1671
Learning Ada
13 begin
14 Success := False;
15
16 for K in Y .. Z loop
17 if K < X and not Success then
18 Ok;
19 end if;
20 end loop;
21
22 if X > Y then
23 Ok;
24 else
25 NOk;
26 end if;
27
28 if Z > Y then
29 NOk;
30 return;
31 else
32 Ok;
33 return;
34 end if;
35
36 if Success then
37 Success := not Success;
38 end if;
39 end Much_Ado_About_Little;
Project: Courses.SPARK_For_The_MISRA_C_Dev.Unreachable_And_Dead_Code.Much_Ado_
↪About_Little
MD5: ccccb112fbab169ba964b3f8ef36ec2d
Build output
Prover output
The only code in the body of Much_Ado_About_Little that affects the result of the pro-
cedure's execution is the if Z > Y... statement, since this statement sets Success to
either True or False regardless of what the previous statements did. I.e., the statements
preceding this if are dead code in the MISRA C sense. Since both branches of the if Z
> Y... statement return from the procedure, the subsequent if Success... statement is
unreachable. GNATprove detects and issues warnings about both the dead code and the
unreachable code.
1673
Learning Ada
EIGHTYFIVE
CONCLUSION
The C programming language is "close to the metal" and has emerged as a lingua franca
for the majority of embedded platforms of all sizes. However, its software engineering
deficiencies (such as the absence of data encapsulation) and its many traps and pitfalls
present major obstacles to those developing critical applications. To some extent, it is
possible to put the blame for programming errors on programmers themselves, as Linus
Torvalds admonished:
"Learn C, instead of just stringing random characters together until it compiles
(with warnings)."
But programmers are human, and even the best would be hard pressed to be 100% cor-
rect about the myriad of semantic details such as those discussed in this document. Pro-
gramming language abstractions have been invented precisely to help developers focus
on the "big picture" (thinking in terms of problem-oriented concepts) rather than low-level
machine-oriented details, but C lacks these abstractions. As Kees Cook from the Kernel Self
Protection Project puts it (during the Linux Security Summit North America 2018):
"Talking about C as a language, and how it's really just a fancy assembler"
Even experts sometimes have problems with the C programming language rules, as il-
lustrated by Microsoft expert David LeBlanc (see Enforcing Strong Typing for Scalars
(page 1639)) or the MISRA C Committee itself (see the Preface (page 1609)).
The rules in MISRA C represent an impressive collective effort to improve the reliability of
C code in critical applications, with a focus on avoiding error-prone features rather than
enforcing a particular programming style. The Rationale provided with each rule is a clear
and unobjectionable justification of the rule's benefit.
At a fundamental level, however, MISRA C is still built on a base language that was not
really designed with the goal of supporting large high-assurance applications. As shown
in this document, there are limits to what static analysis can enforce with respect to the
MISRA C rules. It's hard to retrofit reliability, safety and security into a language that did
not have these as goals from the start.
The SPARK language took a different approach, starting from a base language (Ada) that
was designed from the outset to support solid software engineering, and eliminating fea-
tures that were implementation dependent or otherwise hard to formally analyze. In this
document we have shown how the SPARK programming language and its associated formal
verification tools can contribute usefully to the goal of producing error-free software, going
beyond the guarantees that can be achieved in MISRA C.
1675
Learning Ada
EIGHTYSIX
REFERENCES
The official website of the MISRA association https://www.misra.org.uk/ has many freely
available resources about MISRA C, some of which can be downloaded after registering on
the MISRA Bulletin Board at https://www.misra.org.uk/forum/ (such as the examples from
the MISRA C:2012 standard, which includes a one-line description of each guideline).
The following documents are freely available:
• MISRA Compliance 2016: Achieving compliance with MISRA coding guidelines, 2016,
which explains the rationale and process for compliance, including a thorough discus-
sions of acceptable deviations
• MISRA C:2012 - Amendment 1: Additional security guidelines for MISRA C:2012, 2016,
which contains 14 additional guidelines focusing on security. This is a minor addition
to MISRA C.
The main MISRA C:2012 document can be purchased from the MISRA webstore.
PRQA is the company that first developed MISRA C, and they have been heavily involved
in every version since then. Their webpage http://www.prqa.com/coding-standards/misra/
contains many resources about MISRA C: product datasheets, white papers, webinars, pro-
fessional courses.
The PRQA Resources Library at http://info.prqa.com/resources-library?filter=white_paper
has some freely available white papers on MISRA C and the use of static analyzers:
• An introduction to MISRA C:2012 at http://info.prqa.com/MISRA C-2012-whitepaper-
evaluation-lp
• The Myth of Perfect MISRA Compliance at http://info.prqa.com/myth-of-perfect-MISRA
Compliance-evaluation-lp, providing background information on the use and limita-
tions of static analyzers for checking MISRA C compliance
In 2013 ISO standardized a set of 45 rules focused on security, available in the C Secure
Coding Rules. A draft is freely available at http://www.open-std.org/jtc1/sc22/wg14/www/
docs/n1624.pdf
In 2018 MISRA published MISRA C:2012 - Addendum 2: Coverage of MISRA C:2012 against
ISO/IEC TS 17961:2013 "C Secure", mapping ISO rules to MISRA C:2012 guidelines. This
document is freely available from https://www.misra.org.uk/.
1677
Learning Ada
1679
Learning Ada
This course presents an introduction to the GNAT toolchain. The course includes first steps
to get started with the toolchain and some details on the project manager (GPRbuild) and
the integrated development environment (GNAT Studio).
This document was written by Gustavo A. Hoffmann, with contributions and review from
Richard Kenner and Robert Duff.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
345 http://creativecommons.org/licenses/by-sa/4.0
1681
Learning Ada
1682
CHAPTER
EIGHTYSEVEN
This chapter presents a couple of basic commands from the GNAT toolchain.
Now that the toolchain is installed, you can start using it. From the command line, you can
compile a project using gprbuild. For example:
gprbuild -P project.gpr
You can find the binary built with the command above in the obj directory. You can the run it
in the same way as you would do with any other executable on your platform. For example:
obj/main
A handy command-line option for gprbuild you might want to use is -p, which automati-
cally creates directories such as obj if they aren't in the directory tree:
gprbuild -p -P project.gpr
Ada source-code are stored in .ads and .adb files. To view the content of these files, you
can use GNAT Studio. To open GNAT Studio, double-click on the .gpr project file or invoke
GNAT Studio on the command line:
gps -P project.gpr
To compile your project using GNAT Studio, use the top-level menu to invoke Build →
Project → main.adb (or press the keyboard shortcut F4). To run the main program, click
on Build → Run → main (or press the keyboard shortcut Shift + F2).
One of the strengths of the GNAT compiler is its ability to generate many useful warnings.
Some are displayed by default but others need to be explicitly enabled. In this section, we
discuss some of these warnings, their purpose, and how you activate them.
1683
Learning Ada
end Warnings_Example;
end Warnings_Example;
end Warnings_Example;
end Warnings_Example;
procedure Main is
X : Integer := 0;
Success : Boolean;
begin
Process (X, Success);
Put_Line (Integer'Image (X));
end Main;
If we build the main application using the -gnatw.o switch, the compiler warns us that we
didn't reference the Success variable, which was modified in the call to Process:
main.adb:8:16: warning: "Success" modified by call, but value might not be␣
↪referenced
In this case, this actually points us to a bug in our program, since X only contains a valid
value if Success is True. The corrected code for Main is:
346 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/building_executable_programs_with_
gnat.html#warning-message-control
-- ...
begin
Process (X, Success);
if Success then
Put_Line (Integer'Image (X));
else
Put_Line ("Couldn't process variable X.");
end if;
end Main;
We suggest turning on as many warnings as makes sense for your project. Then, when
you see a warning message, look at the code and decide if it's real. If it is, fix the code. If
it's a false alarm, suppress the warning. In either case, we strongly recommend you make
the warning disappear before you check your code into your configuration management
system.
GNAT provides many options to configure style checking of your code. The main compiler
switch for this is -gnatyy, which sets almost all standard style check options. As indicated
by the section on style checking347 of the GNAT User's Guide, using this switch "is equiva-
lent to -gnaty3aAbcefhiklmnprst, that is all checking options enabled with the exception
of -gnatyB, -gnatyd, -gnatyI, -gnatyLnnn, -gnatyo, -gnatyO, -gnatyS, -gnatyu, and
-gnatyx."
You may find that selecting the appropriate coding style is useful to detect issues at early
stages. For example, the -gnatyO switch checks that overriding subprograms are explicitly
marked as such. Using this switch can avoid surprises when you didn't intentionally want
to override an operation for some data type. We recommend studying the list of coding
style switches and selecting the ones that seem relevant for your project. When in doubt,
you can start by using all of them — using -gnatyy and -gnatyBdIL4oOSux, for example —
and deactivating the ones that cause too much noise during compilation.
347 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/building_executable_programs_with_
gnat.html#style-checking
EIGHTYEIGHT
GPRBUILD
This chapter presents a brief overview of GPRbuild, the project manager of the GNAT
toolchain. It can be used to manage complex builds. In terms of functionality, it's simi-
lar to make and cmake, just to name two examples.
For a detailed presentation of the tool, please refer to the GPRbuild User’s Guide348 .
As mentioned in the previous chapter, you can build a project using gprbuild from the
command line:
gprbuild -P project.gpr
gprclean -P project.gpr
You can create project files using GNAT Studio, which presents many options on its graphical
interface. However, you can also edit project files manually as a normal text file in an editor,
since its syntax is human readable. In fact, project files use a syntax similar to the one from
the Ada language. Let's look at the basic structure of project files and how to customize
them.
The main element of a project file is a project declaration, which contains definitions for
the current project. A project file may also include other project files in order to compose a
complex build. One of the simplest form of a project file is the following:
project Default is
end Default;
348 https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug.html
1687
Learning Ada
In this example, we declare a project named Default. The for Main use expression
indicates that the main.adb file is used as the entry point (main source-code file) of the
project. The main file doesn't necessary need to be called main.adb; we could use any
source-code implementing a main application, or even have a list of multiple main files.
The for Source_Dirs use expression indicates that the src directory contains the source-
file for the application (including the main file).
88.2.2 Customization
GPRbuild support scenario variables, which allow you to control the way binaries are built.
For example, you may want to distinguish between debug and optimized versions of your
binary. In principle, you could pass command-line options to gprbuild that turn debugging
on and off, for example. However, defining this information in the project file is usually
easier to handle and to maintain. Let's define a scenario variable called ver in our project:
project Default is
end Default;
In this example, we're specifying that the scenario variable Ver is initialized with the exter-
nal variable ver. Its default value is set to debug.
We can now set this variable in the call to gprbuild:
Alternatively, we can simply specify an environment variable. For example, on Unix sys-
tems, we can say:
export ver=debug
gprbuild -P project.gpr
In the project file, we can use the scenario variable to customize the build:
project Default is
Ver := external ("ver", "debug");
package Compiler is
case Ver is
when "debug" =>
for Switches ("Ada") use ("-g");
when "opt" =>
for Switches ("Ada") use ("-O2");
when others =>
null;
end case;
(continues on next page)
end Default;
We're now using Ver in the for Object_Dir clause to specify a subdirectory of the obj
directory that contains the object files. Also, we're using Ver to select compiler options in
the Compiler package declaration.
We could also specify all available options in the project file by creating a typed variable.
For example:
project Default is
package Compiler is
case Ver is
when "debug" =>
for Switches ("Ada") use ("-g");
when "opt" =>
for Switches ("Ada") use ("-O2");
when others =>
null;
end case;
end Compiler;
end Default;
The advantage of this approach is that gprbuild can now check whether the value that
you provide for the ver variable is available on the list of possible values and give you an
error if you're entering a wrong value.
GPRbuild supports project dependencies. This allows you to reuse information from existing
projects. Specifically, the keyword with allows you to include another project within the
current project.
Let's look at a very simple example. We have a package called Test_Pkg associated with
the project file test_pkg.gpr, which contains:
project Test_Pkg is
for Source_Dirs use ("src");
for Object_Dir use "obj";
end Test_Pkg;
package Test_Pkg is
type T is record
X : Integer;
Y : Integer;
end record;
end Test_Pkg;
end Test_Pkg;
For this example, we use a directory test_pkg containing the project file and a subdirectory
test_pkg/src containing the source files. The directory structure looks like this:
|- test_pkg
| | test_pkg.gpr
| |- src
| | | test_pkg.adb
| | | test_pkg.ads
Suppose we want to use the Test_Pkg package in a new application. Instead of directly
including the source files of Test_Pkg in the project file of our application (either directly or
indirectly), we can instead reference the existing project file for the package by using with
"test_pkg.gpr". This is the resulting project file:
with "../test_pkg/test_pkg.gpr";
project Default is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Main use ("main.adb");
end Default;
procedure Main is
A : T;
begin
A := Init;
end Main;
When we build the main project file (default.gpr), we're automatically building all de-
pendent projects. More specifically, the project file for the main application automatically
includes the information from the dependent projects such as test_pkg.gpr. Using a with
in the main project file is all we have to do for that to happen.
We can structure project files to make use of dynamic (shared) libraries using a very similar
approach. It's straightforward to convert the project above so that Test_Pkg is now com-
piled into a dynamic library and linked to our main application. All we need to do is to make
a few additions to the project file for the Test_Pkg package:
Configuration pragma files contain a set of pragmas that modify the compilation of source
files according to external requirements. For example, you may use pragmas to either relax
or strengthen requirements depending on your environment.
In GPRbuild, we can use Local_Configuration_Pragmas (in the Compiler package) to
indicate the configuration pragmas file we want GPRbuild to use with the source files in
our project.
The file gnat.adc shown here is an example of a configuration pragma file:
We can use this in our project by declaring a Compiler package. Here's the complete project
file:
project Default is
package Compiler is
for Local_Configuration_Pragmas use "gnat.adc";
end Compiler;
end Default;
Each pragma contained in gnat.adc is used in the compilation of each file, as if that pragma
was placed at the beginning of each file.
You can control the compilation of your source code by creating variants for various cases
and selecting the appropriate variant in the compilation package in the project file. One
example where this is useful is conditional compilation using Boolean constants, shown in
the code below:
with Config;
procedure Main is
begin
if Config.Debug then
Put_Line ("Debug version");
else
Put_Line ("Release version");
end if;
end Main;
In this example, we declared the Boolean constant in the Config package. By having mul-
tiple versions of that package, we can create different behavior for each usage. For this
simple example, there are only two possible cases: either Debug is True or False. However,
we can apply this strategy to create more complex cases.
In our next example, we store the packages in the subdirectories debug and release of the
source code directory. Here's the content of the src/debug/config.ads file:
package Config is
end Config;
package Config is
end Config;
In this case, GPRbuild selects the appropriate directory to look for the config.ads file
according to information we provide for the compilation process. We do this by using a
scenario type called Mode_Type in our project file:
project Default is
end Default;
We declare the scenario variable Mode and use it in the Source_Dirs declaration to add the
desired path to the subdirectory containing the config.ads file. The expression "src/" &
Mode concatenates the user-specified mode to select the appropriate subdirectory. For more
complex cases, we could use either a tree of subdirectories or multiple scenario variables
for each aspect that we need to configure.
EIGHTYNINE
GNAT STUDIO
This chapter presents an introduction to the GNAT Studio, which provides an IDE to develop
applications in Ada. For a detailed overview, please refer to the GNAT Studio tutorial349 .
Also, you can refer to the GNAT Studio product page350 for some introductory videos.
In this chapter, all indications using "→" refer to options from the GNAT Studio menu that
you can click in order to execute commands.
89.1 Start-up
The first step is to start-up the GNAT Studio. The actual step depends on your platform.
89.1.1 Windows
89.1.2 Linux
After starting-up GNAT Studio, you can create a project. These are the steps:
• Click on Create new project in the welcome window
– Alternatively, if the wizard (which let's you customize new projects) isn't already
opened, click on File → New Project... to open it.
– After clicking on Create new project, you should see a window with this title:
Create Project from Template.
• Select one of the options from the list and click on Next.
– The simplest one is Basic > Simple Ada Project, which creates a project con-
taining a main application.
• Select the project location and basic settings, and click on Apply.
349 https://docs.adacore.com/live/wave/gps/html/gps_tutorial/index.html
350 https://www.adacore.com/gnatpro/toolsuite/gps
1695
Learning Ada
– If you selected "Simple Ada Project" in the previous step, you may now select the
name of the project and of the main file.
– Note that you can select any name for the main file.
You should now have a working project file.
89.3 Building
As soon as you've created a project file, you can use it to build an application. These are
the required steps:
• Click on Build → Project → Build All
– You can also click on this icon:
• Alternatively, you can click on Build → Project → Build & Run → <name of your
main application>
– You can also click on this icon:
• You can also use the keyboard for building and running the main application:
– Press F4 to open a window that allows you to build the main application and click
on Execute.
– Then, press Shift + F2 to open a window that allows you to run the application,
and click on Execute.
89.4 Debugging
Before you can debug a project, you need to make sure that debugging symbols have been
included in the binary build. You can do this by manually adding a debug version into your
project, as described in the previous chapter (see GPRbuild (page 1687)).
Alternatively, you can change the project properties directly in GNAT Studio. In order to do
that, click on Edit → Project Properties..., which opens the following window:
Click on Build → Switches → Ada on this window, and make sure that the Debug Informa-
tion option is selected.
If you selected "Simple Ada Project" while creating your project in the beginning, you prob-
ably still have a very simple main application that doesn't do anything useful. Therefore,
in order to make the debugging activity more interesting, please enter some statements to
your application. For example:
procedure Main is
begin
Put_Line ("Hello World!");
Put_Line ("Hello again!");
end Main;
You can now build and debug the application by clicking on Build → Project → Build &
Debug → <name of your main application>.
You can then click on Debug → Run... to open a window that allows you to start the appli-
cation. Alternatively, you can press Shift + F9. As soon as the application has started,
you can press F5 to step through the application or press F6 to execute until the next line.
Both commands are available in the menu by clicking on Debug → Step or Debug → Next.
When you've finished debugging your application, you need to terminate the debugger. To
do this, you can click on Debug → Terminate.
In order to see how SPARK can detect issues, let's creating a simple application that accu-
mulates values in a variable A:
procedure Main
with SPARK_Mode is
A : Natural := 0;
begin
Acc (A, Natural'Last);
Acc (A, 1);
end Main;
You can now click on SPARK → Prove All, which opens a window with various options. For
example, on this window, you can select the proof level — varying between 0 and 4 — on
the Proof level list. Next, click on Execute. After the prover has completed its analysis,
you'll see a list of issues found in the source code of your application.
For the example above, the prover complains about an overflow check that might fail. This
is due to the fact that, in the Acc procedure, we're not dealing with the possibility that the
result of the addition might be out of range. In order to fix this, we could define a new
saturating addition Sat_Add that makes use of a custom type T with an extended range.
For example:
procedure Main
with SPARK_Mode is
A2 : T := T (A);
V2 : constant T := T (V);
A_Last : constant T := T (Natural'Last);
begin
A2 := A2 + V2;
A : Natural := 0;
begin
Acc (A, Natural'Last);
Acc (A, 1);
end Main;
Now, when running the prover again with the modified code, no issues are found.
NINETY
GNAT TOOLS
In chapter we present a brief overview of some of the tools included in the GNAT toolchain.
For further details on how to use these tools, please refer to the GNAT User's Guide351 .
90.1 gnatchop
gnatchop renames files so they match the file structure and naming convention expected
by the rest of the GNAT toolchain. The GNAT compiler expects specifications to be stored
in .ads files and bodies (implementations) to be stored in .adb files. It also expects file
names to correspond to the content of each file. For example, it expects the specification
of a package Pkg.Child to be stored in a file named pkg-child.ads.
However, we may not want to use that convention for our project. For example, we may
have multiple Ada packages contained in a single file. Consider a file example.ada contain-
ing the following:
package P is
procedure Test;
end P;
package body P is
procedure Test is
begin
Put_Line("Test passed.");
end Test;
end P;
with P; use P;
procedure P_Main is
begin
P.Test;
end P_Main;
To compile this code, we first pass the file containing our source code to gnatchop before
we call gprbuild:
gnatchop example.ada
gprbuild p_main
This generates source files for our project, extracted from example_ada, that conform to
the default naming convention and then builds the executable binary p_main from those
351 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn.html
1701
Learning Ada
files. In this example gnatchop created the files p.ads, p.adb, and p_main.adb using the
package names in example.ada.
When we use this mechanism, any warnings or errors the compiler displays refers to the files
generated by gnatchop. We can, however, instruct gnatchop to instrument the generated
files so the compiler refers to the original file (example.ada in our case) when displaying
messages. We do this by using the -r switch:
gnatchop -r example.ada
gprbuild p_main
If, for example, we had an unused variable in example.ada, the compiler warning would
now refer to the line in the original file, not in one of the generated ones.
For documentation of other switches available for gnatchop, please refer to the gnatchop
chapter352 of the GNAT User's Guide.
90.2 gnatprep
We may want to use conditional compilation in some situations. For example, we might
need a customized implementation of a package for a specific platform or need to select a
specific version of an algorithm depending on the requirements of the target environment.
A traditional way to do this uses a source-code preprocessor. However, in many cases where
conditional compilation is needed, we can instead use the syntax of the Ada language or
the functionality provided by GPRbuild to avoid using a preprocessor in those cases. The
conditional compilation section353 of the GNAT User's Guide discusses how to do this in
detail.
Nevertheless, using a preprocessor is often the most straightforward option in complex
cases. When we encounter such a case, we can use gnatprep, which provides a syntax
that reminds us of the C and C++ preprocessor. However, unlike in C and C++, this syntax
is not part of the Ada standard and can only be used with gnatprep. Also, you'll notice some
differences in the syntax from that preprocessor, such as shown in the example below:
Of course, in this simple case, we could have used the Ada language directly and avoided
the preprocessor entirely:
package Config is
Version : constant Integer := 4;
end Config;
with Config;
procedure Do_Something is
begin
if Config.Version >= 4 then
null;
-- Implementation for version 4.0 and above...
else
null;
(continues on next page)
352 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/the_gnat_compilation_model.html#
renaming-files-with-gnatchop
353 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/the_gnat_compilation_model.html#
conditional-compilation
But for the sake of illustrating the use of gnatprep, let's use that tool in this simple case.
This is the complete procedure, which we place in file do_something.org.adb:
procedure Do_Something is
begin
#if VERSION'Defined and then (VERSION >= 4) then
-- Implementation for version 4.0 and above...
null;
#else
-- Standard implementation for older versions...
null;
#end if;
end Do_Something;
To preprocess this file and build the application, we call gnatprep followed by GPRbuild:
If we look at the resulting file after preprocessing, we see that the #else implementation
was selected by gnatprep. To cause it to select the newer "version" of the code, we include
the symbol and its value in our call to gnatprep, just like we'd do for C/C++:
However, a cleaner approach is to create a symbol definition file containing all symbols we
use in our implementation. Let's create the file and name it prep.def:
VERSION := 5
When we use gnatprep in that way, the line numbers of the output file differ from those of
the input file. To preserve line numbers, we can use one of these command-line switches:
• -b: replace stripped-out code by blank lines
• -c: comment-out the stripped-out code
For example:
When we use one of these options, gnatprep ensures that the output file do_something.
adb has the same line numbering as the original file (do_something.org.adb).
The gnatprep chapter354 of the GNAT User's Guide contains further details about this tool,
such as how to integrate gnatprep with project files for GPRbuild and how to replace sym-
bols without using preprocessing directives (using the $symbol syntax).
354 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/the_gnat_compilation_model.html#
preprocessing-with-gnatprep
90.3 gnatmem
Memory allocation errors involving mismatches between allocations and deallocations are
a common source of memory leaks. To test an application for memory allocation issues,
we can use gnatmem. This tool monitors all memory allocations in our application. We use
this tool by linking our application to a special version of the memory allocation library
(libgmem.a).
Let's consider this simple example:
procedure Simple_Mem is
I_Ptr : access Integer := new Integer;
begin
null;
end Simple_Mem;
Global information
------------------
Total number of allocations : 1
Total number of deallocations : 0
Final Water Mark (non freed mem) : 4 Bytes
High Water Mark : 4 Bytes
Allocation Root # 1
-------------------
Number of non freed allocations : 1
Final Water Mark (non freed mem) : 4 Bytes
High Water Mark : 4 Bytes
Backtrace :
simple_mem.adb:2 simple_mem
This shows all the memory we allocated and tells us that we didn't deallocate any of it.
Please refer to the chapter on gnatmem355 of the GNAT User's Guide for a more detailed
discussion of gnatmem.
355 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_and_program_execution.html#
the-gnatmem-tool
90.4 gnatmetric
We can use the GNAT metric tool (gnatmetric) to compute various programming metrics,
either for individual files or for our complete project.
For example, we can compute the metrics of the body of package P above by running
gnatmetric as follows:
gnatmetric p.adb
Please refer to the section on gnatmetric356 of the GNAT User's Guide for the many switches
available for gnatmetric, including the ability to generate reports in XML format.
90.5 gnatdoc
Use GNATdoc to generate HTML documentation for your project. It scans the source files in
the project and extracts information from package, subprogram, and type declarations.
The simplest way to use it is to provide the name of the project or to invoke GNATdoc from
a directory containing a project file:
gnatdoc -P some_directory/default.gpr
gnatdoc
Just using this command is sufficient if your goal is to generate a list of the packages and a
list of subprograms in each. However, to create more meaningful documentation, you can
annotate your source code to add a description of each subprogram, parameter, and field.
For example:
package P is
-- Collection of auxiliary subprograms
the-gnat-metrics-tool-gnatmetric
end P;
package body P is
end P;
with P; use P;
procedure Main is
I : Integer;
begin
I := Add_One (0);
end Main;
When we run this example, GNATdoc will extract the documentation from the specification
of package P and add the description of each element, which we provided as a comment
in the line below the actual declaration. It will also extract the package description, which
we wrote as a comment in the line right after package P is. Finally, it will extract the
documentation of function Add_One (both the description of the V parameter and the return
value).
In addition to the approach we've just seen, GNATdoc also supports the tagged format that's
commonly found in tools such as Javadoc and uses the @ syntax. We could rewrite the
documentation for package P as follows:
package P is
-- @summary Collection of auxiliary subprograms
function Add_One
(V : Integer
) return Integer;
-- @param V Coefficient to be incremented
-- @return Coefficient incremented by one
end P;
You can control what parts of the source-code GNATdoc parses to extract the documentation.
For example, you can specify the -b switch to request that the package body be parsed for
additional documentation and you can use the -p switch to request GNATdoc to parse the
private part of package specifications. For a complete list of switches, please refer to the
GNATdoc User's Guide357 .
357 http://docs.adacore.com/gnatdoc-docs/users_guide/_build/html/index.html
90.6 gnatpp
The term 'pretty-printing' refers to the process of formatting source code according to a
pre-defined convention. gnatpp is used for the pretty-printing of Ada source-code files.
Let's look at this example, which contains very messy formatting:
PrOcEDuRE Main
IS
FUNCtioN
Init_2
RETurn
inteGER iS
(2);
I : INTeger;
BeGiN
I := Init_2;
ENd;
gnatpp main.adb
gnatpp reformats the file in place. After this command, main.adb looks like this:
procedure Main is
I : Integer;
begin
I := Init_2;
end Main;
We can also process all source code files from a project at once by specifying a project file.
For example:
gnatpp -P default.gpr
gnatpp has an extensive list of options, which allow for specifying the formatting of many
aspects of the source and implementing many coding styles. These are extensively dis-
cussed in the section on gnatpp358 of the GNAT User's Guide.
358 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_utility_programs.html#
the-gnat-pretty-printer-gnatpp
90.7 gnatstub
Suppose you've created a complex specification of an Ada package. You can create the
corresponding package body by copying and adapting the content of the package speci-
fication. But you can also have gnatstub do much of that job for you. For example, let's
consider the following package specification:
package Aux is
end Aux;
gnatstub aux.ads
pragma Ada_2012;
package body Aux is
-------------
-- Add_One --
-------------
-----------
-- Reset --
-----------
end Aux;
As we can see in this example, not only has gnatstub created a package body from all the
elements in the package specification, but it also created:
• Headers for each subprogram (as comments);
• Pragmas and exceptions that prevent us from using the unimplemented subprograms
in our application.
This is a good starting point for the implementation of the body. Please refer to the sec-
tion on gnatstub359 of the GNAT User's Guide for a detailed discussion of gnatstub and its
options.
359 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_utility_programs.html#
the-body-stub-generator-gnatstub
1709
Learning Ada
360 http://creativecommons.org/licenses/by-sa/4.0
1711
Learning Ada
1712
CHAPTER
NINETYONE
INTRODUCTION
Ada is a general purpose, high-level programming language designed to support the con-
struction of long-lived, highly-reliable applications. Like all general-purpose languages, only
a subset of the full language is appropriate for safety-critical applications because the full
language includes facilities that are difficult to analyze and verify to the degree required.
This document facilitates identification of subsets appropriate for the highest levels of in-
tegrity, including safety-critical applications.
SPARK is a statically verifiable subset of Ada designed specifically for the most critical ap-
plications. Ada constructs not amenable to verification are precluded, such as arbitrary use
of access types and full tasking. SPARK is also a superset of Ada, with additional contracts
for specifying and verifying programs. Many of the guidelines (and more) are implicit in the
design of SPARK.
Therefore, this document defines guidelines for the development of high-integrity, safety-
critical applications in either the Ada or SPARK programming languages, or both (because
the two can be mixed).
91.1 Scope
This document provides guidelines for development decisions, both at the system level and
at the unit level, regarding the use of the programming languages Ada and SPARK, as well
as related tools, such as static analyzers and unit test generators. It is not concerned with
presentation issues such as naming, use of whitespace, or the like.
91.2 Structure
Rather than defining a specific set of rules defining a single subset, this document defines
a set of criteria, in the form of guidelines, used by system architects to identify project-
specific subsets appropriate to a given project.
The guidelines are separated into related categories, such as storage management, object-
oriented programming, concurrency management, and so on. Each guideline is in a sepa-
rate table, specifying the rule name, a unique identifier, and additional attributes common
to each table.
1713
Learning Ada
91.3 Enforcement
Detection and enforcement mechanisms are indicated for each guideline. These mech-
anisms typically consist of the application of a language standard pragma named Re-
strictions, with policy-specific restriction identifiers given as parameters to the pragma
[AdaRM2016]. Violations of the given restrictions are then detected and enforced by the
Ada compiler.
Alternatively, the AdaCore GNATcheck utility program has rules precisely corresponding
to those restriction identifiers, with the same degree of detection and enforcement. For
example, the language restriction identifier No_Unchecked_Deallocation corresponds to
the GNATcheck +RRestrictions:No_Unchecked_Deallocation rule.
The advantage of GNATcheck over the compiler is that all generated messages will be col-
lected in the GNATcheck report that can be used as evidence of the level of adherence to
the coding standard. In addition, GNATcheck provides a mechanism to deal with accepted
exemptions.
In some cases the enforcement mechanism is the SPARK language and analyzer. Many of
the guidelines (and more) are implicit in the design of SPARK and are, therefore, automati-
cally enforced.
In some (very) rare cases the enforcement mechanism is manual program inspection, al-
though alternatives (e.g., SPARK) are usually available and recommended. These guidelines
are included because they are considered invaluable in this domain.
Although we refer to them as rules in the tables for the sake of brevity, these entries should
be considered guidance because they require both thought and consideration of project-
specific characteristics. For example, in some cases the guidance is to make a selection
from among a set of distinct enumerated policies. In other cases a single guideline should
be followed but not without some exceptional situations allowing it to be violated. The
project lead should consider which guidelines to apply and how best to apply each guideline
selected.
Many of these rules can also be considered good programming practices. As such, many
of them can be directly correlated to the ISO/IEC Guidance to Avoiding Vulnerabilities in
Programming Languages [TR24772]. When a rule addresses one of these vulnerabilities, it
is listed in the appropriate subsection.
NINETYTWO
DEFINITIONS
This section contains terms and values used in the definitions of the rules set forth in this
chapter.
92.1 Level
Level is the compliance level for the rule. Possible values are:
Mandatory
Non-compliance with a Mandatory recommendation level corresponds to a
high risk of a software bug. There would need to be a good reason for non-
conformity to a mandatory rule and, although it is accepted that exceptional
cases may exist, any non-conformance should be accompanied by a clear
technical explanation of the exceptional circumstance.
Required
Non-compliance with a Required recommendation level corresponds to a
medium to high risk of a software bug. Much like a Mandatory recom-
mendation, there would need to be a good reason for non-conformity to a
required rule. Although it is accepted that more exceptional cases may exist,
non-conformance should be accompanied by a clear technical explanation of
the exceptional circumstance.
Advisory
Failure to follow an Advisory recommendation does not necessarily result in
a software bug; the risk of a direct correlation between non-conformance of
an advisory rule and a software bug is low. Non-compliance with an advisory
recommendation level does not require a supporting technical explanation,
however, as the quality of the code may be impacted, the reason for the
non-conformance should be understood.
92.2 Remediation
Remediation indicates the the level of difficulty to modify/update code that does not follow
this particular rule.
High
Failure to follow this rule will likely cause an unreasonable amount of modifi-
cations/updates to bring the code base into compliance.
Medium
Failure to follow this rule will likely cause a large amount of modifica-
tions/updates to bring the code base into compliance, but those changes
may still be cost-effective.
1715
Learning Ada
Low
Failure to follow this rule may cause a small amount of modifications/updates
to bring the code base into compliance, but those changes will be minor
compared to the benefit.
N/A
This rule is more of a design decision (as opposed to a coding flaw) and
therefore, if the rule is violated, it is done so with a specific purpose.
NINETYTHREE
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
✓
Security
✓
Description
Have a plan for managing dynamic memory allocation and deallocation.
Rules
DYN01, DYN02, DYN03, DYN04, DYN05, DYN06
In Ada, objects are created by being either declared or allocated. Declared objects may
be informally thought of as being created "on the stack" although such details are not
specified by the language. Allocated objects may be thought of as being allocated "from the
heap", which is, again, an informal term. Allocated objects are created by the evaluation of
allocators represented by the reserved word new and, unlike declared objects, have lifetimes
independent of scope.
The terms static and dynamic tend to be used in place of declared and allocated, although
in traditional storage management terminology all storage allocation in Ada is dynamic. In
the following discussion, the term dynamic allocation refers to storage that is allocated by
allocators. Static object allocation refers to objects that are declared. Deallocation refers
to the reclamation of allocated storage.
Unmanaged dynamic storage allocation and deallocation can lead to storage exhaustion;
the required analysis is difficult under those circumstances. Furthermore, access values
can establish aliases that complicate verification, and explicit deallocation of dynamic stor-
age can lead to specific errors (e.g., "double free", "use after free") having unpredictable
results. As a result, the prevalent approach to storage management in high-integrity sys-
tems is to disallow dynamic management techniques completely. [SEI-C] [MISRA2013]
[Holzmann2006] [ISO2000]
However, restricted forms of storage management and associated feature usage can sup-
port the necessary reliability and analyzability characteristics while retaining sufficient ex-
pressive power to justify the analysis expense. The following sections present possible
approaches, including the traditional approach in which no dynamic behavior is allowed.
Individual projects may then choose which storage management approach best fits their
requirements and apply appropriate tailoring, if necessary, to the specific guidelines.
Realization
There is a spectrum of management schemes possible, trading ease of analysis against
1717
Learning Ada
increasing expressive power. At one end there is no dynamic memory allocation (and
hence, deallocation) allowed, making analysis trivial. At the other end, nearly the
full expressive power of the Ada facility is available, but with analyzability partially
retained. In the latter, however, the user must create the allocators in such a manner
as to ensure proper behavior.
Rule DYN01 is Required, as it avoids problematic features whatever the strategy chosen.
Rules DYN02-05 are marked as Advisory, because one of them should be chosen and en-
forced throughout a given software project.
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → Compiler restrictions
93.1.1 Reference
93.1.2 Description
For No_Anonymous_Allocators:
For No_Coextensions:
For No_Access_Parameter_Allocators:
93.1.6 Notes
The compiler will detect violations of the first three restrictions. Note that GNATcheck can
detect violations in addition to the compiler.
The fourth restriction is a directive for implementation behavior, not subject to source-based
violation detection.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → Compiler restrictions
93.2.1 Reference
93.2.2 Description
N/A
93.2.6 Notes
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → Compiler restrictions
93.3.1 Reference
MISRA C Rule 21.3: "The memory allocation and deallocation functions of <stdlib.h> shall
not be used."
93.3.2 Description
93.3.6 Notes
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
Remediation → Low
Verification Method → Compiler restrictions
93.4.1 Reference
Power of Ten rule 3: "Do not use dynamic memory allocation after initialization."
93.4.2 Description
Code performing dynamic allocations any time prior to an arbitrary point designated as the
end of the "startup" interval.
93.4.6 Notes
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → Code inspection
93.5.1 Reference
MISRA C Rule 21.3: "The memory allocation and deallocation functions of <stdlib.h> shall
not be used."
93.5.2 Description
There are two issues that make storage utilization analysis difficult:
1. the predictability of the allocation and deallocation implementation, and
2. how access values are used by the application.
The behavior of the underlying implementation is largely undefined and may, for example,
consist of calls to the operating system (if present). Application code can manipulate access
values beyond the scope of analysis.
Under this policy, the full expressive power of access-to-object types is provided but one of
the two areas of analysis difficulty is removed. Specifically, predictability of the allocation
and deallocation implementation is achieved via user-defined storage pools. With these
storage pools, the implementation of allocation (new) and deallocation (instances of Ada.
Unchecked_Deallocation) is defined by the pool type.
If the pool type is implemented with fixed-size blocks on the stack, allocation and dealloca-
tion timing behavior are predictable.
Such an implementation would also be free from fragmentation.
Given an analysis providing the worst-case allocations and deallocations, it would be pos-
sible to verify that pool exhaustion does not occur. However, as mentioned such analysis
can be quite difficult. A mitigation would be the use of the "owning" access-to-object types
provided by SPARK.
In this approach no storage-related constructs are disallowed unless the SPARK subset is
applied.
Heap : Sequential_Fixed_Blocks.Storage_Pool
(Storage_Size => Required_Storage_Size,
Element_Size => Representable_Obj_Size,
Alignment => Representation_Alignment);
type Pointer is access all Unsigned_Longword with
Storage_Pool => Heap;
Ptr : Pointer;
...
Ptr := new Unsigned_Longword; -- from Heap
93.5.6 Notes
Enforcement of this approach can only be provided by manual code review unless SPARK is
used.
However, the User-Defined Storage Pools Policy can be enforced statically by specifying De-
fault_Storage_Pool (null). This essentially requires all access types to have a specified
storage pool if any allocators are used with the access type.
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
Remediation → Low
Verification Method → Static analysis tools
93.6.1 Reference
N/A
93.6.2 Description
Each Ada application task has a stack, as does the "environment task" that elaborates li-
brary packages and calls the main subprogram. A tool to statically determine the maximum
storage required for these stacks must be used, per task.
This guideline concerns another kind of dynamic memory utilization. The previous guide-
lines concerned the management of storage commonly referred to as the "heap." This
guideline concerns the storage commonly referred to as the "stack." (Neither term is de-
fined by the language, but both are commonly recognized and are artifacts of the underlying
run-time library or operating system implementation.)
N/A
N/A
93.6.6 Notes
The GNATstack363 tool can statically determine the maximum requirements per task.
363 http://docs.adacore.com/live/wave/gnatstack/html/gnatstack_ug/index.html
NINETYFOUR
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
✓
Security
✓
Description
Related to managing dynamic storage at the system (policy) level, these statement-
level rules concern the safe reclamation of access (pointer) values.
Rules
RCL01, RCL02, RCL03
Level → Mandatory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
✓
1729
Learning Ada
Remediation → High
Verification Method → Code inspection
94.1.1 Reference
94.1.2 Description
Never deallocate the storage designated by a given access value more than once.
94.1.6 Notes
Enforcement of this rule can be provided by manual code review, unless deallocation is for-
bidden via No_Unchecked_Deallocation or SPARK is used, as ownership analysis in SPARK
detects such cases. Note that storage utilization analysis tools such as Valgrind can usually
find this sort of error. In addition, a GNAT-defined storage pool is available to help debug
such errors.
Level → Mandatory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
✓
Remediation → High
Verification Method → Code inspection
94.2.1 Reference
94.2.2 Description
Only deallocate storage that was dynamically allocated by the evaluation of an allocator
(i.e., new).
This is possible because Ada allows creation of access values designating declared (aliased)
objects.
94.2.6 Notes
Enforcement of this rule can only be provided by manual code review, unless deallocation
is forbidden via No_Unchecked_Deallocation.
Level → Mandatory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
✓
Remediation → High
Verification Method → Code inspection
94.3.1 Reference
N/A
94.3.2 Description
When deallocating, ensure that the pool to which the storage will be returned is the same
pool from which it was allocated. Execution is erroneous otherwise, meaning anything can
happen (Ada Reference Manual: 13.11.2 (16) Unchecked Storage Deallocation364 ).
Each access type has an associated storage pool, either implicitly by default, or explicitly
with a storage pool specified by the programmer. The implicit default pool might not be
the same pool used for another access type, even an access type designating the same
subtype.
364 http://www.ada-auth.org/standards/12rm/html/RM-13-11-2.html
In the above, P1.all was allocated from Pointer1'Storage_Pool, but, via the type con-
version, the code above is attempting to return it to Pointer2'Storage_Pool, which may
be a different pool.
94.3.6 Notes
Enforcement of this rule can only be provided by manual code review, unless deallocation
is forbidden via No_Unchecked_Deallocation.
NINETYFIVE
CONCURRENCY (CON)
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
Description
Have a plan for managing the use of concurrency in high-integrity applications having
real-time requirements.
Rules
CON01, CON02, CON03
The canonical approach to applications having multiple periodic and aperiodic activities is
to map those activities onto independent tasks, i.e., threads of control. The advantages for
the application are both a matter of software engineering and also ease of implementation.
For example, when the different periods are not harmonics of one another, the fact that
each task executes independently means that the differences are trivially represented. In
contrast, such periods are not easily implemented in a cyclic scheduler, which, by definition,
involves only one (implicit) thread of control with one frame rate.
High integrity applications are subject to a number of stringent analyses, including, for ex-
ample, safety analyses and certification against rigorous industry standards. In addition,
high integrity applications with real-time requirements must undergo timing analysis be-
cause they must be shown to meet deadlines prior to deployment — failure to meet hard
deadlines is unacceptable in this domain.
These analyses are applied both to the application and to the implementation of the un-
derlying run-time library. However, analysis of the complete set of general Ada tasking
features is not tractable, neither technically nor in terms of cost. A subset of the language
is required.
The Ravenscar profile [AdaRM2016] is a subset of the Ada concurrency facilities that sup-
ports determinism, schedulability analysis, constrained memory utilization, and certifica-
tion to the highest integrity levels. Four distinct application domains are specifically in-
tended:
• Hard real-time applications requiring predictability;
• Safety-critical systems requiring formal, stringent certification;
• High-integrity applications requiring formal static analysis and verification;
1735
Learning Ada
• Embedded applications requiring both a small memory footprint and low execution
overhead.
Those tasking constructs that preclude analysis at the source level or analysis of the tasking
portion of the underlying run-time library are disallowed.
The Ravenscar profile is necessarily strict in terms of what it removes so that it can support
the stringent analyses, such as safety analysis, that go beyond the timing analysis required
for real-time applications. In addition, the strict subset facilitates that timing analysis in the
first place.
However, not all high-integrity applications are amenable to expression in the Ravenscar
profile subset. The Jorvik profile [AdaRM2020] is an alternative subset of the Ada concur-
rency facilities. It is based directly on the Ravenscar profile but removes selected restric-
tions in order to increase expressive power, while retaining analyzability and performance.
As a result, typical idioms for protected objects can be used, for example, and relative de-
lays statements are allowed. Timing analysis is still possible but slightly more complicated,
and the underlying run-time library is slightly larger and more complex.
When the most stringent analyses are required and the tightest timing is involved, use the
Ravenscar profile. When a slight increase in complexity is tolerable, i.e., in those cases not
undergoing all of these stringent analyses, consider using the Jorvik profile.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
Remediation → High
Verification Method → GNATcheck rule: uses_profile:ravenscar
Mutually Exclusive → CON02
95.1.1 Reference
95.1.2 Description
Any code disallowed by the profile. Remediation is high because use of the facilities outside
the subset can be difficult to retrofit into compliance.
95.1.6 Notes
The Ada builder will detect violations if the programmer specifies this profile or correspond-
ing pragmas. GNATcheck also can detect violations of profile restrictions.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
Remediation → High
Verification Method → GNATcheck rule: uses_profile:jorvik
Mutually Exclusive → CON01
95.2.1 Reference
95.2.2 Description
The following restrictions are part of the Ravenscar profile but not part of the Jorvik profile.
No_Implicit_Heap_Allocations
No_Relative_Delay
Max_Entry_Queue_Length => 1
Max_Protected_Entries => 1
No_Dependence => Ada.Calendar
No_Dependence => Ada.Synchronous_Barriers
Any code disallowed by the profile. Remediation is high because use of the facilities outside
the subset can be difficult to retrofit into compliance.
95.2.6 Notes
The Ada builder will detect violations. GNATcheck can also detect violations.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
Remediation → High
Verification Method → GNATcheck rule: Volatile_Objects_Without_Address_Clauses
95.3.1 Reference
95.3.2 Description
Although the Ravenscar and Jorvik profiles allow the use of shared variables for inter-task
communication, such use is less robust and less reliable than encapsulating shared vari-
ables within protected objects.
Global_Object : Integer
with Volatile;
function Get return Integer is (Global_Object);
Note that variables marked as Atomic are also Volatile, per the Ada Reference Manual:
C.6 (8/3) Shared Variable Control368
367 http://www.ada-auth.org/standards/12rm/html/RM-D-13.html
368 http://www.ada-auth.org/standards/12rm/html/RM-C-6.html
When assigned to a memory address, a Volatile variable can be used to interact with a
memory-mapped device, among other similar usages.
Global_Object : Integer
with Volatile,
Address => To_Address (16#1234_5678#);
function Get return Integer is (Global_Object);
95.3.6 Notes
In additon to GNATcheck, SPARK and CodePeer can also detect conflicting access to unpro-
tected variables.
NINETYSIX
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
✓
Description
These rules promote the production of robust software.
Rules
RPP01, RPP02, RPP03, RPP04, RPP05, RPP06, RPP07, RPP07, RPP08, RPP09, RPP10,
RPP11, RPP12, RPP13, RPP14
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
1743
Learning Ada
Remediation → Low
Verification Method → GNATcheck rule: OTHERS_In_CASE_Statements
96.1.1 Reference
[SEI-C] MSC01-C
96.1.2 Description
Case statement alternatives and case-expressions must not include use of the others dis-
crete choice option. This rule prevents accidental coverage of a choice added after the
initial case statement is written, when an explicit handler was intended for the addition.
Note that this is opposite to typical C guidelines such as [SEI-C] MSC01-C. The reason is that
in C, the default alternative plays the role of defensive code to mitigate the switch state-
ment's non-exhaustivity. In Ada, the case construct is exhaustive: the compiler statically
verifies that for every possible value of the case expression there is a branch alternative,
and there is also a dynamic check against invalid values which serves as implicit defen-
sive code. As a result, Ada's others alternative doesn't play C's defensive code role and
therefore a stronger guideline can be adopted.
96.1.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Enumeration_Ranges_In_CASE_Statements
96.2.1 Reference
Similar to RPP01
96.2.2 Description
A range of enumeration literals must not be used as a choice in a case statement or a case
expression. This includes explicit ranges (A .. B), subtypes, and the 'Range attribute.
Much like the use of others in case statement alternatives, the use of ranges makes it pos-
sible for a new enumeration value to be added but not handled with a specific alternative,
when a specific alternative was intended.
96.2.6 Notes
N/A
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: OTHERS_In_Aggregates
96.3.1 Reference
Similar to RPP01
96.3.2 Description
Do not use an others choice in an extension aggregate. In record and array aggregates,
do not use an others choice unless it is used either to refer to all components, or to all but
one component.
This guideline prevents accidental provision of a general value for a record component or
array component, when a specific value was intended. This possibility includes the case
in which new components are added to an existing composite type.
96.3.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → High
Verification Method → GNATcheck rule: Unassigned_OUT_Parameters
96.4.1 Reference
MISRA C Rule 9.1: "The value of an object with automatic storage duration shall not be read
before it has been set."
96.4.2 Description
For any procedure, all formal parameters of mode out must be assigned a value if the
procedure exits normally. This rule ensures that, upon a normal return, the corresponding
actual parameter has a defined value. Ensuring a defined value is especially important for
scalar parameters because they are passed by value, such that some value is copied out to
the actual. These undefined values can be especially difficult to locate because evaluation
of the actual parameter's value might not occur immediately after the call returns.
In the above example, some value is copied back for an output parameter as specified by
Register. The other parameter is not assigned, and on return the value copied to the
actual parameter may not be a valid representation for a value of the type. (We give the
enumeration values a non-standard representation for the sake of illustration, i.e., to make
it more likely that the undefined value is not valid.)
96.4.6 Notes
The GNATcheck rule specified above only detects a trivial case of an unassigned variable
and doesn't provide a guarantee that there is no uninitialized access. It is not a replacement
for a rigorous check for uninitialized access provided by advanced static analysis tools such
as SPARK and CodePeer.
Note that the GNATcheck rule does not check function parameters (as of Ada 2012 functions
can have out parameters). As a result, the better choice is either SPARK or CodePeer.
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: OTHERS_In_Exception_Handlers
96.5.1 Reference
N/A
96.5.2 Description
Much like the situation with others in case statements and case expressions, the use of
others in exception handlers makes it possible to omit an intended specific handler for an
exception, especially a new exception added to an existing set of handlers. As a result,
a subprogram could return normally without having applied any recovery for the specific
exception occurrence, which is likely a coding error.
N/A
96.5.6 Notes
ISO TR 24772-2: 6.50.2 slightly contradicts this when applying exception handlers around
calls to library routines:
• Put appropriate exception handlers in all routines that call library routines, including
the catch-all exception handler when others =>
• Put appropriate exception handlers in all routines that are called by library routines,
including the catch-all exception handler when others =>
ISO TR 24772-2 also recommends "All tasks should contain an exception handler at the
outer level to prevent silent termination due to unhandled exceptions."
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Medium
Verification Method → Code inspection
96.6.1 Reference
MISRA C Rule 13.2: "The value of an expression and its persistent side effects shall be the
same under all permitted evaluation orders."
96.6.2 Description
Call_Count : Integer := 0;
function F return Boolean is
Result : Boolean;
begin
...
Call_Count := Call_Count + 1;
return Result;
end F;
Remove the update to Call_Count, or change the function into a procedure with a param-
eter for Call_Count.
96.6.6 Notes
Violations are detected by SPARK as part of a rule disallowing side effects on expression
evaluation.
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: function_out_parameters
96.7.1 Reference
N/A
96.7.2 Description
OR
96.7.6 Notes
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → High
Verification Method → Code inspection
96.8.1 Reference
96.8.2 Description
In software, an alias is a name which refers to the same object as another name. In some
cases, it is an error in Ada to reference an object through a name while updating it through
another name in the same subprogram. Most of these cases cannot be detected by a com-
piler. Even when not an error, the presence of aliasing makes it more difficult to understand
the code for both humans and analysis tools, and thus it may lead to errors being introduced
during maintenance.
This rule is meant to detect problematic cases of aliasing that are introduced through the
actual parameters and between actual parameters and global variables in a subprogram
call. It is a simplified version of the SPARK rule for anti-aliasing defined in SPARK Reference
Manual, section 6.4.2: Anti-Aliasing371 .
A formal parameter is said to be immutable when the subprogram cannot modify its value
or modify the value of an object by dereferencing a part of the parameter of access type
(at any depth in the case of SPARK). In Ada and SPARK, this corresponds to either an anony-
mous access-to-constant parameter or a parameter of mode in and not of an access type.
Otherwise, the formal parameter is said to be mutable.
A procedure call shall not pass two actual parameters which potentially introduce aliasing
via parameter passing unless either:
• both of the corresponding formal parameters are immutable; or
• at least one of the corresponding formal parameters is immutable and is of a by-copy
type that is not an access type.
If an actual parameter in a procedure call and a global variable referenced by the called
procedure potentially introduce aliasing via parameter passing, then:
• the corresponding formal parameter shall be immutable; and
• if the global variable is written in the subprogram, then the corresponding formal pa-
rameter shall be of a by-copy type that is not an access type.
Where one of the rules above prohibits the occurrence of an object or any of its subcompo-
nents as an actual parameter, the following constructs are also prohibited in this context:
• A type conversion whose operand is a prohibited construct;
• A call to an instance of Unchecked_Conversion whose operand is a prohibited con-
struct;
• A qualified expression whose operand is a prohibited construct;
• A prohibited construct enclosed in parentheses.
369 http://www.ada-auth.org/standards/12rm/html/RM-6-2.html
370 https://docs.adacore.com/spark2014-docs/html/lrm/subprograms.html#anti-aliasing
371 https://docs.adacore.com/spark2014-docs/html/lrm/subprograms.html#anti-aliasing
type R is record
Data : Integer := 0;
end record;
Obj : R;
begin
Detect_Aliasing (Obj, Obj);
96.8.6 Notes
All violations are detected by SPARK. The GNAT compiler switch -gnateA[1] enables de-
tection of some cases, but not all.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
✓
Remediation → Low
Verification Method → Code inspection
96.9.1 Reference
Power of Ten rule 5: "The assertion density of the code should average to a minimum of
two assertions per function."
96.9.2 Description
Subprograms should declare Pre and/or Post contracts. Developers should consider spec-
ifying the Global contract as well, when the default does not apply.
Subprogram contracts complete the Ada notion of a specification, enabling clients to know
what the subprogram does without having to know how it is implemented.
Preconditions define those logical (Boolean) conditions required for the body to be able to
provide the specified behavior. As such, they are obligations on the callers. These condi-
tions are checked at run-time in Ada, prior to each call, and verified statically in SPARK.
Postconditions define those logical (Boolean) conditions that will hold after the call returns
normally. As such, they express obligations on the implementer, i.e., the subprogram body.
The implementation must be such that the postcondition holds, either at run-time for Ada,
or statically in SPARK.
Not all subprograms will have both a precondition and a postcondition, some will have
neither.
The Global contract specifies interactions with those objects not local to the corresponding
subprogram body. As such, they help complete the specification because, otherwise, one
would need to examine the body of the subprogram itself and all those it calls, directly or
indirectly, to know whether any global objects were accessed.
• 6.42 Violations of the Liskov substitution principle or the contract model [BLP]
96.9.6 Notes
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → Static analysis tools
96.10.1 Reference
N/A
96.10.2 Description
N/A
96.10.6 Notes
This rule can be enforced by CodePeer or SPARK, via detection of dead code.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → Compiler restrictions
96.11.1 Reference
MISRA C Rule 17.7: "The value returned by a function having non-void return type shall be
used," and
Directive 4.7: "If a function returns error information, that error information shall be tested."
96.11.2 Description
In Ada and SPARK, it is not possible to ignore the object returned by a function call. The
call must be treated as a value, otherwise the compiler will reject the call. For example,
the value must be assigned to a variable, or passed as the actual parameter to a formal
parameter of another call, and so on.
However, that does not mean that the value is actually used to compute some further
results. Although almost certainly a programming error, one could call a function, assign
the result to a variable (or constant), and then not use that variable further.
Note that functions will not have side-effects (due to RPP06) so it is only the returned value
that is of interest here.
N/A
N/A
96.11.6 Notes
The GNAT compiler warning switch -gnatwu (or the more general -gnatwa warnings switch)
will cause the compiler to detect variables assigned but not read. CodePeer will detect these
unused variables as well. SPARK goes further by checking that all computations contribute
all the way to subprogram outputs.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Recursive_Subprograms
96.12.1 Reference
MISRA C Rule 17.2: "Functions shall not call themselves, either directly or indirectly."
96.12.2 Description
96.12.6 Notes
The compiler will detect violations with the restriction No_Recursion in place. Note this is
a dynamic check.
The GNATcheck rule specified above is a static check, subject to the limitations described
in GNATcheck Reference Manual: Recursive Subprograms372 .
372 https://docs.adacore.com/live/wave/lkql/html/gnatcheck_rm/gnatcheck_rm/predefined_rules.html#
recursive-subprograms
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: overrides_standard_name
96.13.1 Reference
N/A
96.13.2 Description
Do not reuse the names of standard Ada typemarks (e.g. type Integer is range -1_000
.. 1_000;)
When a developer uses an identifier that has the same name as a standard typemark, such
as Integer, a subsequent maintainer might be unaware that this identifier does not actu-
ally refer to Standard.Integer and might unintentionally use the locally-scoped Integer
rather than the original Standard.Integer. The locally-scoped Integer can have different
attributes (and may not even be of the same base type).
N/A
96.13.6 Notes
N/A
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Numeric_Literals
96.14.1 Reference
N/A
96.14.2 Description
Extensive use of literals in a program can lead to two problems. First, the meaning of the
literal is often obscured or unclear from the context. Second, changing a frequently used
literal requires searching the entire program source for that literal and distinguishing the
uses that must be modified from those that should remain unmodified.
Avoid these problems by declaring objects with meaningfully named constants, setting their
values to the desired literals, and referencing the constants instead of the literals through-
out the program. This approach clearly indicates the meaning or intended use of each
literal. Furthermore, should the constant require modification, the change is limited to the
declaration; searching the code is unnecessary.
Some literals can be replaced with attribute values. For example, when iterating over an
array, it is better to use Array_Object'First .. Array_Object'Last than using 1 ..
Array_Object'Length.
N/A
96.14.6 Notes
N/A
NINETYSEVEN
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
✓
Description
Have a plan for managing the use of Ada exceptions at the application level.
Rules
EXU01, EXU02, EXU03, EXU04
Exceptions in modern languages present the software architect with a dilemma. On one
hand, exceptions can increase integrity by allowing components to signal specific errors
in a manner that cannot be ignored, and, in general, allow residual errors to be caught.
(Although there should be no unexpected errors in high integrity code, there may be some
such errors due, for example, to unforeseeable events such as radiation-induced single-
event upsets.) On the other hand, unmanaged use of exceptions increases verification
expense and difficulty, especially flow analysis, perhaps to an untenable degree. In that
case overall integrity is reduced or unwarranted.
In addition, programming languages may define some system-level errors in terms of
language-defined exceptions. Such exceptions may be unavoidable, at least at the system
level. For example, in Ada, stack overflow is signalled with the language-defined Stor-
age_Error exception. Other system events, such as bus error, may also be mapped to
language-defined or vendor-defined exceptions.
Complicating the issue further is the fact that, if exceptions are completely disallowed,
there will be no exception handling code in the underlying run-time library. The effects are
unpredictable if any exception actually does occur.
Therefore, for the application software the system software architect must decide whether
to allow exceptions at all, and if they are to be used, decide the degree and manner of their
usage. At the system level, the architect must identify the exceptions that are possible and
how they will be addressed.
1767
Learning Ada
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Raising_Predefined_Exceptions
97.1.1 Reference
[SEI-Java] ERR07-J
97.1.2 Description
N/A
97.1.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Unhandled_Exceptions
97.2.1 Reference
N/A
97.2.2 Description
All application-defined exceptions must have at least one corresponding handler that is
applicable. Otherwise, if an exception is raised, undesirable behavior is possible. The term
applicable means that there is no dynamic call chain that can reach the active exception
which does not also include a handler that will be invoked for that exception, somewhere
in that chain.
When an unhandled exception occurs in the sequence of statements of an application task
and propagates to task's body, the task terminates abnormally. No notification of some
sort is required or defined by the language, although some vendors' implementations may
print out a log message or provide some other non-standard response. (Note that such a
notification implies an external persistent environment, such as an operating system, that
may not be present in all platforms.) The task failure does not affect any other tasks unless
those other tasks attempt to communicate with it. In short, failure is silent.
Although the language-defined package Ada.Task_Termination can be used to provide
a response using standard facilities, not all run-time libraries provide that package. For
example, under the Ravenscar profile, application tasks are not intended to terminate, nei-
ther normally nor abnormally, and the language does not define what happens if they do.
A run-time library for a memory-constrained target, especially a bare-metal target without
an operating system, might not include any support for task termination when the tasking
model is Ravenscar. The effects of task termination in that case are not defined by the
language.
When an unhandled exception occurrence reaches the main subprogram and is not han-
dled there, the exception occurrence is propagated to the environment task, which then
completes abnormally. Even if the main subprogram does handle the exception, the envi-
ronment task still completes (normally in that case).
When the environment task completes (normally or abnormally) it waits for the completion
of dependent application tasks, if any. Those dependent tasks continue executing normally,
i.e., they do not complete as a result of the environment task completion. Alternatively,
however, instead of waiting for them, the implementation has permission to abort the de-
pendent application tasks, per Ada Reference Manual: 10.2 (30) Program Execution373 The
resulting application-specific effect is undefined.
Finally, whether the environment task waited for the dependent tasks or aborted them,
the semantics of further execution beyond that point are undefined. There is no concept
of a calling environment beyond the environment task (Ada Reference Manual: 10.2 (30)
Program Execution374 ). In some systems there is no calling environment, such as bare-
metal platforms with only an Ada run-time library and no operating system.
373 http://www.ada-auth.org/standards/12rm/html/RM-10-2.html
374 http://www.ada-auth.org/standards/12rm/html/RM-10-2.html
procedure Main is
begin
if Argument_Count = 0 then
raise Cli_Exception;
else
begin
Start_Application (Argument (1));
exception
when Application_Exception =>
Put_Line ("Application failed");
end;
end if;
end Main;
procedure Main is
begin
if Argument_Count = 0 then
raise Cli_Exception;
else
begin
Start_Application (Argument (1));
exception
when Application_Exception =>
Put_Line ("Application failed");
end;
end if;
exception
when Cli_Exception =>
Put_Line ("Failure");
end Main;
97.2.6 Notes
SPARK can prove that no exception will be raised (or fail to prove it and indicate the failure).
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Non_Visible_Exceptions
97.3.1 Reference
RPP05
97.3.2 Description
An active exception can be propagated dynamically past the point where the name of the
exception is visible (the scope of the declaration). The exception can only be handled via
others past that point. That situation prevents handling the exception specifically, and
violates RPP05.
N/A
Compliant_Exception : exception;
procedure Compliant (Param : in out Integer) is
begin
Param := Param * Param;
exception
when others =>
raise Compliant_Exception;
end Compliant;
97.3.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → Compiler restrictions
97.4.1 Reference
MISRA C Rule 1.3: "There shall be no occurrence of undefined or critical unspecified be-
haviour."
97.4.2 Description
In many high-integrity systems the possible responses to an exception are limited or nonex-
istent. In these cases the only approach is to prove exceptions cannot occur in the first
place. Additionally, the cost of proving exceptions cannot happen may be less than the
cost of analyzing code in which they are allowed to be raised.
The restriction No_Exceptions can be used with pragma Restrictions to enforce this ap-
proach. Specifically, the restriction ensures that raise statements and exception handlers
do not appear in the source code and that language-defined checks are not emitted by the
compiler. However, a run-time check performed automatically by the hardware is permit-
ted because it typically cannot be prevented. An example of such a check would be traps
on invalid addresses. If a hardware check fails, or if an omitted language-defined check
would have failed, execution is unpredictable. As a result, enforcement with the restriction
is not ideal. However, proof of the absence of run-time errors is possible using the SPARK
subset of Ada.
N/A
N/A
N/A
97.4.6 Notes
This restriction is detected by SPARK, in which any statements explicitly raising an exception
must be proven unreachable (or proof fails and the failure is indicated), and any possibility
of run-time exception should be proved not to happen.
NINETYEIGHT
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Description
Have a plan for selecting the OOP facilities of the language to use.
Rules
OOP01, OOP02, OOP03, OOP04, OOP05, OOP06, OOP07
There are many issues to consider when planning the use of Object Oriented features in a
high-integrity application. Choices should be made based on the desired expressive power
of the OO features and the required level of certification or safety case.
For example, the use of inheritance can provide abstraction and separation of concerns.
However, the extensive use of inheritance, particularly in deep hierarchies, can lead to
fragile code bases.
Similarly, when new types of entities are added, dynamic dispatching provides separation
of the code that must change from the code that manipulates those types and need not
be changed to handle new types. However, analysis of dynamic dispatching must con-
sider every candidate object type and analyze the associated subprogram for appropriate
behavior.
Therefore, the system architect has available a range of possibilities for the use of OOP
constructs, with tool enforcement available for the selections. Note that full use of OOP,
including dynamic dispatching, may not be unreasonable.
The following rules assume use of tagged types, a requirement for full OOP in Ada.
1775
Learning Ada
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → N/A
Verification Method → Compiler restrictions
Mutually Exclusive → OOP02
98.1.1 Reference
N/A
98.1.2 Description
In this approach, tagged types are allowed and type extension (inheritance) is allowed, but
there are no class-wide constructs.
This restriction ensures there are no class-wide objects or formal parameters, nor access
types designating class-wide types.
In this approach there are no possible dynamic dispatching calls because such calls can
only occur when a class-wide value is passed as the parameter to a primitive operation of
a tagged type.
X : Object'Class := Some_Object;
X : Object := Some_Object;
98.1.6 Notes
The compiler will detect violations with the standard Ada restriction No_Dispatch applied.
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → N/A
Verification Method → Compiler restrictions
Mutually Exclusive → OOP01
98.2.1 Reference
N/A
98.2.2 Description
In this approach, class-wide constructs are allowed, as well as tagged types and type ex-
tension (inheritance), but dynamic dispatching remains disallowed (i.e., as in OOP01).
This rule ensures there are no class-wide values passed as the parameter to a primitive
operation of a tagged type, hence there are no dynamically dispatched calls.
Note that this rule should not be applied without due consideration.
Some_Primitive (X);
98.2.6 Notes
N/A
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → High
Verification Method → GNATcheck rule: Deep_Inheritance_Hierarchies:2
98.3.1 Reference
98.3.2 Description
A class inheritance hierarchy consists of a set of types related by inheritance. Each class,
other than the root class, is a subclass of other classes, and each, except for "leaf" nodes,
is a base class for those that are derived from it.
Improperly designed inheritance hierarchies complicate system maintenance and increase
the effort in safety certification, in any programming language.
A common characteristic of problematic hierarchies is "excessive" depth, in which a given
class is a subclass of many other classes. Depth can be a problem because a change to
a class likely requires inspection, modification, recompilation, and retesting/reverification
of all classes below it in the hierarchy. The extent of that effect increases as we approach
the root class. This rippling effect is known as the fragile base class problem. Clearly, the
greater the depth the more subclasses there are to be potentially affected. In addition, note
that a change to one class may cause a cascade of other secondary changes to subclasses,
so the effect is often not limited to a single change made to all the subclasses in question.
Deep inheritance hierarchies also contribute to complexity, rather than lessening it, by re-
quiring the reader to understand multiple superclasses in order to understand the behavior
of a given subclass.
The threshold for "too deep" is inexact, but beyond around 4 or 5 levels the complexity
accelerates rapidly.
98.3.6 Notes
N/A
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Medium (easy fix, but a difficult to detect bug)
Verification Method → GNATcheck rule: Direct_Calls_To_Primitives
98.4.1 Reference
N/A
98.4.2 Description
This rule applies only to tagged types, when visibly tagged at the point of a call from one
primitive to another of that same type.
By default, subprogram calls are statically dispatched. Dynamic dispatching only occurs
when a class-wide value is passed to a primitive operation of a specific type. Forcing an
otherwise optional dynamic dispatching call in this case is known as redispatching.
When one primitive operation of a given tagged type invokes another distinct primitive
operation of that same type, use redispatching so that an overriding version of that other
primitive will be invoked if it exists. Otherwise an existing overridden version would not be
invoked, which is very likely an error.
This rule does not apply to the common case in which an overriding of a primitive operation
calls the "parent" type's version of the overridden operation. Such calls occur in the over-
ridden body when the new version is not replacing, but rather, is augmenting the parent
type's version. In this case the new version must do whatever the parent version did, and
can then add functionality specific to the new type.
By default, this rule applies to another common case in which static calls from one primitive
operation to another make sense. Specifically, constructors are often implemented in Ada
as functions that create a new value of the tagged type. As constructors, these functions
are type-specific. They must call the primitive operations of the type being created, not
operations that may be overridden for some type later derived from it. (Note that there is
a GNATcheck rule parameter to not flag this case.)
Typically constructor functions only have the tagged type as the result type, not as the type
for formal parameters, if any, because actual parameters of the tagged type would them-
selves likely require construction. This specific usage is the case ignored by the GNATcheck
rule parameter.
Note that constructors implemented as procedures also call primitive operations of the
specific type, for the same reasons as constructor functions. This usage is allowed by this
rule and does not require the GNATcheck parameter. (The difference between function and
procedure constructors is that these procedures will have a formal parameter of the tagged
type, of mode out.)
• 6.42 Violations of the Liskov substitution principle of the contract model [BLP]
• 6.43 Redispatching [PPH]
• 6.44 Polymorphic variables [BKK]
Class constructs
package Root is
type Root_T is tagged null record;
procedure Noncompliant (X : in out Root_T) is null;
procedure Compliant (X : in out Root_T) is null;
procedure Other_Prim (X : in out Root_T) is null;
end Root;
package Child is
use Root;
(continues on next page)
Noncompliant Code
98.4.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
Remediation → Low
Verification Method → GNATcheck rule: Style_Checks:O
98.5.1 Reference
98.5.2 Description
The declaration of a primitive operation that overrides an inherited operation must include
an explicit overriding annotation.
The semantics of inheritance in mainstream object-oriented languages may result in two
kinds of programming errors: 1) intending, but failing, to override an inherited subprogram,
and 2) intending not to override an inherited subprogram, but doing so anyway. Because
an overridden subprogram may perform subclass-specific safety or security checks, the
invocation of the parent subprogram on a subclass instance can introduce a vulnerability.
The first issue (intending but failing to override) typically occurs when the subprogram
name is misspelled. In this case a new or overloaded subprogram is actually declared.
The second issue (unintended overriding) can arise when a new subprogram is added to a
parent type in an existing inheritance hierarchy. The new subprogram happens to cause
one or more inherited subprograms below it to override the new superclass version. This
mistake typically happens during program maintenance.
In Ada, much like other modern languages, one can annotate a subprogram declaration
(and body) with an indication that the subprogram is an overriding of an inherited version.
This is done with the overriding reserved word preceding the subprogram specification.
Similarly, in Ada one can explicitly indicate that a subprogram is not an overriding. To do
so, the programmer includes the reserved words not overriding immediately prior to the
subprogram specification.
Of course, incorrect marking errors are flagged by the compiler. If a subprogram is explic-
itly marked as overriding but is not actually overriding, the compiler will reject the code.
Likewise, if a primitive subprogram is explicitly marked as not overriding, but actually is
overriding, the compiler will reject the code.
However, most subprograms are not overriding so it would be a heavy burden on the pro-
grammer to make them explicitly indicate that fact. That's not to mention the relatively
heavy syntax required.
In addition, both annotations are optional for the sake of compatibility with prior versions
of the language. Therefore, a subprogram without either annotation might or might not be
overriding. A legal program could contain some explicitly annotated subprograms and some
that are not annotated at all. But because the compiler will reject explicit annotations that
are incorrect, all we require is that one of the two cases be explicitly indicated for all such
subprograms. Any unannotated subprograms not flagged as errors are then necessarily
not that case, they must be the other one.
Since overriding is less common and involves slightly less syntax to annotate, the guideline
requires explicit annotations only for overriding subprograms. It follows that any subpro-
grams not flagged as errors by the compiler are not overriding, so they need not be marked
explicitly as such.
This guideline is implemented by compiler switches, or alternatively, by a GNATcheck rule
(specified below the table). With this guideline applied and enforced, the two inheritance
errors described in the introduction cannot happen.
Note that the compiler switches will also require the explicit overriding indicator when over-
riding a language-defined operator. The switches also apply to inherited primitive subpro-
grams for non-tagged types.
98.5.6 Notes
This rule requires the GNAT compiler switches -gnatyO and -gnatwe in order for the com-
piler to flag missing overriding annotations as errors. The first causes the compiler to gen-
erate the warnings, and the second causes those warnings to be treated as errors.
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → GNATcheck rule: Specific_Pre_Post
98.6.1 Reference
98.6.2 Description
For primitive operations of tagged types, use only class-wide pre/post contracts, if any.
The class-wide form of precondition and postcondition expresses conditions that are in-
tended to apply to any version of the subprogram. Therefore, when a subprogram is de-
rived as part of inheritance, only the class-wide form of those contracts is inherited from
the parent subprogram, if any are defined. As a result, it only makes sense to use the
class-wide form in this situation.
(The same semantics and recommendation applies to type invariants.)
Note: this approach will be required for OOP07 (Ensure Local Type Consistency).
• 6.42 Violations of the Liskov substitution principle or the contract model [BLP]
375 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/source/how_to_write_object_oriented_
contracts.html#writing-contracts-on-dispatching-subprograms
98.6.6 Notes
N/A
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → N/A
Verification Method → Software test
98.7.1 Reference
98.7.2 Description
Either:
• Formally verify local type consistency, or
• Ensure that each tagged type passes all the tests of all the parent types which it can
replace.
Rationale:
One of the fundamental benefits of OOP is the ability to manipulate objects in a class in-
heritance hierarchy without "knowing" at compile-time the specific classes of the objects
being manipulated. By manipulate we mean invoking the primitive operations, the methods
defined by the classes.
We will use the words class and type interchangeably, because classes are composed in Ada
and SPARK using a combination of building blocks, especially type declarations. However,
we will use the term subclass rather than subtype because the latter has a specific meaning
in Ada and SPARK that is unrelated to OOP.
376 https://docs.adacore.com/gnat_ugn-docs/html/gnat_ugn/gnat_ugn/gnat_utility_programs.html
The objects whose operations are being invoked can be of types anywhere in the inheritance
tree, from the root down to the bottom. The location, i.e., the specific type, is transparent to
the manipulating code. This type transparency is possible because the invoked operations
are dynamically dispatched at run-time, rather than statically dispatched at compile-time.
Typically, the code manipulating the objects does so in terms of superclasses closer to the
root of the inheritance tree. Doing so increases generality because it increases the number
of potential subclasses that can be manipulated. The actual superclass chosen will depend
on the operations required by the manipulation. In Ada and SPARK, subclasses can add
operations but can never remove them, so more operations are found as we move down
from the root. That is the nature of specialization. Note that the guarantee of an invoked
operations' existence is essential for languages used in this domain.
However, for this transparent manipulation to be functionally correct — to accomplish what
the caller intends — the primitive operations of subclasses must be functionally indistin-
guishable from those of the superclasses. That doesn't mean the subclasses cannot make
changes. Indeed, the entire point of subclasses is to make changes. In particular, functional
changes can be either introduction of new operations, or overridings of inherited operations.
It is these overridings that must be functionally transparent to the manipulating code. (Of
course, for an inherited operation that is not overridden, the functionality is inherited as-is,
and is thus transparent trivially.)
The concept of functional transparency was introduced, albeit with different terminology,
by Liskov and Wing in 1994 [LiskovWing1994] and is, therefore, known as the Liskov Substi-
tution Principle, or LSP. The substitution in LSP means that a subclass must be substitutable
for its superclass, i.e., a subclass instance should be usable whenever a superclass instance
is required.
Unfortunately, requirements-based testing will not detect violations of LSP because unit-
level requirements do not concern themselves with superclass substitutability.
However, the OO supplement of DO-178C [DO178C] offers solutions, two of which are in
fact the options recommended by this guideline.
Specifically, the supplement suggests adding a specific verification activity it defines as
Local Type Consistency Verification. This activity ensures LSP is respected and, in so doing,
addresses the vulnerability.
Verification can be accomplished statically with formal methods in SPARK, or dynamically
via a modified form of testing.
For the static approach, type consistency is verified by examining the properties of the over-
riding operation's preconditions and postconditions. These are the properties required by
Design by Contract, as codified by Bertrand Meyer [Meyer1997]. Specifically, an overridden
primitive may only replace the precondition with one weaker than that of the parent ver-
sion, and may only replace the postcondition with one stronger. In essence, relative to the
parent version, an overridden operation can only require the same or less, and provide the
same or more. Satisfying that requirement is sufficient to ensure functional transparency
because the manipulating code only "knows" the contracts of the class it manipulates, i.e.,
the view presented by the type, which may very well be a superclass of the one actually
invoked.
In Ada and SPARK, the class-wide form of preconditions and postconditions are inherited
by overridden primitive operations of tagged types. The inherited precondition and that
of the overriding (if any) are combined into a conjunction. All must hold, otherwise the
precondition fails. Likewise, the inherited postcondition is combined with the overriding
postcondition into a disjunction. At least one of them must hold. In Ada these are tested
at run-time. In SPARK, they are verified statically (or not, in which case proof fails and an
error is indicated).
To verify substitutability via testing, all the tests for all superclass types are applied to
objects of the given subclass type. If all the parent tests pass, this provides a high degree
of confidence that objects of the new tagged type can properly substitute for parent type
objects. Note that static proof of consistency provides an even higher degree of confidence.
• 6.42 Violations of the Liskov substitution principle of the contract model [BLP]
• 6.43 Redispatching [PPH]
• 6.44 Polymorphic variables [BKK]
package P is
pragma Elaborate_Body;
type Rectangle is tagged private;
procedure Set_Width (This : in out Rectangle;
Value : Positive)
with
Post => Width (This) = Value and
Height (This) = Height (This'Old);
private
...
end P;
The postcondition for Set_Width states that the Height is not changed. Likewise, for
Set_Height, the postcondition asserts that the Width is not changed. However, these
postconditions are not class-wide so they are not inherited by subclasses.
Now, in a subclass Square, the operations are overridden so that setting the width also
sets the height to the same value, and vice versa. Thus the overridden operations do not
maintain type consistency, but this fact is neither detected at run-time, nor could SPARK
verify it statically (and SPARK is not used at all in these versions of the packages).
with P; use P;
package Q is
pragma Elaborate_Body;
type Square is new Rectangle with private;
overriding
procedure Set_Width (This : in out Square;
Value : Positive)
with
Post => Width (This) = Height (This);
overriding
procedure Set_Height (This : in out Square;
Value : Positive)
(continues on next page)
private
...
end Q;
private
...
end P;
Now the postconditions are class-wide so they are inherited by subclasses. In the subclass
Square, the postconditions will not hold at run-time. Likewise, SPARK can now prove that
type consistency is not verified because the postconditions are weaker than those inherited:
with P; use P;
package Q with SPARK_Mode is
pragma Elaborate_Body;
type Square is new Rectangle with private;
overriding
procedure Set_Width (This : in out Square;
Value : Positive)
with
Post'Class => Width (This) = Height (This);
overriding
procedure Set_Height (This : in out Square;
Value : Positive)
with
Post'Class => Width (This) = Height (This);
private
type Square is new Rectangle with null record;
end Q;
98.7.6 Notes
Verification can be achieved dynamically with the GNATtest tool, using the
--validate-type-extensions switch. SPARK enforces this rule.
NINETYNINE
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
Security
✓
Description
These rules promote "best practices" for software development.
Rules
SWE01, SWE02, SWE03, SWE04
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
✓
1791
Learning Ada
99.1.1 Reference
99.1.2 Description
SPARK has proven itself highly effective, both in terms of low defects, low development
costs, and high productivity. The guideline advises extensive use of SPARK, especially for
the sake of formally proving the most critical parts of the source code. The rest of the code
can be in SPARK as well, even if formal proof is not intended, with some parts in Ada when
features outside the SPARK subset are essential.
N/A
Any code outside the (very large) SPARK subset is flagged by the compiler.
N/A
99.1.6 Notes
Level → Required
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
377 https://docs.adacore.com/live/wave/spark2014/html/spark2014_ug/en/usage_scenarios.html
Reliability
✓
Portability
Performance
Security
✓
Remediation → Low
Verification Method → Compiler restrictions
99.2.1 Reference
Power of 10 rule #10: "All code must be compiled, from the first day of development, with
all compiler warnings enabled at the most pedantic setting available. All code must compile
without warnings."
99.2.2 Description
The Ada compiler does a degree of static analysis itself, and generates many warnings
when they are enabled. These warnings likely indicate very real problems so they should
be examined and addressed, either by changing the code or disabling the warning for the
specific occurrence flagged in the source code.
To ensure that warnings are examined and addressed one way or the other, the compiler
must be configured to treat warnings as errors, i.e., preventing object code generation.
Note that warnings will occasionally be given for code usage that is intentional. In those
cases the warnings should be disabled by using pragma Warnings with the parameter
Off, and a string indicating the error message to be disabled. In other cases, a different
mechanism might be appropriate, such as aspect (or pragma) Unreferenced.
The formal parameter controls dispatching for the sake of selecting the subprogram to be
called but does not participate in the implementation of the body.
The compiler will no longer issue a warning that the formal parameter This is not refer-
enced. Of course, if that changes and This becomes referenced, the compiler will flag the
pragma.
99.2.6 Notes
This rule can be applied via the GNAT -gnatwae compiler switch, which both enables warn-
ings and treats them as errors. Note that the switch enables almost all optional warnings,
but not all. Some optional warnings correspond to very specific circumstances, and would
otherwise generate too much noise for their value.
Level → Mandatory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
✓
Performance
✓
Security
✓
Remediation → High
Verification Method → Static analysis tools
99.3.1 Reference
Power of 10 rule #10: "All code must also be checked daily with at least one, but preferably
more than one, strong static source code analyzer and should pass all analyses with zero
warnings."
99.3.2 Description
If not using SPARK for regular development, use a static analyzer, such as CodePeer, ex-
tensively. No warnings or errors should remain unresolved at the given level adopted for
analysis (which can be selected to adjust the false positive ratio).
Specifically, any code checked into the configuration management system must be checked
by the analyzer and be error-free prior to check-in. Similarly, each nightly build should
produce a CodePeer baseline for the project.
N/A
N/A
99.3.6 Notes
CodePeer is the recommended static analyzer. Note that CodePeer can detect GNATcheck
rule violations (via the --gnatcheck CodePeer switch and a rules file).
Level → Advisory
Category
Safety
✓
Cyber
✓
Goal
Maintainability
✓
Reliability
✓
Portability
Performance
Security
✓
Remediation → High, as retrofit can be extensive
Verification Method → GNATcheck rule: Visible_Components
99.4.1 Reference
MISRA C Rule 8.7: "Functions and objects should not be defined with external linkage if they
are referenced in only one translation unit."
99.4.2 Description
Do not make implementation artifacts compile-time visible to clients. Only make available
those declarations that define the abstraction presented to clients by the component. In
other words, define Abstract Data Types and use the language to enforce the abstraction.
This is a fundamental Object-Oriented Design principle.
This guideline minimizes client dependencies and thus allows the maximum flexibility for
changes in the underlying implementation. It minimizes the editing changes required for
client code when implementation changes are made.
This guideline also limits the region of code required to find any bugs to the package and
child packages, if any, defining the abstraction.
This guideline is to be followed extensively as the design default for components. Once the
application code size becomes non-trivial, the cost of retrofit is extremely high.
N/A
package Noncompliant is
type Content_T is array (Capacity_T range <>) of Integer;
type Stack_T (Capacity : Capacity_T) is tagged record
Content : Content_T (1 .. Capacity);
Top : Capacity_T := 0;
end record;
procedure Push
(Stack : in out Stack_T;
Item : Integer);
procedure Pop
(Stack : in out Stack_T;
Item : out Integer);
end Noncompliant;
Note that both type Content_T, as well as the record type components of type Stack_T,
are visible to clients. Client code may declare variables of type Content_T and may di-
rectly access and modify the record components. Bugs introduced via this access could be
anywhere in the entire client codebase.
package Compliant is
type Stack_T (Capacity : Capacity_T) is tagged private;
procedure Push
(Stack : in out Stack_T;
Item : Integer);
procedure Pop
(Stack : in out Stack_T;
Item : out Integer);
private
type Content_T is array (Capacity_T range <>) of Integer;
type Stack_T (Capacity : Capacity_T) is tagged record
Content : Content_T (1 .. Capacity);
Top : Capacity_T := 0;
end record;
end Compliant;
Type Content_T, as well as the record type components of type Stack_T, are no longer
visible to clients. Any bugs in the stack processing code must be in this package, or its
child packages, if any.
99.4.6 Notes
REFERENCES
379 http://docs.adacore.com/spark2014-docs/html/ug/index.html
380 http://docs.adacore.com/live/wave/gnat_ugn/html/gnat_ugn/gnat_ugn.html
381 http://docs.adacore.com/live/wave/gnatstack/html/gnatstack_ug/index.html
1799
Learning Ada
Introduction to Ada:
Laboratories
1801
Learning Ada
These labs contain exercises for the Introduction to Ada (page 5) course.
This document was written by Gustavo A. Hoffmann and reviewed by Michael Frank.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
382 http://creativecommons.org/licenses/by-sa/4.0
1803
Learning Ada
1804
CHAPTER
ONE
IMPERATIVE LANGUAGE
For the exercises below (except for the first one), don't worry about the details of the Main
procedure. You should just focus on implementing the application in the subprogram spec-
ified by the exercise.
Listing 1: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4 begin
5 -- Implement the application here!
6 null;
7 end Main;
101.2 Greetings
1805
Learning Ada
Listing 2: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Main is
5
12 begin
13 if Argument_Count < 1 then
14 Put_Line ("ERROR: missing arguments! Exiting...");
15 return;
16 elsif Argument_Count > 1 then
17 Put_Line ("Ignoring additional arguments...");
18 end if;
19
Listing 3: classify_number.ads
1 procedure Classify_Number (X : Integer);
Listing 4: classify_number.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
Listing 5: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Classify_Number;
5
6 procedure Main is
7 A : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Classify_Number (A);
19 end Main;
101.4 Numbers
Listing 6: display_numbers.ads
1 procedure Display_Numbers (A, B : Integer);
Listing 7: display_numbers.adb
1 procedure Display_Numbers (A, B : Integer) is
2 begin
3 -- Implement the application here!
4 null;
5 end Display_Numbers;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Display_Numbers;
5
6 procedure Main is
7 A, B : Integer;
8 begin
9 if Argument_Count < 2 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
(continues on next page)
TWO
SUBPROGRAMS
Listing 1: subtract.ads
1 -- Write the correct parameters for the procedure below.
2 procedure Subtract;
Listing 2: subtract.adb
1 procedure Subtract is
2 begin
3 -- Implement the procedure here.
4 null;
5 end Subtract;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Subtract;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Sub_10_1_Chk,
9 Sub_10_100_Chk,
10 Sub_0_5_Chk,
11 Sub_0_Minus_5_Chk);
12
1809
Learning Ada
32 begin
33 if Argument_Count < 1 then
34 Put_Line ("ERROR: missing arguments! Exiting...");
35 return;
36 elsif Argument_Count > 1 then
37 Put_Line ("Ignoring additional arguments...");
38 end if;
39
Listing 4: subtract.ads
1 -- Write the correct signature for the function below.
2 -- Don't forget to replace the keyword "procedure" by "function."
3 procedure Subtract;
Listing 5: subtract.adb
1 procedure Subtract is
2 begin
3 -- Implement the function here!
4 null;
5 end Subtract;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Subtract;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Sub_10_1_Chk,
9 Sub_10_100_Chk,
10 Sub_0_5_Chk,
(continues on next page)
32 begin
33 if Argument_Count < 1 then
34 Put_Line ("ERROR: missing arguments! Exiting...");
35 return;
36 elsif Argument_Count > 1 then
37 Put_Line ("Ignoring additional arguments...");
38 end if;
39
Goal: write a function that compares two values and returns a flag.
Steps:
1. Complete the Is_Equal subprogram.
Requirements:
1. Is_Equal returns a flag as a Boolean value.
2. The flag must indicate whether the values are equal (flag is True) or not (flag is False).
Listing 7: is_equal.ads
1 -- Write the correct signature for the function below.
2 -- Don't forget to replace the keyword "procedure" by "function."
3 procedure Is_Equal;
Listing 8: is_equal.adb
1 procedure Is_Equal is
2 begin
3 -- Implement the function here!
4 null;
5 end Is_Equal;
Listing 9: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Is_Equal;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Equal_Chk,
9 Inequal_Chk);
10
25 Result : Boolean;
26 begin
27 case TC is
28 when Equal_Chk =>
29 for I in 0 .. 10 loop
30 Result := Is_Equal (I, I);
31 Display_Equal (I, I, Result);
32 end loop;
33 when Inequal_Chk =>
34 for I in 0 .. 10 loop
35 Result := Is_Equal (I, I - 1);
36 Display_Equal (I, I - 1, Result);
37 end loop;
38 end case;
39 end Check;
40
41 begin
42 if Argument_Count < 1 then
43 Put_Line ("ERROR: missing arguments! Exiting...");
44 return;
45 elsif Argument_Count > 1 then
46 Put_Line ("Ignoring additional arguments...");
47 end if;
48
102.4 States
Number State
0 Off
1 On: Simple Processing
2 On: Advanced Processing
2. The procedure Display_State receives the number corresponding to a state and dis-
plays the state (indicated by the table above) as a user message.
Remarks:
1. You can use a case statement to implement this procedure.
4 with Display_State;
5
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Display_State (State);
19 end Main;
102.5 States #2
4 procedure Main is
5 S : constant String := Get_Hello;
6 begin
7 Put_Line (S);
8 end Main;
2. You can reuse your previous implementation and replace it by a case expression.
1. For values that do not correspond to a state, you can simply return an empty string
("").
4 with Get_State;
(continues on next page)
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
102.6 States #3
4 with Display_On_Off;
5 with Is_On;
6
7 procedure Main is
8 State : Integer;
9 begin
10 if Argument_Count < 1 then
11 Put_Line ("ERROR: missing arguments! Exiting...");
12 return;
13 elsif Argument_Count > 1 then
14 Put_Line ("Ignoring additional arguments...");
15 end if;
16
19 Display_On_Off (State);
20 Put_Line (Boolean'Image (Is_On (State)));
21 end Main;
102.7 States #4
4 with Set_Next;
5
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Set_Next (State);
19 Put_Line (Integer'Image (State));
20 end Main;
THREE
MODULAR PROGRAMMING
103.1 Months
Listing 1: months.ads
1 -- Create specification for Months package, which includes
2 -- the declaration of the Display_Months procedure.
3 --
4 procedure Months;
Listing 2: months.adb
1 -- Create body of Months package, which includes
2 -- the implementation of the Display_Months procedure.
3 --
4 procedure Months is
5
6 procedure Display_Months is
7 begin
8 Put_Line ("Months:");
9 Put_Line ("- " & Jan);
10 Put_Line ("- " & Feb);
11 Put_Line ("- " & Mar);
(continues on next page)
1819
Learning Ada
23 begin
24 null;
25 end Months;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7
8 type Test_Case_Index is
9 (Months_Chk);
10
19 begin
20 if Argument_Count < 1 then
21 Put_Line ("ERROR: missing arguments! Exiting...");
22 return;
23 elsif Argument_Count > 1 then
24 Put_Line ("Ignoring additional arguments...");
25 end if;
26
103.2 Operations
Listing 4: operations.ads
1 package Operations is
2
7 end Operations;
Listing 5: operations.adb
1 package body Operations is
2
6 end Operations;
Listing 6: operations-test.ads
1 package Operations.Test is
2
9 end Operations.Test;
Listing 7: operations-test.adb
1 package body Operations.Test is
2
18 end Operations.Test;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Operations;
5 with Operations.Test; use Operations.Test;
6
7 procedure Main is
8
9 type Test_Case_Index is
10 (Operations_Chk,
11 Operations_Display_Chk);
12
31 begin
32 if Argument_Count < 1 then
33 Put_Line ("ERROR: missing arguments! Exiting...");
34 return;
35 elsif Argument_Count > 1 then
36 Put_Line ("Ignoring additional arguments...");
37 end if;
38
FOUR
104.1 Colors
Goal: create a package to represent HTML colors in hexadecimal form and its corresponding
names.
Steps:
1. Implement the Color_Types package.
1. Declare the HTML_Color enumeration.
2. Declare the Basic_HTML_Color enumeration.
3. Implement the To_Integer function.
4. Implement the To_HTML_Color function.
Requirements:
1. Enumeration HTML_Color has the following colors:
• Salmon
• Firebrick
• Red
• Darkred
• Lime
• Forestgreen
• Green
• Darkgreen
• Blue
• Mediumblue
• Darkblue
2. Enumeration Basic_HTML_Color has the following colors: Red, Green, Blue.
3. Function To_Integer converts from the HTML_Color type to the HTML color
code — as integer values in hexadecimal notation.
• You can find the HTML color codes in the table below.
4. Function To_HTML_Color converts from Basic_HTML_Color to HTML_Color.
5. This is the table to convert from an HTML color to a HTML color code in hex-
adecimal notation:
1823
Learning Ada
Remarks:
1. In order to express the hexadecimal values above in Ada, use the following syntax:
16#<hex_value># (e.g.: 16#FFFFFF#).
2. For function To_Integer, you may use a case for this.
Listing 1: color_types.ads
1 package Color_Types is
2
Listing 2: color_types.adb
1 package body Color_Types is
2
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Integer_Text_IO;
4
7 procedure Main is
8 type Test_Case_Index is
9 (HTML_Color_Range,
10 HTML_Color_To_Integer,
11 Basic_HTML_Color_To_HTML_Color);
12
34 begin
35 if Argument_Count < 1 then
36 Put_Line ("ERROR: missing arguments! Exiting...");
37 return;
38 elsif Argument_Count > 1 then
39 Put_Line ("Ignoring additional arguments...");
40 end if;
41
104.2 Integers
Listing 4: int_types.ads
1 package Int_Types is
2
25 end Int_Types;
Listing 5: int_types.adb
1 package body Int_Types is
2
49 end Int_Types;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 package I_100_IO is new Ada.Text_IO.Integer_IO (I_100);
8 package U_100_IO is new Ada.Text_IO.Modular_IO (U_100);
9 package D_50_IO is new Ada.Text_IO.Integer_IO (D_50);
10
11 use I_100_IO;
12 use U_100_IO;
13 use D_50_IO;
14
15 type Test_Case_Index is
16 (I_100_Range,
17 U_100_Range,
18 U_100_Wraparound,
19 U_100_To_I_100,
20 I_100_To_U_100,
21 D_50_Range,
22 S_50_Range,
23 I_100_To_D_50,
24 I_100_To_S_50,
25 D_50_To_I_100,
26 S_50_To_I_100);
27
34 case TC is
35 when I_100_Range =>
36 Put (I_100'First);
37 New_Line;
38 Put (I_100'Last);
39 New_Line;
40 when U_100_Range =>
(continues on next page)
93 begin
94 if Argument_Count < 1 then
95 Put_Line ("ERROR: missing arguments! Exiting...");
96 return;
97 elsif Argument_Count > 1 then
98 Put_Line ("Ignoring additional arguments...");
99 end if;
100
104.3 Temperatures
Listing 7: temperature_types.ads
1 package Temperature_Types is
2
Listing 8: temperature_types.adb
1 package body Temperature_Types is
2
Listing 9: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 package Celsius_IO is new Ada.Text_IO.Float_IO (Celsius);
8 package Kelvin_IO is new Ada.Text_IO.Float_IO (Kelvin);
(continues on next page)
11 use Celsius_IO;
12 use Kelvin_IO;
13 use Int_Celsius_IO;
14
15 type Test_Case_Index is
16 (Celsius_Range,
17 Celsius_To_Int_Celsius,
18 Int_Celsius_To_Celsius,
19 Kelvin_To_Celsius,
20 Celsius_To_Kelvin);
21
28 case TC is
29 when Celsius_Range =>
30 Put (Celsius'First);
31 New_Line;
32 Put (Celsius'Last);
33 New_Line;
34 when Celsius_To_Int_Celsius =>
35 Put (To_Int_Celsius (Celsius'First));
36 New_Line;
37 Put (To_Int_Celsius (0.0));
38 New_Line;
39 Put (To_Int_Celsius (Celsius'Last));
40 New_Line;
41 when Int_Celsius_To_Celsius =>
42 Put (To_Celsius (Int_Celsius'First));
43 New_Line;
44 Put (To_Celsius (0));
45 New_Line;
46 Put (To_Celsius (Int_Celsius'Last));
47 New_Line;
48 when Kelvin_To_Celsius =>
49 Put (To_Celsius (Kelvin'First));
50 New_Line;
51 Put (To_Celsius (0));
52 New_Line;
53 Put (To_Celsius (Kelvin'Last));
54 New_Line;
55 when Celsius_To_Kelvin =>
56 Put (To_Kelvin (Celsius'First));
57 New_Line;
58 Put (To_Kelvin (Celsius'Last));
59 New_Line;
60 end case;
61 end Check;
62
63 begin
64 if Argument_Count < 1 then
65 Put_Line ("ERROR: missing arguments! Exiting...");
66 return;
67 elsif Argument_Count > 1 then
68 Put_Line ("Ignoring additional arguments...");
69 end if;
(continues on next page)
FIVE
RECORDS
105.1 Directions
Listing 1: directions.ads
1 package Directions is
2
5 type Direction is
6 (North,
7 Northeast,
8 East,
9 Southeast,
10 South,
11 Southwest,
12 West,
13 Northwest);
(continues on next page)
1835
Learning Ada
28 end Directions;
Listing 2: directions.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
40 end Directions;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Direction_Chk);
9
26 begin
27 if Argument_Count < 1 then
28 Put_Line ("ERROR: missing arguments! Exiting...");
29 return;
30 elsif Argument_Count > 1 then
31 Put_Line ("Ignoring additional arguments...");
32 end if;
33
105.2 Colors
Goal: create a package to represent HTML colors in RGB format using the hexadecimal
form.
Steps:
1. Implement the Color_Types package.
1. Declare the RGB record.
2. Implement the To_RGB function.
3. Implement the Image function for the RGB type.
Requirements:
1. The following table contains the HTML colors and the corresponding value in hexadec-
imal form for each color element:
2. The hexadecimal information of each HTML color can be mapped to three color ele-
ments: red, green and blue.
1. Each color element has a value between 0 and 255, or 00 and FF in hexadecimal.
2. For example, for the color salmon, the hexadecimal value of the color elements
are:
• red = FA,
• green = 80, and
• blue = 72.
3. Record RGB stores information about HTML colors in RGB format, so that we can retrieve
the individual color elements.
4. Function To_RGB converts from the HTML_Color enumeration to the RGB type based on
the information from the table above.
5. Function Image returns a string representation of the RGB type in this format:
• "(Red => 16#..#, Green => 16#...#, Blue => 16#...# )"
Remarks:
1. We use the exercise on HTML colors from the previous lab on Strongly typed language
(page 1823) as a starting point.
Listing 4: color_types.ads
1 package Color_Types is
2
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
18 type Basic_HTML_Color is
19 (Red,
(continues on next page)
39 end Color_Types;
Listing 5: color_types.adb
1 with Ada.Integer_Text_IO;
2
21 end To_Integer;
22
64 end Color_Types;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (HTML_Color_To_RGB);
9
21 begin
22 if Argument_Count < 1 then
23 Put_Line ("ERROR: missing arguments! Exiting...");
24 return;
25 elsif Argument_Count > 1 then
26 Put_Line ("Ignoring additional arguments...");
27 end if;
28
105.3 Inventory
Goal: create a simplified inventory system for a store to enter items and keep track of
assets.
Steps:
1. Implement the Inventory_Pkg package.
1. Declare the Item record.
2. Implement the Init function.
3. Implement the Add procedure.
Requirements:
1. Record Item collects information about products from the store.
1. To keep it simple, this record only contains the name, quantity and price of each
item.
2. The record components are:
• Name of Item_Name type;
• Quantity of Natural type;
• Price of Float type.
2. Function Init returns an initialized item (of Item type).
1. Function Init must also display the item name by calling the To_String function
for the Item_Name type.
• This is already implemented in the code below.
3. Procedure Add adds an item to the assets.
1. Since we want to keep track of the assets, the implementation must accumulate
the total value of each item's inventory, the result of multiplying the item quantity
and its price.
Listing 7: inventory_pkg.ads
1 package Inventory_Pkg is
2
3 type Item_Name is
4 (Ballpoint_Pen, Oil_Based_Pen_Marker, Feather_Quill_Pen);
5
19 end Inventory_Pkg;
Listing 8: inventory_pkg.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
33 end Inventory_Pkg;
Listing 9: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 -- Remark: the following line is not relevant.
8 F : array (1 .. 10) of Float := (others => 42.42);
9
10 type Test_Case_Index is
11 (Inventory_Chk);
12
16 use F_IO;
17 begin
18 Put ("Assets: $");
19 Put (Assets, 1, 2, 0);
20 Put (".");
21 New_Line;
22 end Display;
23
49 begin
50 if Argument_Count < 1 then
51 Put_Line ("ERROR: missing arguments! Exiting...");
52 return;
53 elsif Argument_Count > 1 then
54 Put_Line ("Ignoring additional arguments...");
55 end if;
56
SIX
ARRAYS
1845
Learning Ada
Listing 1: constrained_arrays.ads
1 package Constrained_Arrays is
2
21 end Constrained_Arrays;
Listing 2: constrained_arrays.adb
1 package body Constrained_Arrays is
2
6 end Constrained_Arrays;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Range_Chk,
9 Array_Range_Chk,
10 A_Obj_Chk,
11 Init_Chk,
12 Double_Chk,
13 First_Elem_Chk,
14 Last_Elem_Chk,
15 Length_Chk);
16
61 begin
62 if Argument_Count < 1 then
63 Put_Line ("ERROR: missing arguments! Exiting...");
64 return;
65 elsif Argument_Count > 1 then
66 Put_Line ("Ignoring additional arguments...");
67 end if;
68
Goal: rewrite a package to represent HTML colors in RGB format using a lookup table.
Steps:
1. Implement the Color_Types package.
1. Declare the array type HTML_Color_RGB.
2. Declare the To_RGB_Lookup_Table object and initialize it.
3. Adapt the implementation of the To_RGB function.
Requirements:
1. Array type HTML_Color_RGB is used for the table.
Listing 4: color_types.ads
1 package Color_Types is
2
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
36 end Color_Types;
Listing 5: color_types.adb
1 with Ada.Integer_Text_IO;
2 package body Color_Types is
3
9 -- Use the code below from the previous version of the To_RGB
10 -- function to declare the To_RGB_Lookup_Table:
11 --
12 -- case C is
13 -- when Salmon => return (16#FA#, 16#80#, 16#72#);
14 -- when Firebrick => return (16#B2#, 16#22#, 16#22#);
15 -- when Red => return (16#FF#, 16#00#, 16#00#);
16 -- when Darkred => return (16#8B#, 16#00#, 16#00#);
17 -- when Lime => return (16#00#, 16#FF#, 16#00#);
18 -- when Forestgreen => return (16#22#, 16#8B#, 16#22#);
19 -- when Green => return (16#00#, 16#80#, 16#00#);
20 -- when Darkgreen => return (16#00#, 16#64#, 16#00#);
21 -- when Blue => return (16#00#, 16#00#, 16#FF#);
22 -- when Mediumblue => return (16#00#, 16#00#, 16#CD#);
23 -- when Darkblue => return (16#00#, 16#00#, 16#8B#);
24 -- end case;
25
26 end To_RGB;
27
49 end Color_Types;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Color_Table_Chk,
9 HTML_Color_To_Integer_Chk);
10
27 begin
28 if Argument_Count < 1 then
29 Put_Line ("ERROR: missing arguments! Exiting...");
30 return;
31 elsif Argument_Count > 1 then
32 Put_Line ("Ignoring additional arguments...");
33 end if;
34
Listing 7: unconstrained_arrays.ads
1 package Unconstrained_Arrays is
2
15 end Unconstrained_Arrays;
Listing 8: unconstrained_arrays.adb
1 package body Unconstrained_Arrays is
2
14 end Unconstrained_Arrays;
Listing 9: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Init_Chk,
9 Init_Proc_Chk,
10 Double_Chk,
11 Diff_Prev_Chk,
12 Diff_Prev_Single_Chk);
13
30 begin
31 case TC is
32 when Init_Chk =>
33 AA := Init (AA'First, AA'Length);
34 AB := Init (AB'First, AB'Length);
35 Display (AA);
36 Display (AB);
37 when Init_Proc_Chk =>
38 Init (AA);
39 Init (AB);
40 Display (AA);
41 Display (AB);
42 when Double_Chk =>
43 Local_Init (AB);
44 Double (AB);
45 Display (AB);
46 when Diff_Prev_Chk =>
47 Local_Init (AB);
48 AB := Diff_Prev_Elem (AB);
49 Display (AB);
50 when Diff_Prev_Single_Chk =>
51 declare
52 A1 : My_Array (1 .. 1) := (1 => 42);
53 begin
54 A1 := Diff_Prev_Elem (A1);
55 Display (A1);
56 end;
57 end case;
58 end Check;
59
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
25 end Product_Info_Pkg;
13 end Product_Info_Pkg;
6 procedure Main is
7 package Currency_IO is new Ada.Text_IO.Float_IO (Currency);
8
9 type Test_Case_Index is
10 (Total_Func_Chk,
11 Total_Proc_Chk,
12 Total_Value_Chk);
13
17 P : Product_Infos (Test_Range);
18 Tots : Currency_Array (Test_Range);
19 Tot : Currency;
20
38 begin
39 Currency_IO.Default_Fore := 1;
40 Currency_IO.Default_Aft := 2;
41 Currency_IO.Default_Exp := 0;
42
43 case TC is
44 when Total_Func_Chk =>
45 Local_Init (P);
46 Tots := Total (P);
47 Display (Tots);
48 when Total_Proc_Chk =>
49 Local_Init (P);
50 Total (P, Tots);
51 Display (Tots);
52 when Total_Value_Chk =>
53 Local_Init (P);
54 Tot := Total (P);
55 Currency_IO.Put (Tot);
56 New_Line;
57 end case;
58 end Check;
59
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
106.5 String_10
14 end Strings_10;
7 end Strings_10;
6 procedure Main is
7 type Test_Case_Index is
8 (String_10_Long_Chk,
9 String_10_Short_Chk);
10
16 begin
17 case TC is
18 when String_10_Long_Chk =>
19 S_10 := To_String_10 (SL);
20 Put_Line (String (S_10));
21 when String_10_Short_Chk =>
22 S_10 := (others => ' ');
23 S_10 := To_String_10 (SS);
24 Put_Line (String (S_10));
25 end case;
26 end Check;
(continues on next page)
28 begin
29 if Argument_Count < 1 then
30 Ada.Text_IO.Put_Line ("ERROR: missing arguments! Exiting...");
31 return;
32 elsif Argument_Count > 1 then
33 Ada.Text_IO.Put_Line ("Ignoring additional arguments...");
34 end if;
35
NAME: XXXX
AGE: YY
Remarks:
1. In the implementation of procedure Add, you may use an index to indicate the last
valid position in the array — see Last_Valid in the code below.
2. In the implementation of procedure Display, you should use the Trim function from
the Ada.Strings.Fixed package to format the person's name — for example: Trim
(P.Name, Right).
3. You may need the Integer'Min (A, B) and the Integer'Max (A, B) functions to
get the minimum and maximum values in a comparison between two integer values
A and B.
4. Fixed-length strings can be initialized with whitespaces using the others syntax. For
example: S : String_10 := (others => ' ');
5. You may implement additional subprograms to deal with other types declared in the
Names_Ages package below, such as the Name_Type and the Person type.
1. For example, a function To_Name_Type to convert from String to Name_Type might
be useful.
2. Take a moment to reflect on which additional subprograms could be useful as well.
18 -- Replace type declaration for People record. You may use the
19 -- following template:
20 --
21 -- type People is record
22 -- People_A : People_Array ...;
23 -- Last_Valid : Natural;
24 -- end record;
25 --
26 type People is null record;
27
42 end Names_Ages;
36 end Names_Ages;
6 procedure Main is
7 type Test_Case_Index is
8 (Names_Ages_Chk,
9 Get_Age_Chk);
10
35 begin
36 if Argument_Count < 1 then
37 Ada.Text_IO.Put_Line ("ERROR: missing arguments! Exiting...");
38 return;
39 elsif Argument_Count > 1 then
40 Ada.Text_IO.Put_Line ("Ignoring additional arguments...");
41 end if;
42
SEVEN
1863
Learning Ada
Listing 1: aggregates.ads
1 package Aggregates is
2
7 procedure Init;
8
13 end Aggregates;
Listing 2: aggregates.adb
1 package body Aggregates is
2
5 end Aggregates;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 -- Remark: the following line is not relevant.
8 F : array (1 .. 10) of Float := (others => 42.42)
9 with Unreferenced;
10
11 type Test_Case_Index is
12 (Default_Rec_Chk,
13 Init_Rec_Chk,
14 Init_Some_Arr_Chk,
15 Init_Arr_Chk);
16
54 begin
55 if Argument_Count < 1 then
56 Put_Line ("ERROR: missing arguments! Exiting...");
57 return;
58 elsif Argument_Count > 1 then
59 Put_Line ("Ignoring additional arguments...");
60 end if;
61
107.2 Versioning
Listing 4: versioning.ads
1 package Versioning is
2
7 -- function Convert
8
9 end Versioning;
Listing 5: versioning.adb
1 with Ada.Strings; use Ada.Strings;
2 with Ada.Strings.Fixed; use Ada.Strings.Fixed;
3
23 end Versioning;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Ver_String_Chk,
9 Ver_Float_Chk);
10
22 begin
23 if Argument_Count < 1 then
24 Put_Line ("ERROR: missing arguments! Exiting...");
25 return;
26 elsif Argument_Count > 1 then
27 Put_Line ("Ignoring additional arguments...");
28 end if;
29
Listing 7: todo_lists.ads
1 package Todo_Lists is
2
17 end Todo_Lists;
Listing 8: todo_lists.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
16 end Todo_Lists;
Listing 9: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Todo_List_Chk);
9
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
35 Put_Line ("Ignoring additional arguments...");
36 end if;
37
procedure Test is
L : Price_List (10);
begin
Reset (L);
Add (L, 1.45);
Add (L, 2.37);
Display (L);
end Test;
PRICE LIST
1.45
2.37
Remarks:
1. To implement the package, you'll use the following features of the Ada language:
22 end Price_Lists;
25 end Price_Lists;
6 procedure Main is
7 type Test_Case_Index is
8 (Price_Type_Chk,
9 Price_List_Chk,
10 Price_List_Get_Chk);
11
15 procedure Local_Init_List is
16 begin
17 Reset (L);
18 Add (L, 1.45);
19 Add (L, 2.37);
20 Add (L, 3.21);
21 Add (L, 4.14);
22 Add (L, 5.22);
23 Add (L, 6.69);
24 Add (L, 7.77);
25 Add (L, 8.14);
26 Add (L, 9.99);
27 Add (L, 10.01);
28 end Local_Init_List;
29
48 end Get_Display;
49
50 begin
51 case TC is
52 when Price_Type_Chk =>
53 Put_Line ("The delta value of Price_Type is "
54 & Price_Type'Image (Price_Type'Delta) & ";");
55 Put_Line ("The minimum value of Price_Type is "
56 & Price_Type'Image (Price_Type'First) & ";");
57 Put_Line ("The maximum value of Price_Type is "
58 & Price_Type'Image (Price_Type'Last) & ";");
59 when Price_List_Chk =>
(continues on next page)
69 begin
70 if Argument_Count < 1 then
71 Put_Line ("ERROR: missing arguments! Exiting...");
72 return;
73 elsif Argument_Count > 1 then
74 Put_Line ("Ignoring additional arguments...");
75 end if;
76
EIGHT
PRIVACY
108.1 Directions
Goal: create a package that handles directions and geometric angles using a previous
implementation.
Steps:
1. Fix the implementation of the Test_Directions procedure.
Requirements:
1. The implementation of the Test_Directions procedure must compile correctly.
Remarks:
1. This exercise is based on the Directions exercise from the Records (page 1835) labs.
1. In this version, however, Ext_Angle is a private type.
2. In the implementation of the Test_Directions procedure below, the Ada developer
tried to initialize All_Directions — an array of Ext_Angle type — with aggregates.
1. Since we now have a private type, the compiler complains about this initialization.
3. To fix the implementation of the Test_Directions procedure, you should use the ap-
propriate function from the Directions package.
4. The initialization of All_Directions in the code below contains a consistency error
where the angle doesn't match the assessed direction.
1. See if you can spot this error!
2. This kind of errors can happen when record components that have correlated in-
formation are initialized individually without consistency checks — using private
types helps to avoid the problem by requiring initialization routines that can en-
force consistency.
Listing 1: directions.ads
1 package Directions is
2
5 type Direction is
6 (North,
7 Northwest,
8 West,
9 Southwest,
10 South,
11 Southeast,
12 East);
(continues on next page)
1875
Learning Ada
22 private
23
29 end Directions;
Listing 2: directions.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
34 end Directions;
Listing 3: test_directions.adb
1 with Directions; use Directions;
2
3 procedure Test_Directions is
(continues on next page)
14 begin
15 for I in All_Directions'Range loop
16 Display (All_Directions (I));
17 end loop;
18
19 end Test_Directions;
Listing 4: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Test_Directions;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Direction_Chk);
9
18 begin
19 if Argument_Count < 1 then
20 Put_Line ("ERROR: missing arguments! Exiting...");
21 return;
22 elsif Argument_Count > 1 then
23 Put_Line ("Ignoring additional arguments...");
24 end if;
25
1. For both Copy and =, the two parameters may refer to strings with different lengths.
We'll limit the implementation to just take the minimum length:
1. In case of copying the string "Hello World" to a string with 5 characters, the copied
string is "Hello":
2. When comparing "Hello World" to "Hello", the = operator indicates that these
strings are equivalent:
2. When copying from a short string to a longer string, the remaining characters of the
longer string must be initialized with underscores (_). For example:
Remarks:
1. As we've discussed in the course:
1. Variables of limited types have the following limitations:
• they cannot be assigned to;
• they don't have an equality operator (=).
2. We can, however, define our own, custom subprograms to circumvent these limi-
tations:
• In order to copy instances of a limited type, we can define a custom Copy
procedure.
• In order to compare instances of a limited type, we can define an = operator.
2. You can use the Min_Last constant — which is already declared in the implementation
of these subprograms — in the code you write.
3. Some details about the Limited_Strings package:
1. The Lim_String type acts as a container for strings.
1. In the the private part, Lim_String is declared as an access type to a String.
2. There are two versions of the Init function that initializes an object of Lim_String
type:
1. The first one takes another string.
2. The second one receives the number of characters for a string container.
3. Procedure Put_Line displays object of Lim_String type.
4. The design and implementation of the Limited_Strings package is very simplis-
tic.
1. A good design would have better handling of access types, for example.
Listing 5: limited_strings.ads
1 package Limited_Strings is
2
16 private
17
20 end Limited_Strings;
Listing 6: limited_strings.adb
1 with Ada.Text_IO;
2
44 end Limited_Strings;
Listing 7: check_lim_string.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Check_Lim_String is
6 S : constant String := "----------";
7 S1 : constant Lim_String := Init ("Hello World");
8 S2 : constant Lim_String := Init (30);
9 S3 : Lim_String := Init (5);
10 S4 : Lim_String := Init (S & S & S);
11 begin
12 Put ("S1 => ");
13 Put_Line (S1);
14 Put ("S2 => ");
15 Put_Line (S2);
16
17 if S1 = S2 then
18 Put_Line ("S1 is equal to S2.");
19 else
20 Put_Line ("S1 isn't equal to S2.");
21 end if;
22
27 if S1 = S3 then
28 Put_Line ("S1 is equal to S3.");
29 else
30 Put_Line ("S1 isn't equal to S3.");
31 end if;
32
37 if S1 = S4 then
38 Put_Line ("S1 is equal to S4.");
39 else
40 Put_Line ("S1 isn't equal to S4.");
41 end if;
42 end Check_Lim_String;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Lim_String_Chk);
9
18 begin
19 if Argument_Count < 1 then
20 Put_Line ("ERROR: missing arguments! Exiting...");
21 return;
22 elsif Argument_Count > 1 then
23 Put_Line ("Ignoring additional arguments...");
24 end if;
25
In previous labs, we had many source-code snippets containing records that could be de-
clared private. The source-code for the exercise above (Directions) is an example: we've
modified the type declaration of Ext_Angle, so that the record is now private. Encapsulat-
ing the record components — by declaring record components in the private part — makes
the code safer. Also, because many of the code snippets weren't making use of record
components directly (but handling record types via the API instead), they continue to work
fine after these modifications.
This exercise doesn't contain any source-code. In fact, the goal here is to modify previous
labs, so that the record declarations are made private. You can look into those labs, modify
the type declarations, and recompile the code. The corresponding test-cases must still
pass.
If no other changes are needed apart from changes in the declaration, then that indicates
we have used good programming techniques in the original code. On the other hand, if
further changes are needed, then you should investigate why this is the case.
Also note that, in some cases, you can move support types into the private part of the
specification without affecting its compilation. This is the case, for example, for the Peo-
ple_Array type of the List of Names lab mentioned below. You should, in fact, keep only
relevant types and subprograms in the public part and move all support declarations to the
private part of the specification whenever possible.
Below, you find the selected labs that you can work on, including changes that you should
make. In case you don't have a working version of the source-code of previous labs, you
can look into the corresponding solutions.
108.3.1 Colors
NINE
GENERICS
<HEADER>
<index #1>: <element #1>
<index #2>: <element #2>
...
4. For example:
• For the following code:
procedure Test is
A : Int_Array (1 .. 2) := (1, 5);
begin
Display_Int_Array ("Elements of A", A);;
end Test;
Elements of A
1: 1
2: 5
1883
Learning Ada
• This type must be declared in such a way that it can be mapped to any type
in the instantiation — including record types.
3. an array type T_Array using the T_Range and T_Element types;
4. a function Image that converts a variable of T_Element type to a String.
Listing 1: display_array.ads
1 generic
2 procedure Display_Array (Header : String;
3 A : T_Array);
Listing 2: display_array.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Display_Array;
5
6 procedure Main is
7 type Test_Case_Index is (Int_Array_Chk,
8 Point_Array_Chk);
9
10 procedure Test_Int_Array is
11 type Int_Array is array (Positive range <>) of Integer;
12
24 procedure Test_Point_Array is
25 type Point is record
26 X : Float;
27 Y : Float;
28 end record;
29
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
Goal: create a generic function that calculates the average of an array of floating-point
elements.
Steps:
1. Declare and implement the generic function Average.
Requirements:
1. Generic function Average calculates the average of an array containing floating-point
values of arbitrary precision.
2. Generic function Average must contain the following formal parameters:
1. a range type T_Range for the array;
2. a formal type T_Element that can be mapped to floating-point types of arbitrary
precision;
3. an array type T_Array using T_Range and T_Element;
Remarks:
1. You should use the Float type for the accumulator.
Listing 4: average.ads
1 generic
2 function Average (A : T_Array) return T_Element;
Listing 5: average.adb
1 function Average (A : T_Array) return T_Element is
2 begin
3 return 0.0;
4 end Average;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Average;
5
6 procedure Main is
7 type Test_Case_Index is (Float_Array_Chk,
8 Digits_7_Float_Array_Chk);
9
10 procedure Test_Float_Array is
11 type Float_Array is array (Positive range <>) of Float;
12
23 procedure Test_Digits_7_Float_Array is
24 type Custom_Float is digits 7 range 0.0 .. 1.0;
25
26 type Float_Array is
27 array (Integer range <>) of Custom_Float;
28
50 begin
51 if Argument_Count < 1 then
52 Put_Line ("ERROR: missing arguments! Exiting...");
53 return;
54 elsif Argument_Count > 1 then
55 Put_Line ("Ignoring additional arguments...");
56 end if;
57
Goal: create a generic function that calculates the average of an array of elements of any
arbitrary type.
Steps:
1. Declare and implement the generic function Average.
2. Implement the test procedure Test_Item.
1. Declare the F_IO package.
2. Implement the Get_Total function for the Item type.
3. Implement the Get_Price function for the Item type.
4. Declare the Average_Total function.
5. Declare the Average_Price function.
Requirements:
1. Generic function Average calculates the average of an array containing elements of
any arbitrary type.
2. Generic function Average has the same formal parameters as in the previous exercise,
except for:
1. T_Element, which is now a formal type that can be mapped to any arbitrary type.
2. To_Float, which is an additional formal parameter.
• To_Float is a function that converts the arbitrary element of T_Element type
to the Float type.
3. Procedure Test_Item is used to test the generic Average procedure for a record type
(Item).
1. Record type Item contains the Quantity and Price components.
4. The following functions have to implemented to be used for the formal To_Float func-
tion parameter:
1. For the Decimal type, the function is pretty straightforward: it simply returns the
floating-point value converted from the decimal type.
2. For the Item type, two functions must be created to convert to floating-point type:
1. Get_Total, which returns the multiplication of the quantity and the price com-
ponents of the Item type;
2. Get_Price, which returns just the price.
Remarks:
1. In this exercise, you'll abstract the Average function from the previous exercises a step
further.
1. In this case, the function shall be able to calculate the average of any arbitrary
type — including arrays containing elements of record types.
2. Since record types can be composed by many components of different types, we
need to provide a way to indicate which component (or components) of the record
will be used when calculating the average of the array.
3. This problem is solved by specifying a To_Float function as a formal parameter,
which converts the arbitrary element of T_Element type to the Float type.
4. In the implementation of the Average function, we use the To_Float function and
calculate the average using a floating-point variable.
Listing 7: average.ads
1 generic
2 function Average (A : T_Array) return Float;
Listing 8: average.adb
1 function Average (A : T_Array) return Float is
2 begin
3 null;
4 end Average;
Listing 9: test_item.ads
1 procedure Test_Item;
3 with Average;
4
5 procedure Test_Item is
6 type Amount is delta 0.01 digits 12;
7
13 type Item_Array is
14 array (Positive range <>) of Item;
15
16 A : constant Item_Array (1 .. 4)
17 := ((Quantity => 5, Price => 10.00),
18 (Quantity => 80, Price => 2.50),
19 (Quantity => 40, Price => 5.00),
20 (Quantity => 20, Price => 12.50));
21
22 begin
23 Put ("Average per item & quantity: ");
24 F_IO.Put (Average_Total (A));
25 New_Line;
26
4 with Test_Item;
5
6 procedure Main is
7 type Test_Case_Index is (Item_Array_Chk);
8
17 begin
18 if Argument_Count < 1 then
19 Put_Line ("ERROR: missing arguments! Exiting...");
20 return;
21 elsif Argument_Count > 1 then
22 Put_Line ("Ignoring additional arguments...");
23 end if;
24
Goal: create a system based on a generic list to add and displays elements.
Steps:
1. Declare and implement the generic package Gen_List.
1. Implement the Init procedure.
2. Implement the Add procedure.
3. Implement the Display procedure.
Requirements:
1. Generic package Gen_List must have the following subprograms:
1. Procedure Init initializes the list.
2. Procedure Add adds an item to the list.
1. This procedure must contain a Status output parameter that is set to False
when the list was full — i.e. if the procedure failed while trying to add the item;
3. Procedure Display displays the complete list.
1. This includes the name of the list and its elements — using one line per ele-
ment.
2. This is the expected format:
<NAME>
<element #1>
<element #2>
...
Remarks:
1. In previous labs, you've been implementing lists for a variety of types.
• The List of Names exercise from the Arrays (page 1845) labs is an example.
• In this exercise, you have to abstract those implementations to create the generic
Gen_List package.
4 procedure Init;
5
9 procedure Display;
10
11 end Gen_List;
5 procedure Init is
6 begin
7 null;
8 end Init;
9
16 procedure Display is
17 begin
18 null;
19 end Display;
20
21 end Gen_List;
3 with Gen_List;
4
5 procedure Test_Int is
6
9 A : Integer_Array (1 .. 3);
(continues on next page)
12 Success : Boolean;
13
22 end Display_Add_Success;
23
24 begin
25 Int_List.Init;
26
39 Int_List.Display;
40 end Test_Int;
4 with Test_Int;
5
6 procedure Main is
7 type Test_Case_Index is (Int_Chk);
8
17 begin
18 if Argument_Count < 1 then
19 Put_Line ("ERROR: missing arguments! Exiting...");
20 return;
21 elsif Argument_Count > 1 then
22 Put_Line ("Ignoring additional arguments...");
23 end if;
24
EXCEPTIONS
Listing 1: options.ads
1 package Options is
2
8 end Options;
Listing 2: options.adb
1 package body Options is
2
1893
Learning Ada
8 end Options;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Exceptions; use Ada.Exceptions;
4
7 procedure Main is
8 type Test_Case_Index is
9 (Options_Chk);
10
21 begin
22 case TC is
23 when Options_Chk =>
24 for O in Option loop
25 Check (O);
26 end loop;
27 end case;
28 end Check;
29
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
35 Put_Line ("Ignoring additional arguments...");
36 end if;
37
Listing 4: tests.ads
1 package Tests is
2
5 Custom_Exception : exception;
6
9 end Tests;
Listing 5: tests.adb
1 package body Tests is
2
20 end Tests;
Listing 6: check_exception.adb
1 with Tests; use Tests;
2
Listing 7: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Exceptions; use Ada.Exceptions;
4
8 procedure Main is
9 type Test_Case_Index is
10 (Exception_1_Chk,
11 Exception_2_Chk);
12
27 begin
28 case TC is
29 when Exception_1_Chk =>
30 Check_Handle_Exception (Test_1);
31 when Exception_2_Chk =>
32 Check_Handle_Exception (Test_2);
33 end case;
34 end Check;
35
36 begin
37 if Argument_Count < 1 then
38 Put_Line ("ERROR: missing arguments! Exiting...");
39 return;
40 elsif Argument_Count > 1 then
41 Put_Line ("Ignoring additional arguments...");
42 end if;
43
Listing 8: tests.ads
1 package Tests is
2
5 Custom_Exception : exception;
6
9 end Tests;
Listing 9: tests.adb
1 package body Tests is
2
20 end Tests;
8 procedure Main is
9 type Test_Case_Index is
10 (Exception_1_Chk,
11 Exception_2_Chk);
12
27 begin
28 case TC is
29 when Exception_1_Chk =>
30 Check_Handle_Exception (Test_1);
31 when Exception_2_Chk =>
32 Check_Handle_Exception (Test_2);
33 end case;
34 end Check;
35
36 begin
37 if Argument_Count < 1 then
38 Put_Line ("ERROR: missing arguments! Exiting...");
39 return;
40 elsif Argument_Count > 1 then
41 Put_Line ("Ignoring additional arguments...");
42 end if;
43
ONE
TASKING
Listing 1: display_services.ads
1 package Display_Services is
2
3 end Display_Services;
Listing 2: display_services.adb
1 package body Display_Services is
2
3 end Display_Services;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is (Display_Service_Chk);
8
1899
Learning Ada
23 begin
24 if Argument_Count < 1 then
25 Put_Line ("ERROR: missing arguments! Exiting...");
26 return;
27 elsif Argument_Count > 1 then
28 Put_Line ("Ignoring additional arguments...");
29 end if;
30
2. In the Event entry, you should use the Time type from the Ada.Real_Time package
for the time parameter.
3. Note that the test application below creates an array of event managers with different
delays.
Listing 4: event_managers.ads
1 package Event_Managers is
2
3 end Event_Managers;
Listing 5: event_managers.adb
1 package body Event_Managers is
2
7 end Event_Managers;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
7 procedure Main is
8 type Test_Case_Index is (Event_Manager_Chk);
9
26 begin
27 if Argument_Count < 1 then
28 Put_Line ("ERROR: missing arguments! Exiting...");
29 return;
30 elsif Argument_Count > 1 then
31 Put_Line ("Ignoring additional arguments...");
32 end if;
33
Listing 7: gen_queues.ads
1 package Gen_Queues is
2
3 end Gen_Queues;
Listing 8: gen_queues.adb
1 package body Gen_Queues is
2
3 end Gen_Queues;
Listing 9: queue_tests.ads
1 package Queue_Tests is
2
3 procedure Simple_Test;
4
5 procedure Concurrent_Test;
6
7 end Queue_Tests;
3 with Gen_Queues;
4
10 procedure Simple_Test is
11 package Queues_Float is new Gen_Queues (Queue_Mod, Float);
12
13 Q_F : Queues_Float.Queue;
14 V : Float;
15 begin
16 V := 10.0;
17 while not Q_F.Full loop
18 Q_F.Push (V);
19 V := V + 1.5;
20 end loop;
21
28 procedure Concurrent_Test is
29 package Queues_Integer is new Gen_Queues (Queue_Mod, Integer);
30
31 Q_I : Queues_Integer.Queue;
32
33 task T_Producer;
34 task T_Consumer;
35
60 end Queue_Tests;
6 procedure Main is
7 type Test_Case_Index is (Simple_Queue_Chk,
8 Concurrent_Queue_Chk);
9
12 begin
13 case TC is
14 when Simple_Queue_Chk =>
15 Simple_Test;
16 when Concurrent_Queue_Chk =>
17 Concurrent_Test;
18 end case;
19 end Check;
20
21 begin
22 if Argument_Count < 1 then
23 Put_Line ("ERROR: missing arguments! Exiting...");
24 return;
25 elsif Argument_Count > 1 then
26 Put_Line ("Ignoring additional arguments...");
27 end if;
28
TWO
DESIGN BY CONTRACTS
2. Interestingly, you can replace ranges by predicates, which is the goal of this ex-
ercise.
Listing 1: prices.ads
1 package Prices is
2
7 end Prices;
Listing 2: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with System.Assertions; use System.Assertions;
4
7 procedure Main is
8
9 type Test_Case_Index is
10 (Price_Range_Chk);
11
1905
Learning Ada
20 begin
21 case TC is
22 when Price_Range_Chk =>
23 Check_Range (-2.0);
24 end case;
25 exception
26 when Constraint_Error =>
27 Put_Line ("Constraint_Error detected (NOT as expected).");
28 when Assert_Failure =>
29 Put_Line ("Assert_Failure detected (as expected).");
30 end Check;
31
32 begin
33 if Argument_Count < 1 then
34 Put_Line ("ERROR: missing arguments! Exiting...");
35 return;
36 elsif Argument_Count > 1 then
37 Put_Line ("Ignoring additional arguments...");
38 end if;
39
Listing 3: triangles.ads
1 package Triangles is
2
15 end Triangles;
Listing 4: triangles-io.ads
1 package Triangles.IO is
2
5 end Triangles.IO;
Listing 5: triangles-io.adb
1 package body Triangles.IO is
2
9 end Triangles.IO;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with System.Assertions; use System.Assertions;
4
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
40 end case;
41 end Check;
42
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
Listing 7: triangles.ads
1 package Triangles is
2
15 end Triangles;
Listing 8: triangles-io.ads
1 package Triangles.IO is
2
5 end Triangles.IO;
Listing 9: triangles-io.adb
1 package body Triangles.IO is
2
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
(continues on next page)
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
15 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
(continues on next page)
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
9 private
10
21 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
40 end case;
41 end Check;
42
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
Goal: extend a package for HTML colors so that it can handle primary colors.
Steps:
1. Complete the Color_Types package.
1. Declare the HTML_RGB_Color subtype.
2. Implement the To_Int_Color function.
Requirements:
1. The HTML_Color type is an enumeration that contains a list of HTML colors.
2. The To_RGB_Lookup_Table array implements a lookup-table to convert the colors into
a hexadecimal value using RGB color components (i.e. Red, Green and Blue)
3. Function To_Int_Color extracts one of the RGB components of an HTML color and
returns its hexadecimal value.
1. The function has two parameters:
• First parameter is the HTML color (HTML_Color type).
• Second parameter indicates which RGB component is to be extracted from the
HTML color (HTML_RGB_Color subtype).
2. For example, if we call To_Int_Color (Salmon, Red), the function returns #FA,
• This is the hexadecimal value of the red component of the Salmon color.
• You can find further remarks below about this color as an example.
4. The HTML_RGB_Color subtype is limited to the primary RGB colors components (i.e.
Red, Green and Blue).
1. This subtype is used to select the RGB component in calls to To_Int_Color.
Color Value
Salmon #FA8072
Firebrick #B22222
Red #FF0000
Darkred #8B0000
Lime #00FF00
Forestgreen #228B22
Green #008000
Darkgreen #006400
Blue #0000FF
Mediumblue #0000CD
Darkblue #00008B
3. You can extract the hexadecimal value of each primary color by splitting the values
from the table above into three hexadecimal values with two digits each.
• For example, the hexadecimal value of Salmon is #FA8072, where:
– the first part of this hexadecimal value (#FA) corresponds to the red compo-
nent,
– the second part (#80) corresponds to the green component, and
– the last part (#72) corresponds to the blue component.
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
51 end Color_Types;
35 end Color_Types;
6 procedure Main is
7 type Test_Case_Index is
8 (HTML_Color_Red_Chk,
9 HTML_Color_Green_Chk,
10 HTML_Color_Blue_Chk);
11
23 begin
24 case TC is
25 when HTML_Color_Red_Chk =>
26 Check_HTML_Colors (Red);
27 when HTML_Color_Green_Chk =>
28 Check_HTML_Colors (Green);
29 when HTML_Color_Blue_Chk =>
30 Check_HTML_Colors (Blue);
31 end case;
32 end Check;
33
34 begin
35 if Argument_Count < 1 then
36 Put_Line ("ERROR: missing arguments! Exiting...");
37 return;
38 elsif Argument_Count > 1 then
39 Put_Line ("Ignoring additional arguments...");
40 end if;
41
THREE
OBJECT-ORIENTED PROGRAMMING
Goal: work with type extensions using record types containing numeric components.
Steps:
1. Implement the Type_Extensions package.
1. Declare the record type T_Float.
2. Declare the record type T_Mixed
3. Implement the Init function for the T_Float type with a floating-point input pa-
rameter.
4. Implement the Init function for the T_Float type with an integer input parameter.
5. Implement the Image function for the T_Float type.
6. Implement the Init function for the T_Mixed type with a floating-point input pa-
rameter.
7. Implement the Init function for the T_Mixed type with an integer input parameter.
8. Implement the Image function for the T_Mixed type.
Requirements:
1. Record type T_Float contains the following component:
1. F, a floating-point type.
2. Record type T_Mixed is derived from the T_Float type.
1. T_Mixed extends T_Float with the following component:
1. I, an integer component.
2. Both components must be numerically synchronized:
• For example, if the floating-point component contains the value 2.0, the value
of the integer component must be 2.
• In order to simplify the implementation, you can simply use Integer (F) to
convert a floating-point variable F to integer.
3. Function Init returns an object of the corresponding type (T_Float or T_Mixed).
1. For each type, two versions of Init must be declared:
1. one with a floating-point input parameter,
2. another with an integer input parameter.
2. The parameter to Init is used to initialize the record components.
1919
Learning Ada
4. Function Image returns a string for the components of the record type.
1. In case of the Image function for the T_Float type, the string must have the format
"{ F => <float value> }".
• For example, the call Image (T_Float'(Init (8.0)))) should return the
string "{ F => 8.00000E+00 }".
2. In case of the Image function for the T_Mixed type, the string must have the format
"{ F => <float value>, I => <integer value> }".
• For example, the call Image (T_Mixed'(Init (8.0)))) should return the
string "{ F => 8.00000E+00, I => 8 }".
Listing 1: type_extensions.ads
1 package Type_Extensions is
2
13 end Type_Extensions;
Listing 2: type_extensions.adb
1 package body Type_Extensions is
2
3 end Type_Extensions;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7
8 type Test_Case_Index is
9 (Type_Extension_Chk);
10
22 if M2 in T_Float'Class then
23 Put_Line ("T_Mixed is in T_Float'Class as expected");
24 end if;
25
33 begin
34 if Argument_Count < 1 then
35 Put_Line ("ERROR: missing arguments! Exiting...");
36 return;
37 elsif Argument_Count > 1 then
38 Put_Line ("Ignoring additional arguments...");
39 end if;
40
Member # <number>
Status: <status>
Since: <year>
Due Amount: <value>
--------
4. In order to pass the tests, the information displayed by a call to Simple_Test must
conform to the format described above.
• You can find another example in the remarks below.
Remarks:
1. In previous labs, we could have implemented a simplified version of the system de-
scribed above by simply using an enumeration type to specify the membership status.
For example:
1. In this case, the Get_Price function would then evaluate the membership
status and adapt the item price — assuming a fixed discount rate for all
full members. This could be the corresponding function declaration:
type Amount is delta 10.0**(-2) digits 10;
2. In this exercise, however, we'll use type extension to represent the mem-
bership status in our application.
2. For the procedure Simple_Test, let's consider the following list of members as an
example:
• For this list, the test procedure displays the following information (in this
exact format):
Member # 1
Status: Associate Member
Since: 2002
Due Amount: 100.00
--------
Member # 2
Status: Full Member
Since: 2005
Due Amount: 90.00
--------
• Here, although both members had the same full price (as indicated by the
last column), member #2 gets a reduced due amount of 90.00 because
of the full membership status.
Listing 4: online_store.ads
1 with Ada.Calendar; use Ada.Calendar;
2
3 package Online_Store is
4
32 end Online_Store;
Listing 5: online_store.adb
1 package body Online_Store is
2
16 end Online_Store;
Listing 6: online_store-tests.ads
1 package Online_Store.Tests is
2
3 procedure Simple_Test;
4
5 end Online_Store.Tests;
Listing 7: online_store-tests.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
5 procedure Simple_Test is
6 begin
7 null;
8 end Simple_Test;
9
10 end Online_Store.Tests;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
7 procedure Main is
8
9 type Test_Case_Index is
10 (Type_Chk,
11 Unit_Test_Chk);
12
18 begin
19 case TC is
20 when Type_Chk =>
21 declare
22 AM : constant Member := (Start => 2002);
23 FM : constant Full_Member := (Start => 1990,
24 Discount => 0.2);
25 begin
26 Put_Line ("Testing Status of Associate Member Type => "
27 & Result_Image (AM.Get_Status = "Associate Member"));
28 Put_Line ("Testing Status of Full Member Type => "
29 & Result_Image (FM.Get_Status = "Full Member"));
30 Put_Line ("Testing Discount of Associate Member Type => "
31 & Result_Image (AM.Get_Price (100.0) = 100.0));
32 Put_Line ("Testing Discount of Full Member Type => "
33 & Result_Image (FM.Get_Price (100.0) = 80.0));
34 end;
35 when Unit_Test_Chk =>
36 Simple_Test;
37 end case;
38 end Check;
39
40 begin
41 if Argument_Count < 1 then
42 Put_Line ("ERROR: missing arguments! Exiting...");
43 return;
44 elsif Argument_Count > 1 then
45 Put_Line ("Ignoring additional arguments...");
46 end if;
47
FOUR
Listing 1: todo_lists.ads
1 package Todo_Lists is
2
1927
Learning Ada
12 end Todo_Lists;
Listing 2: todo_lists.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
16 end Todo_Lists;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Todo_List_Chk);
9
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
35 Put_Line ("Ignoring additional arguments...");
36 end if;
(continues on next page)
Goal: create function that removes duplicates from and orders a collection of elements.
Steps:
1. Implement package Ops.
1. Declare the Int_Array type.
2. Declare the Integer_Sets type.
3. Implement the Get_Unique function that returns a set.
4. Implement the Get_Unique function that returns an array of integer values.
Requirements:
1. The Int_Array type is an unconstrained array of positive range.
2. The Integer_Sets package is an instantiation of the Ordered_Sets package for the
Integer type.
3. The Get_Unique function must remove duplicates from an input array of integer values
and order the elements.
1. For example:
• if the input array contains (7, 7, 1)
• the function must return (1, 7).
2. You must implement this function by using sets from the Ordered_Sets package.
3. Get_Unique must be implemented in two versions:
• one version that returns a set — Set type from the Ordered_Sets package.
• one version that returns an array of integer values — Int_Array type.
Remarks:
1. Sets — as the one found in the generic Ordered_Sets package — are useful for quickly
and easily creating an algorithm that removes duplicates from a list of elements.
Listing 4: ops.ads
1 with Ada.Containers.Ordered_Sets;
2
3 package Ops is
4
15 end Ops;
Listing 5: ops.adb
1 package body Ops is
2
13 end Ops;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Get_Unique_Set_Chk,
9 Get_Unique_Array_Chk);
10
30 begin
31 case TC is
32 when Get_Unique_Set_Chk => Display_Unique_Set (A);
33 when Get_Unique_Array_Chk => Display_Unique_Array (A);
34 end case;
35 end Check;
36
37 begin
38 if Argument_Count < 3 then
39 Put_Line ("ERROR: missing arguments! Exiting...");
(continues on next page)
FIVE
Goal: create a function that returns the year in the Holocene calendar.
Steps:
1. Implement the To_Holocene_Year function.
Requirements:
1. The To_Holocene_Year extracts the year from a time object (Time type) and returns
the corresponding year for the Holocene calendar384 .
1. For positive (AD) years, the Holocene year is calculated by adding 10,000 to the
year number.
Remarks:
1. In this exercise, we don't deal with BC years.
2. Note that the year component of the Time type from the Ada.Calendar package is
limited to years starting with 1901.
Listing 1: to_holocene_year.adb
1 with Ada.Calendar; use Ada.Calendar;
2
Listing 2: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Calendar; use Ada.Calendar;
4
5 with To_Holocene_Year;
6
7 procedure Main is
8 type Test_Case_Index is
9 (Holocene_Chk);
10
1933
Learning Ada
28 begin
29 if Argument_Count < 1 then
30 Put_Line ("ERROR: missing arguments! Exiting...");
31 return;
32 elsif Argument_Count > 1 then
33 Put_Line ("Ignoring additional arguments...");
34 end if;
35
2. You shall use the Time type from the Ada.Calendar package for the dates.
3. Since we expect the events to be ordered by the date, you shall use ordered maps
for the Event_List type.
5. Procedure Add adds an event into the list of events for a specific date.
6. Procedure Display must display all events for each date (ordered by date) using the
following format:
<event_date #1>
<description of item #1a>
<description of item #1b>
<event_date #2>
<description of item #2a>
<description of item #2b>
1. You should use the auxiliary Date_Image function — available in the body
of the Events.Lists package — to display the date in the YYYY-MM-DD
format.
Remarks:
1. Let's briefly illustrate the expected output of this system.
1. Consider the following example:
with Ada.Calendar;
with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
procedure Test is
EL : Event_List;
begin
EL.Add (Time_Of (2019, 4, 16),
"Item #2");
EL.Add (Time_Of (2019, 4, 15),
"Item #1");
EL.Add (Time_Of (2019, 4, 16),
"Item #3");
EL.Display;
end Test;
EVENTS LIST
- 2019-04-15
- Item #1
- 2019-04-16
- Item #2
- Item #3
Listing 3: events.ads
1 package Events is
2
7 end Events;
Listing 4: events-lists.ads
1 with Ada.Calendar; use Ada.Calendar;
2
3 package Events.Lists is
4
13 private
14
17 end Events.Lists;
Listing 5: events-lists.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
3
26 end Events.Lists;
Listing 6: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Calendar;
4 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
5
8 procedure Main is
9 type Test_Case_Index is
10 (Event_List_Chk);
(continues on next page)
29 begin
30 if Argument_Count < 1 then
31 Put_Line ("ERROR: missing arguments! Exiting...");
32 return;
33 elsif Argument_Count > 1 then
34 Put_Line ("Ignoring additional arguments...");
35 end if;
36
SIX
116.1 Concatenation
Listing 1: str_concat.ads
1 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
2
3 package Str_Concat is
4
1939
Learning Ada
15 end Str_Concat;
Listing 2: str_concat.adb
1 with Ada.Strings; use Ada.Strings;
2
19 end Str_Concat;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
4
7 procedure Main is
8 type Test_Case_Index is
9 (Unbounded_Concat_No_Trim_No_WS_Chk,
10 Unbounded_Concat_Trim_No_WS_Chk,
11 String_Concat_Trim_WS_Chk,
12 Concat_Single_Element);
13
55 begin
56 if Argument_Count < 1 then
57 Put_Line ("ERROR: missing arguments! Exiting...");
58 return;
59 elsif Argument_Count > 1 then
60 Put_Line ("Ignoring additional arguments...");
61 end if;
62
1. We use the lab on the list of events from the previous chapter (Standard library: Dates
& Times (page 1933)) as a starting point.
Listing 4: events.ads
1 with Ada.Containers.Vectors;
2
3 package Events is
4
5 -- subtype Event_Item is
6
14 end Events;
Listing 5: events-lists.ads
1 with Ada.Calendar; use Ada.Calendar;
2 with Ada.Containers.Ordered_Maps;
3
4 package Events.Lists is
5
14 private
15
24 end Events.Lists;
Listing 6: events-lists.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
3
38 end Events.Lists;
Listing 7: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Ada.Calendar;
4 with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
5 with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
6
7 with Events;
8 with Events.Lists; use Events.Lists;
9
10 procedure Main is
11 type Test_Case_Index is
12 (Unbounded_String_Chk,
13 Event_List_Chk);
14
38 begin
39 if Argument_Count < 1 then
40 Put_Line ("ERROR: missing arguments! Exiting...");
41 return;
42 elsif Argument_Count > 1 then
43 Put_Line ("Ignoring additional arguments...");
44 end if;
45
SEVEN
Goal: implement functions to convert from Decibel values to factors and vice-versa.
Steps:
1. Implement the Decibels package.
1. Implement the To_Decibel function.
2. Implement the To_Factor function.
Requirements:
1. The subtypes Decibel and Factor are based on a floating-point type.
2. Function To_Decibel converts a multiplication factor (or ratio) to decibels.
• For the implementation, use 20 ∗ 𝑙𝑜𝑔10 (𝐹 ), where F is the factor/ratio.
3. Function To_Factor converts a value in decibels to a multiplication factor (or ratio).
• For the implementation, use 10𝐷/20 , where D is the value in Decibel.
Remarks:
1. The Decibel385 is used to express the ratio of two values on a logarithmic scale.
1. For example, an increase of 6 dB corresponds roughly to a multiplication by two
(or an increase by 100 % of the original value).
2. You can find the functions that you'll need for the calculation in the Ada.Numerics.
Elementary_Functions package.
Listing 1: decibels.ads
1 package Decibels is
2
10 end Decibels;
385 https://en.wikipedia.org/wiki/Decibel
1945
Learning Ada
Listing 2: decibels.adb
1 package body Decibels is
2
13 end Decibels;
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
6 procedure Main is
7 type Test_Case_Index is
8 (Db_Chk,
9 Factor_Chk);
10
43 begin
(continues on next page)
117.2 Root-Mean-Square
Listing 4: signals.ads
1 package Signals is
2
9 end Signals;
386 https://en.wikipedia.org/wiki/Root_mean_square
Listing 5: signals.adb
1 with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions;
2
10 end Signals;
Listing 6: signals-std.ads
1 package Signals.Std is
2
11 end Signals.Std;
Listing 7: signals-std.adb
1 with Ada.Numerics; use Ada.Numerics;
2 with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions;
3
13 return S;
14 end;
15
36 end Signals.Std;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
7 procedure Main is
8 type Test_Case_Index is
9 (Sine_Signal_Chk,
10 Square_Signal_Chk,
11 Triangular_Signal_Chk);
12
37 begin
38 if Argument_Count < 1 then
39 Put_Line ("ERROR: missing arguments! Exiting...");
40 return;
41 elsif Argument_Count > 1 then
42 Put_Line ("Ignoring additional arguments...");
43 end if;
44
117.3 Rotation
Goal: use complex numbers to calculate the positions of an object in a circle after rotation.
Steps:
1. Implement the Rotation package.
1. Implement the Rotation function.
Requirements:
1. Type Complex_Points is an unconstrained array of complex values.
2. Function Rotation returns a list of positions (represented by the Complex_Points
type) when dividing a circle in N equal slices.
1. See the remarks below for a more detailed explanation.
2. You must use functions from Ada.Numerics.Complex_Types to implement Rota-
tion.
3. Subtype Angle is based on a floating-point type.
4. Type Angles is an unconstrained array of angles.
5. Function To_Angles returns a list of angles based on an input list of positions.
Remarks:
1. Complex numbers are particularly useful in computer graphics to simplify the calcula-
tion of rotations.
1. For example, let's assume you've drawn an object on your screen on position (1.0,
0.0).
2. Now, you want to move this object in a circular path — i.e. make it rotate around
position (0.0, 0.0) on your screen.
• You could use sine and cosine functions to calculate each position of the path.
• However, you could also calculate the positions using complex numbers.
2. In this exercise, you'll use complex numbers to calculate the positions of an object that
starts on zero degrees — on position (1.0, 0.0) — and rotates around (0.0, 0.0) for N
slices of a circle.
1. For example, if we divide the circle in four slices, the object's path will consist of
following points / positions:
Listing 9: rotation.ads
1 with Ada.Numerics.Complex_Types;
2 use Ada.Numerics.Complex_Types;
3
4 package Rotation is
5
10 end Rotation;
11 end Rotation;
3 package Angles is
4
11 end Angles;
15 end Angles;
7 end Rotation.Tests;
12 --
13 -- Adapt value due to floating-point inaccuracies
14 --
15
52 end Rotation.Tests;
6 procedure Main is
7 type Test_Case_Index is
8 (Rotation_Chk,
9 Angles_Chk);
10
21 begin
22 if Argument_Count < 2 then
23 Put_Line ("ERROR: missing arguments! Exiting...");
24 return;
25 elsif Argument_Count > 2 then
26 Put_Line ("Ignoring additional arguments...");
27 end if;
28
EIGHT
SOLUTIONS
Listing 1: main.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
3 procedure Main is
4 begin
5 Put_Line ("Hello World!");
6 end Main;
118.1.2 Greetings
Listing 2: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 procedure Main is
5
11 begin
12 if Argument_Count < 1 then
13 Put_Line ("ERROR: missing arguments! Exiting...");
14 return;
15 elsif Argument_Count > 1 then
16 Put_Line ("Ignoring additional arguments...");
17 end if;
18
1955
Learning Ada
Listing 3: classify_number.ads
1 procedure Classify_Number (X : Integer);
Listing 4: classify_number.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
Listing 5: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Classify_Number;
5
6 procedure Main is
7 A : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Classify_Number (A);
19 end Main;
118.1.4 Numbers
Listing 6: display_numbers.ads
1 procedure Display_Numbers (A, B : Integer);
Listing 7: display_numbers.adb
1 with Ada.Text_IO; use Ada.Text_IO;
2
14 for I in X .. Y loop
15 Put_Line (Integer'Image (I));
16 end loop;
17 end Display_Numbers;
Listing 8: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3
4 with Display_Numbers;
5
6 procedure Main is
7 A, B : Integer;
8 begin
9 if Argument_Count < 2 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 2 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
118.2 Subprograms
Listing 9: subtract.ads
1 procedure Subtract (A, B : Integer;
2 Result : out Integer);
6 procedure Main is
7 type Test_Case_Index is
8 (Sub_10_1_Chk,
9 Sub_10_100_Chk,
10 Sub_0_5_Chk,
11 Sub_0_Minus_5_Chk);
12
32 begin
33 if Argument_Count < 1 then
34 Put_Line ("ERROR: missing arguments! Exiting...");
35 return;
36 elsif Argument_Count > 1 then
37 Put_Line ("Ignoring additional arguments...");
38 end if;
39
4 with Subtract;
(continues on next page)
6 procedure Main is
7 type Test_Case_Index is
8 (Sub_10_1_Chk,
9 Sub_10_100_Chk,
10 Sub_0_5_Chk,
11 Sub_0_Minus_5_Chk);
12
32 begin
33 if Argument_Count < 1 then
34 Put_Line ("ERROR: missing arguments! Exiting...");
35 return;
36 elsif Argument_Count > 1 then
37 Put_Line ("Ignoring additional arguments...");
38 end if;
39
4 with Is_Equal;
5
25 Result : Boolean;
26 begin
27 case TC is
28 when Equal_Chk =>
29 for I in 0 .. 10 loop
30 Result := Is_Equal (I, I);
31 Display_Equal (I, I, Result);
32 end loop;
33 when Inequal_Chk =>
34 for I in 0 .. 10 loop
35 Result := Is_Equal (I, I - 1);
36 Display_Equal (I, I - 1, Result);
37 end loop;
38 end case;
39 end Check;
40
41 begin
42 if Argument_Count < 1 then
43 Put_Line ("ERROR: missing arguments! Exiting...");
44 return;
45 elsif Argument_Count > 1 then
46 Put_Line ("Ignoring additional arguments...");
47 end if;
48
118.2.4 States
4 with Display_State;
5
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Display_State (State);
19 end Main;
118.2.5 States #2
4 with Get_State;
(continues on next page)
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
118.2.6 States #3
4 with Display_On_Off;
5 with Is_On;
6
7 procedure Main is
8 State : Integer;
9 begin
10 if Argument_Count < 1 then
11 Put_Line ("ERROR: missing arguments! Exiting...");
12 return;
(continues on next page)
19 Display_On_Off (State);
20 Put_Line (Boolean'Image (Is_On (State)));
21 end Main;
118.2.7 States #4
4 with Set_Next;
5
6 procedure Main is
7 State : Integer;
8 begin
9 if Argument_Count < 1 then
10 Put_Line ("ERROR: missing arguments! Exiting...");
11 return;
12 elsif Argument_Count > 1 then
13 Put_Line ("Ignoring additional arguments...");
14 end if;
15
18 Set_Next (State);
19 Put_Line (Integer'Image (State));
20 end Main;
118.3.1 Months
16 procedure Display_Months;
17
18 end Months;
5 procedure Display_Months is
6 begin
7 Put_Line ("Months:");
8 Put_Line ("- " & Jan);
9 Put_Line ("- " & Feb);
10 Put_Line ("- " & Mar);
11 Put_Line ("- " & Apr);
12 Put_Line ("- " & May);
13 Put_Line ("- " & Jun);
14 Put_Line ("- " & Jul);
15 Put_Line ("- " & Aug);
16 Put_Line ("- " & Sep);
17 Put_Line ("- " & Oct);
18 Put_Line ("- " & Nov);
19 Put_Line ("- " & Dec);
20 end Display_Months;
21
22 end Months;
6 procedure Main is
7
8 type Test_Case_Index is
(continues on next page)
19 begin
20 if Argument_Count < 1 then
21 Put_Line ("ERROR: missing arguments! Exiting...");
22 return;
23 elsif Argument_Count > 1 then
24 Put_Line ("Ignoring additional arguments...");
25 end if;
26
118.3.2 Operations
11 end Operations;
23 end Operations;
5 end Operations.Test;
24 end Operations.Test;
4 with Operations;
5 with Operations.Test; use Operations.Test;
6
7 procedure Main is
8
9 type Test_Case_Index is
10 (Operations_Chk,
11 Operations_Display_Chk);
12
31 begin
32 if Argument_Count < 1 then
33 Put_Line ("ERROR: missing arguments! Exiting...");
34 return;
35 elsif Argument_Count > 1 then
36 Put_Line ("Ignoring additional arguments...");
37 end if;
38
118.4.1 Colors
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
18 type Basic_HTML_Color is
19 (Red,
20 Green,
21 Blue);
22
25 end Color_Types;
19 end To_Integer;
20
30 end Color_Types;
7 procedure Main is
8 type Test_Case_Index is
9 (HTML_Color_Range,
10 HTML_Color_To_Integer,
11 Basic_HTML_Color_To_HTML_Color);
12
34 begin
35 if Argument_Count < 1 then
36 Put_Line ("ERROR: missing arguments! Exiting...");
37 return;
38 elsif Argument_Count > 1 then
39 Put_Line ("Ignoring additional arguments...");
40 end if;
41
118.4.2 Integers
21 end Int_Types;
42 end Int_Types;
6 procedure Main is
7 package I_100_IO is new Ada.Text_IO.Integer_IO (I_100);
8 package U_100_IO is new Ada.Text_IO.Modular_IO (U_100);
9 package D_50_IO is new Ada.Text_IO.Integer_IO (D_50);
10
11 use I_100_IO;
12 use U_100_IO;
13 use D_50_IO;
14
15 type Test_Case_Index is
16 (I_100_Range,
17 U_100_Range,
18 U_100_Wraparound,
19 U_100_To_I_100,
20 I_100_To_U_100,
21 D_50_Range,
22 S_50_Range,
23 I_100_To_D_50,
24 I_100_To_S_50,
25 D_50_To_I_100,
26 S_50_To_I_100);
27
34 case TC is
35 when I_100_Range =>
36 Put (I_100'First);
37 New_Line;
38 Put (I_100'Last);
39 New_Line;
40 when U_100_Range =>
41 Put (U_100'First);
42 New_Line;
43 Put (U_100'Last);
44 New_Line;
45 when U_100_Wraparound =>
46 Put (U_100'First - 1);
47 New_Line;
48 Put (U_100'Last + 1);
49 New_Line;
50 when U_100_To_I_100 =>
51 for I in U_100'Range loop
52 I_100_IO.Put (To_I_100 (I));
53 New_Line;
54 end loop;
55 when I_100_To_U_100 =>
56 for I in I_100'Range loop
57 Put (To_U_100 (I));
58 New_Line;
59 end loop;
60 when D_50_Range =>
61 Put (D_50'First);
62 New_Line;
63 Put (D_50'Last);
64 New_Line;
65 when S_50_Range =>
66 Put (S_50'First);
67 New_Line;
68 Put (S_50'Last);
69 New_Line;
70 when I_100_To_D_50 =>
71 for I in I_100'Range loop
72 Put (To_D_50 (I));
73 New_Line;
74 end loop;
75 when I_100_To_S_50 =>
76 for I in I_100'Range loop
77 Put (To_S_50 (I));
78 New_Line;
79 end loop;
80 when D_50_To_I_100 =>
81 for I in D_50'Range loop
82 Put (To_I_100 (I));
83 New_Line;
84 end loop;
85 when S_50_To_I_100 =>
86 for I in S_50'Range loop
87 Put (I);
88 New_Line;
89 end loop;
(continues on next page)
93 begin
94 if Argument_Count < 1 then
95 Put_Line ("ERROR: missing arguments! Exiting...");
96 return;
97 elsif Argument_Count > 1 then
98 Put_Line ("Ignoring additional arguments...");
99 end if;
100
118.4.3 Temperatures
17 end Temperature_Types;
35 end Temperature_Types;
6 procedure Main is
7 package Celsius_IO is new Ada.Text_IO.Float_IO (Celsius);
8 package Kelvin_IO is new Ada.Text_IO.Float_IO (Kelvin);
9 package Int_Celsius_IO is new Ada.Text_IO.Integer_IO (Int_Celsius);
10
11 use Celsius_IO;
12 use Kelvin_IO;
13 use Int_Celsius_IO;
14
15 type Test_Case_Index is
16 (Celsius_Range,
17 Celsius_To_Int_Celsius,
18 Int_Celsius_To_Celsius,
19 Kelvin_To_Celsius,
20 Celsius_To_Kelvin);
21
28 case TC is
29 when Celsius_Range =>
30 Put (Celsius'First);
31 New_Line;
32 Put (Celsius'Last);
33 New_Line;
34 when Celsius_To_Int_Celsius =>
35 Put (To_Int_Celsius (Celsius'First));
36 New_Line;
37 Put (To_Int_Celsius (0.0));
38 New_Line;
39 Put (To_Int_Celsius (Celsius'Last));
40 New_Line;
41 when Int_Celsius_To_Celsius =>
42 Put (To_Celsius (Int_Celsius'First));
43 New_Line;
(continues on next page)
63 begin
64 if Argument_Count < 1 then
65 Put_Line ("ERROR: missing arguments! Exiting...");
66 return;
67 elsif Argument_Count > 1 then
68 Put_Line ("Ignoring additional arguments...");
69 end if;
70
118.5 Records
118.5.1 Directions
5 type Direction is
6 (North,
7 Northeast,
8 East,
9 Southeast,
10 South,
11 Southwest,
12 West,
13 Northwest);
14
26 end Directions;
34 end Directions;
6 procedure Main is
7 type Test_Case_Index is
8 (Direction_Chk);
9
26 begin
27 if Argument_Count < 1 then
28 Put_Line ("ERROR: missing arguments! Exiting...");
29 return;
30 elsif Argument_Count > 1 then
31 Put_Line ("Ignoring additional arguments...");
32 end if;
33
118.5.2 Colors
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
18 type Basic_HTML_Color is
19 (Red,
20 Green,
21 Blue);
22
37 end Color_Types;
21 end To_Integer;
22
48 end To_RGB;
49
71 end Color_Types;
6 procedure Main is
7 type Test_Case_Index is
8 (HTML_Color_To_RGB);
9
21 begin
22 if Argument_Count < 1 then
23 Put_Line ("ERROR: missing arguments! Exiting...");
24 return;
25 elsif Argument_Count > 1 then
26 Put_Line ("Ignoring additional arguments...");
27 end if;
28
118.5.3 Inventory
3 type Item_Name is
4 (Ballpoint_Pen, Oil_Based_Pen_Marker, Feather_Quill_Pen);
5
21 end Inventory_Pkg;
31 end Inventory_Pkg;
6 procedure Main is
7 -- Remark: the following line is not relevant.
8 F : array (1 .. 10) of Float := (others => 42.42);
9
10 type Test_Case_Index is
11 (Inventory_Chk);
12
16 use F_IO;
17 begin
18 Put ("Assets: $");
19 Put (Assets, 1, 2, 0);
20 Put (".");
21 New_Line;
22 end Display;
23
49 begin
50 if Argument_Count < 1 then
51 Put_Line ("ERROR: missing arguments! Exiting...");
52 return;
53 elsif Argument_Count > 1 then
54 Put_Line ("Ignoring additional arguments...");
55 end if;
56
118.6 Arrays
19 end Constrained_Arrays;
10 return A;
11 end Init;
12
35 end Constrained_Arrays;
6 procedure Main is
7 type Test_Case_Index is
8 (Range_Chk,
9 Array_Range_Chk,
10 A_Obj_Chk,
11 Init_Chk,
12 Double_Chk,
13 First_Elem_Chk,
14 Last_Elem_Chk,
15 Length_Chk);
16
61 begin
62 if Argument_Count < 1 then
63 Put_Line ("ERROR: missing arguments! Exiting...");
64 return;
65 elsif Argument_Count > 1 then
66 Put_Line ("Ignoring additional arguments...");
67 end if;
68
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
43 end Color_Types;
30 end Color_Types;
6 procedure Main is
7 type Test_Case_Index is
8 (Color_Table_Chk,
9 HTML_Color_To_Integer_Chk);
10
27 begin
28 if Argument_Count < 1 then
29 Put_Line ("ERROR: missing arguments! Exiting...");
30 return;
31 elsif Argument_Count > 1 then
32 Put_Line ("Ignoring additional arguments...");
33 end if;
34
13 end Unconstrained_Arrays;
34 return A_Out;
35 end Diff_Prev_Elem;
36
37 end Unconstrained_Arrays;
6 procedure Main is
7 type Test_Case_Index is
8 (Init_Chk,
9 Init_Proc_Chk,
10 Double_Chk,
11 Diff_Prev_Chk,
12 Diff_Prev_Single_Chk);
13
30 begin
31 case TC is
32 when Init_Chk =>
33 AA := Init (AA'First, AA'Length);
34 AB := Init (AB'First, AB'Length);
35 Display (AA);
36 Display (AB);
37 when Init_Proc_Chk =>
38 Init (AA);
39 Init (AB);
40 Display (AA);
41 Display (AB);
42 when Double_Chk =>
(continues on next page)
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
23 end Product_Info_Pkg;
33 end Product_Info_Pkg;
6 procedure Main is
7 package Currency_IO is new Ada.Text_IO.Float_IO (Currency);
8
9 type Test_Case_Index is
10 (Total_Func_Chk,
11 Total_Proc_Chk,
12 Total_Value_Chk);
13
17 P : Product_Infos (Test_Range);
18 Tots : Currency_Array (Test_Range);
19 Tot : Currency;
20
38 begin
39 Currency_IO.Default_Fore := 1;
40 Currency_IO.Default_Aft := 2;
41 Currency_IO.Default_Exp := 0;
42
43 case TC is
44 when Total_Func_Chk =>
45 Local_Init (P);
46 Tots := Total (P);
47 Display (Tots);
48 when Total_Proc_Chk =>
49 Local_Init (P);
50 Total (P, Tots);
51 Display (Tots);
52 when Total_Value_Chk =>
53 Local_Init (P);
54 Tot := Total (P);
55 Currency_IO.Put (Tot);
56 New_Line;
57 end case;
58 end Check;
59
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
118.6.5 String_10
14 return S_Out;
15 end To_String_10;
16
17 end Strings_10;
6 procedure Main is
7 type Test_Case_Index is
8 (String_10_Long_Chk,
9 String_10_Short_Chk);
10
16 begin
17 case TC is
18 when String_10_Long_Chk =>
19 S_10 := To_String_10 (SL);
20 Put_Line (String (S_10));
21 when String_10_Short_Chk =>
22 S_10 := (others => ' ');
23 S_10 := To_String_10 (SS);
24 Put_Line (String (S_10));
25 end case;
26 end Check;
27
28 begin
29 if Argument_Count < 1 then
30 Ada.Text_IO.Put_Line ("ERROR: missing arguments! Exiting...");
31 return;
32 elsif Argument_Count > 1 then
33 Ada.Text_IO.Put_Line ("Ignoring additional arguments...");
34 end if;
35
35 end Names_Ages;
14 return S_Out;
15 end To_Name_Type;
16
68 return 0;
69 end Get;
70
90 end Names_Ages;
6 procedure Main is
7 type Test_Case_Index is
8 (Names_Ages_Chk,
9 Get_Age_Chk);
10
35 begin
36 if Argument_Count < 1 then
37 Ada.Text_IO.Put_Line ("ERROR: missing arguments! Exiting...");
38 return;
39 elsif Argument_Count > 1 then
40 Ada.Text_IO.Put_Line ("Ignoring additional arguments...");
41 end if;
42
18 end Aggregates;
21 end Aggregates;
6 procedure Main is
7 -- Remark: the following line is not relevant.
8 F : array (1 .. 10) of Float := (others => 42.42)
9 with Unreferenced;
(continues on next page)
11 type Test_Case_Index is
12 (Default_Rec_Chk,
13 Init_Rec_Chk,
14 Init_Some_Arr_Chk,
15 Init_Arr_Chk);
16
54 begin
55 if Argument_Count < 1 then
56 Put_Line ("ERROR: missing arguments! Exiting...");
57 return;
58 elsif Argument_Count > 1 then
59 Put_Line ("Ignoring additional arguments...");
60 end if;
61
118.7.2 Versioning
13 end Versioning;
25 end Versioning;
6 procedure Main is
7 type Test_Case_Index is
8 (Ver_String_Chk,
9 Ver_Float_Chk);
10
22 begin
23 if Argument_Count < 1 then
24 Put_Line ("ERROR: missing arguments! Exiting...");
25 return;
26 elsif Argument_Count > 1 then
27 Put_Line ("Ignoring additional arguments...");
28 end if;
29
17 end Todo_Lists;
24 end Todo_Lists;
6 procedure Main is
7 type Test_Case_Index is
8 (Todo_List_Chk);
9
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
35 Put_Line ("Ignoring additional arguments...");
36 end if;
37
31 end Price_Lists;
41 end Price_Lists;
6 procedure Main is
7 type Test_Case_Index is
8 (Price_Type_Chk,
9 Price_List_Chk,
10 Price_List_Get_Chk);
11
15 procedure Local_Init_List is
16 begin
17 Reset (L);
18 Add (L, 1.45);
19 Add (L, 2.37);
20 Add (L, 3.21);
21 Add (L, 4.14);
22 Add (L, 5.22);
23 Add (L, 6.69);
24 Add (L, 7.77);
25 Add (L, 8.14);
26 Add (L, 9.99);
27 Add (L, 10.01);
28 end Local_Init_List;
29
48 end Get_Display;
49
50 begin
51 case TC is
52 when Price_Type_Chk =>
53 Put_Line ("The delta value of Price_Type is "
54 & Price_Type'Image (Price_Type'Delta) & ";");
55 Put_Line ("The minimum value of Price_Type is "
56 & Price_Type'Image (Price_Type'First) & ";");
57 Put_Line ("The maximum value of Price_Type is "
58 & Price_Type'Image (Price_Type'Last) & ";");
59 when Price_List_Chk =>
60 Local_Init_List;
61 Display (L);
62 when Price_List_Get_Chk =>
63 Local_Init_List;
64 Get_Display (5);
65 Get_Display (40);
66 end case;
67 end Check;
68
69 begin
70 if Argument_Count < 1 then
71 Put_Line ("ERROR: missing arguments! Exiting...");
72 return;
73 elsif Argument_Count > 1 then
74 Put_Line ("Ignoring additional arguments...");
75 end if;
76
118.8 Privacy
118.8.1 Directions
5 type Direction is
6 (North,
7 Northwest,
8 West,
9 Southwest,
10 South,
11 Southeast,
12 East);
(continues on next page)
22 private
23
29 end Directions;
34 end Directions;
3 procedure Test_Directions is
(continues on next page)
14 begin
15 for I in All_Directions'Range loop
16 Display (All_Directions (I));
17 end loop;
18
19 end Test_Directions;
4 with Test_Directions;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Direction_Chk);
9
18 begin
19 if Argument_Count < 1 then
20 Put_Line ("ERROR: missing arguments! Exiting...");
21 return;
22 elsif Argument_Count > 1 then
23 Put_Line ("Ignoring additional arguments...");
24 end if;
25
16 private
17
20 end Limited_Strings;
49 end Limited_Strings;
5 procedure Check_Lim_String is
6 S : constant String := "----------";
7 S1 : constant Lim_String := Init ("Hello World");
8 S2 : constant Lim_String := Init (30);
9 S3 : Lim_String := Init (5);
10 S4 : Lim_String := Init (S & S & S);
11 begin
12 Put ("S1 => ");
13 Put_Line (S1);
14 Put ("S2 => ");
15 Put_Line (S2);
16
17 if S1 = S2 then
18 Put_Line ("S1 is equal to S2.");
19 else
20 Put_Line ("S1 isn't equal to S2.");
21 end if;
22
27 if S1 = S3 then
28 Put_Line ("S1 is equal to S3.");
29 else
30 Put_Line ("S1 isn't equal to S3.");
31 end if;
32
37 if S1 = S4 then
38 Put_Line ("S1 is equal to S4.");
39 else
40 Put_Line ("S1 isn't equal to S4.");
41 end if;
42 end Check_Lim_String;
4 with Check_Lim_String;
5
6 procedure Main is
7 type Test_Case_Index is
8 (Lim_String_Chk);
(continues on next page)
18 begin
19 if Argument_Count < 1 then
20 Put_Line ("ERROR: missing arguments! Exiting...");
21 return;
22 elsif Argument_Count > 1 then
23 Put_Line ("Ignoring additional arguments...");
24 end if;
25
118.9 Generics
4 with Display_Array;
5
6 procedure Main is
7 type Test_Case_Index is (Int_Array_Chk,
(continues on next page)
10 procedure Test_Int_Array is
11 type Int_Array is array (Positive range <>) of Integer;
12
24 procedure Test_Point_Array is
25 type Point is record
26 X : Float;
27 Y : Float;
28 end record;
29
60 begin
61 if Argument_Count < 1 then
62 Put_Line ("ERROR: missing arguments! Exiting...");
63 return;
64 elsif Argument_Count > 1 then
65 Put_Line ("Ignoring additional arguments...");
66 end if;
67
4 with Average;
5
6 procedure Main is
7 type Test_Case_Index is (Float_Array_Chk,
8 Digits_7_Float_Array_Chk);
9
10 procedure Test_Float_Array is
11 type Float_Array is array (Positive range <>) of Float;
12
23 procedure Test_Digits_7_Float_Array is
24 type Custom_Float is digits 7 range 0.0 .. 1.0;
25
26 type Float_Array is
27 array (Integer range <>) of Custom_Float;
28
50 begin
51 if Argument_Count < 1 then
52 Put_Line ("ERROR: missing arguments! Exiting...");
53 return;
54 elsif Argument_Count > 1 then
55 Put_Line ("Ignoring additional arguments...");
56 end if;
57
3 with Average;
4
5 procedure Test_Item is
6 package F_IO is new Ada.Text_IO.Float_IO (Float);
7
15 type Item_Array is
16 array (Positive range <>) of Item;
17
36 A : constant Item_Array (1 .. 4)
37 := ((Quantity => 5, Price => 10.00),
38 (Quantity => 80, Price => 2.50),
39 (Quantity => 40, Price => 5.00),
40 (Quantity => 20, Price => 12.50));
41
42 begin
43 Put ("Average per item & quantity: ");
44 F_IO.Put (Average_Total (A), 3, 2, 0);
45 New_Line;
46
4 with Test_Item;
5
6 procedure Main is
(continues on next page)
17 begin
18 if Argument_Count < 1 then
19 Put_Line ("ERROR: missing arguments! Exiting...");
20 return;
21 elsif Argument_Count > 1 then
22 Put_Line ("Ignoring additional arguments...");
23 end if;
24
10 procedure Init;
11
15 procedure Display;
16
17 end Gen_List;
5 procedure Init is
6 begin
7 Last := List_Array'First - 1;
8 end Init;
9
15 if Status then
16 Last := Last + 1;
17 List_Array (Last) := I;
18 end if;
19 end Add;
20
21 procedure Display is
22 begin
23 Put_Line (Name);
24 for I in List_Array'First .. Last loop
25 Put (List_Array (I));
26 New_Line;
27 end loop;
28 end Display;
29
30 end Gen_List;
3 with Gen_List;
4
5 procedure Test_Int is
6
14 A : Integer_Array (1 .. 3);
15 L : Natural;
16
24 Success : Boolean;
25
34 end Display_Add_Success;
35
36 begin
(continues on next page)
51 Int_List.Display;
52 end Test_Int;
4 with Test_Int;
5
6 procedure Main is
7 type Test_Case_Index is (Int_Chk);
8
17 begin
18 if Argument_Count < 1 then
19 Put_Line ("ERROR: missing arguments! Exiting...");
20 return;
21 elsif Argument_Count > 1 then
22 Put_Line ("Ignoring additional arguments...");
23 end if;
24
118.10 Exceptions
8 Uninitialized_Value : exception;
9
12 end Options;
13 end Options;
7 procedure Main is
8 type Test_Case_Index is
9 (Options_Chk);
10
21 begin
22 case TC is
23 when Options_Chk =>
24 for O in Option loop
25 Check (O);
26 end loop;
27 end case;
28 end Check;
29
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
(continues on next page)
5 Custom_Exception : exception;
6
9 end Tests;
20 end Tests;
8 procedure Main is
9 type Test_Case_Index is
10 (Exception_1_Chk,
11 Exception_2_Chk);
12
27 begin
28 case TC is
29 when Exception_1_Chk =>
30 Check_Handle_Exception (Test_1);
31 when Exception_2_Chk =>
32 Check_Handle_Exception (Test_2);
33 end case;
34 end Check;
35
36 begin
37 if Argument_Count < 1 then
38 Put_Line ("ERROR: missing arguments! Exiting...");
39 return;
40 elsif Argument_Count > 1 then
41 Put_Line ("Ignoring additional arguments...");
42 end if;
43
9 end Tests;
20 end Tests;
8 procedure Main is
9 type Test_Case_Index is
10 (Exception_1_Chk,
11 Exception_2_Chk);
12
27 begin
28 case TC is
29 when Exception_1_Chk =>
30 Check_Handle_Exception (Test_1);
31 when Exception_2_Chk =>
32 Check_Handle_Exception (Test_2);
33 end case;
34 end Check;
35
36 begin
37 if Argument_Count < 1 then
38 Put_Line ("ERROR: missing arguments! Exiting...");
39 return;
40 elsif Argument_Count > 1 then
41 Put_Line ("Ignoring additional arguments...");
42 end if;
43
118.11 Tasking
8 end Display_Services;
22 end Display_Services;
6 procedure Main is
7 type Test_Case_Index is (Display_Service_Chk);
8
23 begin
24 if Argument_Count < 1 then
25 Put_Line ("ERROR: missing arguments! Exiting...");
26 return;
27 elsif Argument_Count > 1 then
(continues on next page)
3 package Event_Managers is
4
10 end Event_Managers;
22 end Event_Managers;
7 procedure Main is
8 type Test_Case_Index is (Event_Manager_Chk);
9
26 begin
27 if Argument_Count < 1 then
28 Put_Line ("ERROR: missing arguments! Exiting...");
29 return;
30 elsif Argument_Count > 1 then
31 Put_Line ("Ignoring additional arguments...");
32 end if;
33
19 end Gen_Queues;
15 Idx := Idx + 1;
16 N := N + 1;
17 end Push;
18
26 end Queue;
27
28 end Gen_Queues;
3 procedure Simple_Test;
4
5 procedure Concurrent_Test;
6
7 end Queue_Tests;
3 with Gen_Queues;
4
10 procedure Simple_Test is
11 package Queues_Float is new Gen_Queues (Queue_Mod, Float);
12
13 Q_F : Queues_Float.Queue;
14 V : Float;
15 begin
16 V := 10.0;
17 while not Q_F.Full loop
18 Q_F.Push (V);
19 V := V + 1.5;
20 end loop;
21
28 procedure Concurrent_Test is
29 package Queues_Integer is new Gen_Queues (Queue_Mod, Integer);
30
31 Q_I : Queues_Integer.Queue;
32
33 task T_Producer;
34 task T_Consumer;
35
60 end Queue_Tests;
6 procedure Main is
7 type Test_Case_Index is (Simple_Queue_Chk,
8 Concurrent_Queue_Chk);
9
20 begin
21 if Argument_Count < 1 then
22 Put_Line ("ERROR: missing arguments! Exiting...");
23 return;
(continues on next page)
10 end Prices;
7 procedure Main is
8
9 type Test_Case_Index is
10 (Price_Range_Chk);
11
20 begin
21 case TC is
22 when Price_Range_Chk =>
23 Check_Range (-2.0);
24 end case;
25 exception
26 when Constraint_Error =>
27 Put_Line ("Constraint_Error detected (NOT as expected).");
28 when Assert_Failure =>
29 Put_Line ("Assert_Failure detected (as expected).");
30 end Check;
31
(continues on next page)
16 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
40 end case;
41 end Check;
42
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
16 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
40 end case;
41 end Check;
42
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
18 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
(continues on next page)
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
12 private
13
27 end Triangles;
5 end Triangles.IO;
9 end Triangles.IO;
8 procedure Main is
9
10 type Test_Case_Index is
11 (Triangle_8_6_Pass_Chk,
12 Triangle_8_6_Fail_Chk,
13 Triangle_10_24_Pass_Chk,
14 Triangle_10_24_Fail_Chk,
15 Triangle_18_24_Pass_Chk,
16 Triangle_18_24_Fail_Chk);
17
32 begin
33 case TC is
34 when Triangle_8_6_Pass_Chk => Check_Triangle (10, 8, 6);
35 when Triangle_8_6_Fail_Chk => Check_Triangle (12, 8, 6);
36 when Triangle_10_24_Pass_Chk => Check_Triangle (26, 10, 24);
37 when Triangle_10_24_Fail_Chk => Check_Triangle (12, 10, 24);
38 when Triangle_18_24_Pass_Chk => Check_Triangle (30, 18, 24);
39 when Triangle_18_24_Fail_Chk => Check_Triangle (32, 18, 24);
40 end case;
41 end Check;
42
43 begin
44 if Argument_Count < 1 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 1 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
3 type HTML_Color is
4 (Salmon,
5 Firebrick,
6 Red,
7 Darkred,
8 Lime,
9 Forestgreen,
10 Green,
11 Darkgreen,
12 Blue,
13 Mediumblue,
14 Darkblue);
15
52 end Color_Types;
39 end Color_Types;
6 procedure Main is
7 type Test_Case_Index is
8 (HTML_Color_Red_Chk,
9 HTML_Color_Green_Chk,
10 HTML_Color_Blue_Chk);
11
23 begin
24 case TC is
25 when HTML_Color_Red_Chk =>
26 Check_HTML_Colors (Red);
27 when HTML_Color_Green_Chk =>
28 Check_HTML_Colors (Green);
29 when HTML_Color_Blue_Chk =>
30 Check_HTML_Colors (Blue);
31 end case;
32 end Check;
33
34 begin
35 if Argument_Count < 1 then
36 Put_Line ("ERROR: missing arguments! Exiting...");
37 return;
38 elsif Argument_Count > 1 then
39 Put_Line ("Ignoring additional arguments...");
40 end if;
41
23 end Type_Extensions;
36 end Type_Extensions;
6 procedure Main is
7
8 type Test_Case_Index is
9 (Type_Extension_Chk);
10
22 if M2 in T_Float'Class then
23 Put_Line ("T_Mixed is in T_Float'Class as expected");
24 end if;
25
33 begin
34 if Argument_Count < 1 then
35 Put_Line ("ERROR: missing arguments! Exiting...");
36 return;
37 elsif Argument_Count > 1 then
38 Put_Line ("Ignoring additional arguments...");
39 end if;
40
3 package Online_Store is
4
29 end Online_Store;
16 end Online_Store;
3 procedure Simple_Test;
4
5 end Online_Store.Tests;
5 procedure Simple_Test is
6
19 DB : constant Member_Due_Amounts (1 .. 4)
20 := ((Member => new Member'(Start => 2010),
21 Due_Amount => 250.0),
22 (Member => new Full_Member'(Start => 1998,
23 Discount => 0.1),
24 Due_Amount => 160.0),
25 (Member => new Full_Member'(Start => 1987,
26 Discount => 0.2),
27 Due_Amount => 400.0),
28 (Member => new Member'(Start => 2013),
29 Due_Amount => 110.0));
30 begin
31 for I in DB'Range loop
32 Put_Line ("Member #" & Positive'Image (I));
(continues on next page)
40 end Online_Store.Tests;
7 procedure Main is
8
9 type Test_Case_Index is
10 (Type_Chk,
11 Unit_Test_Chk);
12
18 begin
19 case TC is
20 when Type_Chk =>
21 declare
22 AM : constant Member := (Start => 2002);
23 FM : constant Full_Member := (Start => 1990,
24 Discount => 0.2);
25 begin
26 Put_Line ("Testing Status of Associate Member Type => "
27 & Result_Image (AM.Get_Status = "Associate Member"));
28 Put_Line ("Testing Status of Full Member Type => "
29 & Result_Image (FM.Get_Status = "Full Member"));
30 Put_Line ("Testing Discount of Associate Member Type => "
31 & Result_Image (AM.Get_Price (100.0) = 100.0));
32 Put_Line ("Testing Discount of Full Member Type => "
33 & Result_Image (FM.Get_Price (100.0) = 80.0));
34 end;
35 when Unit_Test_Chk =>
36 Simple_Test;
37 end case;
38 end Check;
39
40 begin
41 if Argument_Count < 1 then
42 Put_Line ("ERROR: missing arguments! Exiting...");
43 return;
44 elsif Argument_Count > 1 then
45 Put_Line ("Ignoring additional arguments...");
46 end if;
47
3 package Todo_Lists is
4
18 end Todo_Lists;
19 end Todo_Lists;
6 procedure Main is
7 type Test_Case_Index is
8 (Todo_List_Chk);
9
30 begin
31 if Argument_Count < 1 then
32 Put_Line ("ERROR: missing arguments! Exiting...");
33 return;
34 elsif Argument_Count > 1 then
35 Put_Line ("Ignoring additional arguments...");
36 end if;
37
3 package Ops is
4
16 end Ops;
10 return S;
11 end Get_Unique;
12
23 return AR;
24 end Get_Unique;
25
26 end Ops;
6 procedure Main is
7 type Test_Case_Index is
8 (Get_Unique_Set_Chk,
9 Get_Unique_Array_Chk);
10
30 begin
31 case TC is
32 when Get_Unique_Set_Chk => Display_Unique_Set (A);
33 when Get_Unique_Array_Chk => Display_Unique_Array (A);
34 end case;
35 end Check;
36
37 begin
38 if Argument_Count < 3 then
(continues on next page)
5 with To_Holocene_Year;
6
7 procedure Main is
8 type Test_Case_Index is
9 (Holocene_Chk);
10
28 begin
(continues on next page)
3 package Events is
4
14 end Events;
4 package Events.Lists is
5
14 private
15
24 end Events.Lists;
38 end Events.Lists;
8 procedure Main is
9 type Test_Case_Index is
10 (Event_List_Chk);
11
29 begin
30 if Argument_Count < 1 then
31 Put_Line ("ERROR: missing arguments! Exiting...");
32 return;
33 elsif Argument_Count > 1 then
34 Put_Line ("Ignoring additional arguments...");
35 end if;
36
118.16.1 Concatenation
3 package Str_Concat is
4
15 end Str_Concat;
30 return US;
31 end Concat;
32
40 end Str_Concat;
7 procedure Main is
8 type Test_Case_Index is
9 (Unbounded_Concat_No_Trim_No_WS_Chk,
10 Unbounded_Concat_Trim_No_WS_Chk,
11 String_Concat_Trim_WS_Chk,
12 Concat_Single_Element);
13
55 begin
56 if Argument_Count < 1 then
57 Put_Line ("ERROR: missing arguments! Exiting...");
58 return;
59 elsif Argument_Count > 1 then
60 Put_Line ("Ignoring additional arguments...");
61 end if;
62
4 package Events is
5
15 end Events;
4 package Events.Lists is
5
14 private
15
24 end Events.Lists;
38 end Events.Lists;
7 with Events;
8 with Events.Lists; use Events.Lists;
9
10 procedure Main is
11 type Test_Case_Index is
12 (Unbounded_String_Chk,
13 Event_List_Chk);
14
38 begin
39 if Argument_Count < 1 then
40 Put_Line ("ERROR: missing arguments! Exiting...");
41 return;
42 elsif Argument_Count > 1 then
43 Put_Line ("Ignoring additional arguments...");
44 end if;
45
10 end Decibels;
15 end Decibels;
6 procedure Main is
7 type Test_Case_Index is
8 (Db_Chk,
9 Factor_Chk);
10
43 begin
44 if Argument_Count < 2 then
45 Put_Line ("ERROR: missing arguments! Exiting...");
46 return;
47 elsif Argument_Count > 2 then
48 Put_Line ("Ignoring additional arguments...");
49 end if;
50
118.17.2 Root-Mean-Square
9 end Signals;
15 end Signals;
11 end Signals.Std;
13 return S;
14 end;
15
33 return S;
34 end;
35
36 end Signals.Std;
7 procedure Main is
8 type Test_Case_Index is
9 (Sine_Signal_Chk,
10 Square_Signal_Chk,
11 Triangular_Signal_Chk);
12
37 begin
38 if Argument_Count < 1 then
39 Put_Line ("ERROR: missing arguments! Exiting...");
40 return;
41 elsif Argument_Count > 1 then
42 Put_Line ("Ignoring additional arguments...");
43 end if;
44
118.17.3 Rotation
4 package Rotation is
5
10 end Rotation;
18 end Rotation;
3 package Angles is
4
11 end Angles;
15 end Angles;
7 end Rotation.Tests;
12 --
13 -- Adapt value due to floating-point inaccuracies
14 --
15
52 end Rotation.Tests;
6 procedure Main is
7 type Test_Case_Index is
8 (Rotation_Chk,
9 Angles_Chk);
10
21 begin
22 if Argument_Count < 2 then
23 Put_Line ("ERROR: missing arguments! Exiting...");
24 return;
25 elsif Argument_Count > 2 then
26 Put_Line ("Ignoring additional arguments...");
27 end if;
28
2057
Learning Ada
Workshop project: Learn to write maintainable bug-free code with SPARK Ada.
This document was written by Robert Tice.
Note: The code examples in this course use an 80-column limit, which is a typical limit
for Ada code. Note that, on devices with a small screen size, some code examples might
be difficult to read.
387 http://creativecommons.org/licenses/by-sa/4.0
2059
Learning Ada
2060
CHAPTER
NINE
In this lab we will build a stack data structure and use the SPARK provers to find the errors
in the below implementation.
119.1 Background
2061
Learning Ada
The list starts out empty. Each time we push a character onto the stack, Last increments
by 1.
Popping items from the stack
Here's what should happen if we popped 2 characters off our stack & then clear it.
Note that pop and clear don't unset the Storage array's elements, they just change the
value of Last.
119.3 Constraints
If the stack currently has the characters "M", "L", and "H" then the program should print
the stack like this:
[M, L, H]
MLHdpdpdpd
Listing 1: stack.ads
1 package Stack with SPARK_Mode => On is
2
11 procedure Clear
12 with Post => Size = 0;
13
17 Max_Size : constant := 9;
(continues on next page)
32 end Stack;
Listing 2: stack.adb
1 package body Stack with SPARK_Mode => On is
2
3 -----------
4 -- Clear --
5 -----------
6
7 procedure Clear
8 is
9 begin
10 Last := Tab'First;
11 end Clear;
12
13 ----------
14 -- Push --
15 ----------
16
23 ---------
24 -- Pop --
25 ---------
26
34 ---------
35 -- Top --
36 ---------
37
Listing 3: main.adb
1 with Ada.Command_Line; use Ada.Command_Line;
2 with Ada.Text_IO; use Ada.Text_IO;
3 with Stack; use Stack;
4
8 -----------
9 -- Debug --
10 -----------
11
12 procedure Debug
13 is
14 begin
15
18 Put ("[");
19 for I in Stack.Tab'First .. Stack.Size - 1 loop
20 Put (Stack.Tab (I) & ", ");
21 end loop;
22 Put_Line (Stack.Tab (Stack.Size) & "]");
23 else
24 Put_Line ("[]");
25 end if;
26
27 end Debug;
28
29 S : Character;
30
31 begin
32
33 ----------
34 -- Main --
35 ----------
36
43 if S = 'd' then
44 Debug;
45 elsif S = 'p' then
46 if not Stack.Empty then
47 Stack.Pop (S);
48 else
49 Put_Line ("Nothing to Pop, Stack is empty!");
50 end if;
51 else
52 if not Stack.Full then
53 Stack.Push (S);
54 else
55 Put_Line ("Could not push '" & S & "', Stack is full!");
56 end if;
57 end if;
(continues on next page)
60 end loop;
61
62 end Main;
378 https://www.adacore.com/uploads/techPapers/HighIntegrityAda.pdf
2069