So You Think You Know Ada?
This is yet another article I wrote a long time ago but never published, but it was a joy to proof-read it before publishing now. Ada is a very well-designed language. It looks funky with its Pascal-like syntax, but it’s clear people thought hard about how things should fit together to give the programmer a good experience. There are reasonable alternatives to Ada these days (D, Rust, among others) but Ada will always hold a special place in my heart.
In the style of So You Think You Know C? I present this article. Fair warning, though: it may be much easier than you would have hoped. I have converted the C code to Ada, and I have tried to keep it as faithful to the original as possible.
1. A Stroll Down Memory Lane
Tne following code is supposed to output some number. Can you tell which?
with Ada.Integer_Text_IO; procedure Main is type S_Type is record I : Integer; C : Character; end record; S : S_Type; begin Ada.Integer_Text_IO (S'Size); end Main;
- a
- 40 bits
- b
- 64 bits
- c
- Compilation error
- d
- Runtime error
- e
- I don’t know
Answer
The correct answer would be “I don’t know”, because how much memory the compiler allocates for an object is an important opimisation question. In fact, on my system, the compiler refuses to allocate anything less than 64 bits. However, for the picky, there are several ways to control the specific size and layout used for records, depending on what our requirements are:
- The
Bit_Order
attribute allows control over the endianness of objects, - The
Alignment
attribute allows control over the alignment of objects, - The
Size
attribute allows control over allocated space for an object, - The
Component_Size
attribute allows control over allocated space for each element in an array, and - A record representation clause let’s us specify down to the bit level exactly how the compiler should lay out a record.
2. The Size of Arithmetic Intermediary Results
with Ada.Integer_Text_IO; procedure Main is type Short_Integer is range -2**8+1 .. 2**8-1; A : Character := 0; B : Short_Integer := 0; begin Ada.Integer_Text_IO (B'Size = (A+B)'Size); end Main;
- a
- 0
- b
- 1
- c
- Compilation error
- d
- Runtime error
- e
- I don’t know
Answer
The most important out of the way first: the addition operator in A+B
is
only defined when A
and B
are of the same type. Since A
and B
are
different types in this case, we will get a compilation error.
When defined, the result of addition will be of the same type as the operands
– unless the result does not fit into the range of that type, in which case a
Constraint_Error
exception will be thrown.1 In other words, simple
signed addition does not cause undefined behaviour in Ada.
The type system shows through in a couple of other places in this code, though:
- It is illegal to assign
0
to a variable of typeCharacter
. Why? BecauseCharacter
is an enumeration type covering the 256 codepoints in the first row of unicode basic multilingual plane – it is not a numeric type. - The result of the comparison operator
=
is of boolean type, and Ada will not let us pretend it’s an integer.
Oh, and we also can’t get the Size
attribute of an intermediary value –
this is mostly just because that’s left out of the syntax. To get the size,
we have to have a variable storing the object. That variable will have to
have a type explicitly specified, which avoids the unknowns in implicit type
conversion too.
So, yes, this is very much a compilation error.
3. The Value of Characters
with Ada.Integer_Text_IO; procedure Main is A : Character := ' ' * 13; begin Ada.Integer_Text_IO (A); end Main;
- a
- 20 (0x14)
- b
- 260 (0x104)
- c
- Compilation error
- d
- Runtime error
- e
- I don’t know
Answer
You probably see the theme already: this is yet another big, honking compilation error because we can’t multiply things of different types.
If we actually want the multiplication, we can get the numeric value of
space as Character'Pos(' ')
, but then of course we can’t store the result
in a variable of character type. 2 There is also a slightly imaginative
alternative interpretation of the intended meaning of the program: the
standard library Ada.Strings.Fixed
overloads the multiplication operator
such that we can do ' ' * 13
and get the string " "
. But
that is a string and thus cannot be stored in a Character
type variable.
4. Shift Happens
with Ada.Integer_Text_IO; with Interfaces; procedure Main is use type Interfaces.Unsigned_32; I : Interfaces.Unsigned_32 := 32; J : Interfaces.Unsigned_32 := Interfaces.Shift_Right_Arithmetic (Interfaces.Shift_Left (I, Integer (I)), Integer (I)); begin Ada.Integer_Text_IO.Put (Integer (I)); Ada.Integer_Text_IO.Put (Integer (J)); end Main;
- a
- 32 0
- b
- 32 32
- c
- Compilation error
- d
- Runtime error
- e
- I don’t know
Answer
I should lead this by saying that in the original C code, it is not specified whether the author intended for the right shifts to be arithmetic or logical. It doesn’t matter, because we can’t know what the code will produce anyway, but it’s an important distinction that is explicit in the Ada code above.
In principle, this should yield 32 32
, because we’re shifting by a certain
amount, and then shifting back by the same amount. However, bit shifting is
not considered a numeric operation in Ada, so we shouldn’t expect infinite
precision. Bit shifts are thought of as low-level operations needed primarily
to interface with hardware or other languages. That’s why we had to import it
from the Interfaces
standard library.
So the question is: what does Ada 2012 deal with overshifting?
I don’t know. Is it unknowable? I also don’t know.
I have found references to people who should know what they are doing who work under the assumption that overshifting is specified as silently dropping the excess significant bits. I think this is indeed what happens in most (all?) implementations, but the standard does not mention it. The compiler documentation is just as silent.
What I do know is this: shifting is considered an Intrinsic
. This means
that no implementation is provided in the library, because the compiler is
responsible for knowing how to generate efficient code for those functions.
This could be interpreted as shifts being implementation defined. I think the
defined part of “implementation defined” means that at worst, an
implementation must define it to throw an exception at run time. Crucially,
that interpretation means it cannot result in erroneous execution, which is
the Ada equivalent of C’s infamous undefined behaviour, and what we
desperately want to avoid.
If so, I could chalk it up to sloppy documentation writing for the compiler, but I’m still not quite satisfied. At some point in the future, when I have more time to spare, I’d have to follow up on it.
5. Sequence Points
There is no way to construct a post-increment operator in Ada, but I believe this can be made interesting anyway.
with Ada.Integer_Text_IO; procedure Main is I : Integer := -1; function Increment (I : in out Integer) return Integer is begin I := I + 1; return I; end Increment; begin Ada.Integer_Text_IO.Put (Increment (I) / Increment (I)); end Main;
- a
- 0
- b
- 1
- c
- Compilation error
- d
- Runtime error
- e
- I don’t know
Answer
In all cases, this is a compilation error. However, the exact error message will differ a little depending on which version of Ada we use.
Ada 2005 is the less interesting one, because in all Ada versions prior to
2012, functions were not allowed to have parameters marked out
, so the
Increment
function triggers a compilation error. This restriction was put
in place exactly to prevent problems like this.
This means that if we want to call a subprogram that modifies variables in the environment it’s called, we’ll have to call it as a separate statement, and we can’t embed it in an expression where data dependencies are uncertain. Restricting functions like this is a pretty blunt tool, but if safety and reliability are important to us, we might consider this worth the little flexibility we lose.
In Ada 2012, the restriction on out
parameters in functions was lifted, and
functions are allowed to modify variables in their environment. This is
possible because in Ada 2012, it is instead specified on a more fine-grained
level in which cases functions with out
parameters are allowed. If the
compiler detects an expression where the same object is supplied to an out
mode parameter more than once, it will end compilation saying something along
the lines of “conflict of writable function parameter in construct with
arbitrary order of evaluation.”
Summary
In all five of these examples, the answer in the case of C was “I don’t know”. Let’s see how Ada compares:
- I don’t know. Defined but unpredictable. Decided by the compiler somewhere in the deep dark woods of optimisation. Can be controlled with representation aspects.
- Compilation error. Clarify intent to successfully compile.
- Compilation error. Clarify intent to successfully compile.
- I don’t know. Answer truly unknown, but likely defined to be one of the reasonable alternatives.
- Compilation error. Clarify intent to successfully compile.
What did we learn from this experiment?
Well, many tricky C questions really boil down to code where it is unclear what the programmer really meant for their code to do. What frightens me is that these examples of unclear intent were discovered when the author was doing
safety critical PLC programming […] it was a research project in nuclear power plant automation
I get that C is dominant, but are we not past the stage where someone hears “nuclear power plant automation systems” and immediately go “yeah, that’s the ideal use case for C.” There are so many large security issues that can be traced back to an underspecified C construct, and I struggle to understand how people rationalise trusting their critical infrastructure and their lives to it, when it’s so easy not to.
Note that I’m not suggesting Ada code will be free from error – anyone who looks up Ada eventually runs across the Ariane 5 rocket crash. But have you seen the code for that? They had to work really hard to force Ada to do the wrong thing. The correct code would have been easier to write and would not have suffered from the same bug because it would have been a compiler error.