Entropic Thoughts

So You Think You Know Ada?

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:

  1. It is illegal to assign 0 to a variable of type Character. Why? Because Character is an enumeration type covering the 256 codepoints in the first row of unicode basic multilingual plane – it is not a numeric type.
  2. 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:

  1. 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.
  2. Compilation error. Clarify intent to successfully compile.
  3. Compilation error. Clarify intent to successfully compile.
  4. I don’t know. Answer truly unknown, but likely defined to be one of the reasonable alternatives.
  5. 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.