Constraint_Error raised when using modular types not divisble by 8

Viewed 232

I've encountered an issue where using modular types in Ada that are not divisible by the system's Storage_Unit ( as defined in the runtime's system.ads ) will raise a Constraint_Error at runtime when accessed. I originally encountered this issue working on an bare-metal system using a minimal runtime while trying to read 12bit values from a buffer by overlaying the 12bit array over the buffer in memory. Does anyone know why this is occurring?

The following minimal example illustrates the issue I'm encountering. I tested this using AdaCore's GNAT 2019, compiled with the included zfp runtime. Using the standard runtime does not reproduce the issue.

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is
   ----------------------------------------------------------------------------
   --  Modular type with standard size divisible by 8.
   ----------------------------------------------------------------------------
   type Type_One is mod 2 ** 16;
   type Buffer_Type_One is array (1 .. 128) of Type_One;

   ----------------------------------------------------------------------------
   --  Modular type with non-base 8 size.
   ----------------------------------------------------------------------------
   type Type_Two is mod 2 ** 12;
   type Buffer_Type_Two is array (1 .. 128) of Type_Two;

   type Buffer is array (1 .. 256) of Character;

   ----------------------------------------------------------------------------
   --  Example buffer.
   ----------------------------------------------------------------------------
   Test_Buffer : Buffer := (others => ' ');
begin
   ----------------------------------------------------------------------------
   --  Will not raise an exception.
   ----------------------------------------------------------------------------
   Test_One :
      declare
         Buffer_One : Buffer_Type_One
         with Import,
         Convention => Ada,
         Address    => Test_Buffer'Address;
      begin
         Put_Line ("Testing type one");
         for I in Natural range 1 .. 16 loop
            Put_Line ("Test: " & Buffer_One (I)'Image);
         end loop;
      end Test_One;

   ----------------------------------------------------------------------------
   --  Will raise a constraint error at runtime.
   ----------------------------------------------------------------------------
   Test_Two :
      declare
         Buffer_Two : Buffer_Type_Two
         with Import,
         Convention => Ada,
         Address    => Test_Buffer'Address;
      begin
         Put_Line ("Testing type two");
         for I in Natural range 1 .. 16 loop
            Put_Line ("Test: " & Buffer_Two (I)'Image);
         end loop;
      exception
         when Constraint_Error =>
            Put_Line ("Constraint error encountered.");
      end Test_Two;

end Main;

Here is the project file I used to compile this example:

project Test is

   for Object_Dir use "obj";
   for Exec_Dir use "build";
   for Create_Missing_Dirs use "True";

   for Languages use ("Ada");

   package Builder is
      for Executable ("main.adb") use "test";
   end Builder;

   for Main use ("main.adb");

   package Compiler is
      for Default_Switches ("Ada") use (
        "-gnat2012",
        "-gnatwadehl",
        "-gnatVa",
        "-gnaty3abcdefhiklmnoprstux"
      );
   end Compiler;

   for Runtime ("Ada") use "zfp";
end Test;

I can't seem to find anything in the RM that would indicate why this would happen.

EDIT: Simon Wright below has figured out why this is happening. My naive understanding was that an instance of Buffer_Type_Two overlaid at the specified memory address would interpret the memory at this location as a sequence of 12bit values. It appears that this is not the case. It appears as though the compiler is rounding the size of the type up to 16bits, then raising a Constraint_Error when the 16bit value read from the array does not conform to the 12bit type.

If anyone can think of a better way to read a sequence of 12bit values from a location in memory in a sequential way I would greatly appreciate it, thank you.

3 Answers

With recent GNATs, you can achieve the behaviour you want by defining Buffer_Type_Two as

type Buffer_Type_Two is array (1 .. 128) of Type_Two
  with Pack;

ARM 13.2(9) warns that this may not do what you want for 13-bit values (recent GNATs do, though).

An alternative would be

type Buffer_Type_Two is array (1 .. 128) of Type_Two
  with Component_Size => 12;

The results are

...
Testing type two
Test:  32
Test:  514
Test:  32
Test:  514
...

For 13 bits, with either approach,

...
Testing type two
Test:  32
Test:  257
Test:  2056
Test:  64
Test:  514
Test:  4112
Test:  128
Test:  1028
Test:  32
...

HOWEVER, for an embedded target, you’ll need to use the -full- runtime system; for others, as pointed out by @egilhh above,

ajxs.adb:14:09: packing of 12-bit components not supported by configuration

Considering the compilation warnings, the code didn’t really deserve to work ...

31.          Buffer_One : Buffer_Type_One
32.            with Import,
33.              Convention => Ada,
34.              Address    => Test_Buffer'Address;
                 |
    >>> warning: specified address for "Buffer_One" may be inconsistent with alignment
    >>> warning: program execution may be erroneous (RM 13.3(27))
    >>> warning: alignment of "Buffer_One" is 2
    >>> warning: alignment of "Test_Buffer" is 1

and

49.          Buffer_Two : Buffer_Type_Two
50.            with Import,
51.              Convention => Ada,
52.              Address    => Test_Buffer'Address;
                 |
    >>> warning: specified address for "Buffer_Two" may be inconsistent with alignment
    >>> warning: program execution may be erroneous (RM 13.3(27))
    >>> warning: alignment of "Buffer_Two" is 2
    >>> warning: alignment of "Test_Buffer" is 1

but that’s not the problem, as it happens: Type_Two is mod 2**12, i.e. mod 4096, but the value in Buffer_Two(1) is 16#2020# (two space characters), which is 8224 in decimal.

"But why isn’t the stored value automatically masked to the 12 bits I asked for?" you say. For reasons of efficiency, "the size of an object is not necessarily the same as the size of the type of [the] object", GNAT RM 4.43, and GNAT expects the spare 4 bits at the top of the 16-bit word to be zero. On its own, a value of type Type_Two occupies (has ’Object_Size of) 16 bits. You could get the 12-bit size you want by including the Type_Two field in a record and specifying its layout, but that does add complication.

The problem wasn’t detected here without the -gnatVa (turn on all validity checking options).

The physical value of 16#2020# is not in the range of Type_Two so Constraint_Error is expected with -gnatVa.

GNAT CE 2020 raises Constraint_Error.

Uncommenting the exception/when part leads to further information:

raised CONSTRAINT_ERROR : main.adb:51 invalid data

Related