Next: , Up: Compatibility and Porting Guide


18.1 Writing Portable Fixed-Point Declarations

The Ada Reference Manual gives an implementation freedom to choose bounds that are narrower by Small from the given bounds. For example, if we write

    type F1 is delta 1.0 range -128.0 .. +128.0;

then the implementation is allowed to choose -128.0 .. +127.0 if it likes, but is not required to do so.

This leads to possible portability problems, so let's have a closer look at this, and figure out how to avoid these problems.

First, why does this freedom exist, and why would an implementation take advantage of it? To answer this, take a closer look at the type declaration for F1 above. If the compiler uses the given bounds, it would need 9 bits to hold the largest positive value (and typically that means 16 bits on all machines). But if the implementation chooses the +127.0 bound then it can fit values of the type in 8 bits.

Why not make the user write +127.0 if that's what is wanted? The rationale is that if you are thinking of fixed point as a kind of 'poor man's floating-point', then you don't want to be thinking about the scaled integers that are used in its representation. Let's take another example:

    type F2 is delta 2.0**(-15) range -1.0 .. +1.0;

Looking at this declaration, it seems casually as though it should fit in 16 bits, but again that extra positive value +1.0 has the scaled integer equivalent of 2**15 which is one too big for signed 16 bits. The implementation can treat this as:

    type F2 is delta 2.0**(-15) range -1.0 .. +1.0-(2.0**(-15));

and the Ada language design team felt that this was too annoying to require. We don't need to debate this decision at this point, since it is well established (the rule about narrowing the ranges dates to Ada 83).

But the important point is that an implementation is not required to do this narrowing, so we have a potential portability problem. We could imagine three types of implementation:

  1. those that narrow the range automatically if they can figure out that the narrower range will allow storage in a smaller machine unit,
  2. those that will narrow only if forced to by a 'Size clause, and
  3. those that will never narrow.

Now if we are language theoreticians, we can imagine a fourth approach: to narrow all the time, e.g. to treat

    type F3 is delta 1.0 range -10.0 .. +23.0;

as though it had been written:

    type F3 is delta 1.0 range -9.0 .. +22.0;

But although technically allowed, such a behavior would be hostile and silly, and no real compiler would do this. All real compilers will fall into one of the categories (a), (b) or (c) above.

So, how do you get the compiler to do what you want? The answer is give the actual bounds you want, and then use a 'Small clause and a 'Size clause to absolutely pin down what the compiler does. E.g., for F2 above, we will write:

    My_Small : constant := 2.0**(-15);
    My_First : constant := -1.0;
    My_Last  : constant := +1.0 - My_Small;
    
    type F2 is delta My_Small range My_First .. My_Last;

and then add

    for F2'Small use my_Small;
    for F2'Size  use 16;

In practice all compilers will do the same thing here and will give you what you want, so the above declarations are fully portable. If you really want to play language lawyer and guard against ludicrous behavior by the compiler you could add

    Test1 : constant := 1 / Boolean'Pos (F2'First = My_First);
    Test2 : constant := 1 / Boolean'Pos (F2'Last  = My_Last);

One or other or both are allowed to be illegal if the compiler is behaving in a silly manner, but at least the silly compiler will not get away with silently messing with your (very clear) intentions.

If you follow this scheme you will be guaranteed that your fixed-point types will be portable.