Two Wrongs

So You Think You Know Ada?

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:

  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 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:

  1. 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.
  2. Compilation error. Clarify intent to successfully compile.
  3. Compilation error. Clarify intent to successfully compile.
  4. I don’t know.44 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 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.