So You Think You Know Ada?
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 your 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 you 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. The result of
addition in general 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.11 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 you pretend it’s an integer, obviously.
Oh, and you 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,
you 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 you can’t multiply things of different types. If
you actually want the multiplication, you can get the numeric value of space
as Character'Pos(' ')
, but then of course you can’t store the result in a
variable of character type. 22 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 " "
.
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
What happens here depends on which version of Ada you use. We’ll start with
Ada 2005, because that’s the less interesting one. 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 you want to call a subprogram that modifies variables in the environment it’s called, you’ll have to call it as a separate statement, and you 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 is important to you, you should immediatly see how it’s worth the little flexibility you 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
To recap, these were the answers:
- I don’t know.33 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.44 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 unclear intent; it is simply unclear what the programmer really intended for their code to do. What frightens me is that the author of the original article discovered these cases of unclear intent was doing
safety critical PLC programming […] it was a research project in nuclear power plant automation
I wish I could find words to express how incredibly disappointed it makes me that people hear about nuclear power plant automation systems and they immediately think, “Hey, I know! Let’s use C for this.” After all the small (and large) security issues that can be traced back to an underspecified C construct, I simply cannot understand how people rationalise trusting their critical infrastructure and their lives with it, when it’s so easy not to.
Ending on a slightly more optimistic note, I’ll attempt to retain the take-home message of the original author. In the end, a programming language doesn’t risk lives: programmers do. Technology will never absolve you from responsibility. Safety is an attitude, not a feature.
Learn an engineering attitude: trust measurements, not presumptions; rely on the standard instead of folklore.