1

Suppose the following generic procedure to print the elements of an array indexed by a discreet type (note the slight logic added to prevent the printing of an extra , past the end of the last element):

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Array_Printer is
   
   generic
      type Index is (<>);
      type Int_Array is array (Index range <>) of Integer;
   procedure Print(IA: Int_Array); 
   
   procedure Print(IA: Int_Array) is
      -- Penultimate: Index := Index'Pred(IA'Last); -- raises CONSTRAINT_ERROR for IA'Length = 1
   begin
      Put("[");
      for I in IA'First .. Index'Pred(IA'Last) loop
         Put(IA(I), 0);Put(",");
      end loop;
      Put(IA(IA'Last), 0);
      Put("]");      
   end Print;
   
   type Int_Array is array(Positive range <>) of Integer;
   
   IA: Int_Array := (-3, -2, -1, 0, 1, 2, 3);
   IA2: Int_Array := (1 => 0);
   
   procedure Print_Int_Array is new Print(Index => Positive,
                                          Int_Array => Int_Array);
   
   begin
      Print_Int_Array(IA);   
end Array_Printer;

When this procedure runs with an array of length > 1 (e.g. IA) it correctly prints the array ([-3,-2,-1,0,1,2,3]). However, when it is given an array of length = 1 (e.g. IA2) then, perhaps surprisingly, the penultimate index calculation in the for-loop doesn't raise a CONSTRAINT_ERROR (due to a predecessor not existing) and the expected result ([0]) gets printed.

When that calculation is done elsewhere however (e.g. in the declarative section of the procedure) then that exception is raised, indeed.

Is the compiler smart enough to figure-out there's only one element in the array and hence generates a null range for the for loop?

Gnatstudio seems to be invoking gprbuild with -cargs -g -O0

Any thoughts? - Thanks

Sidisyom
  • 163
  • 5

2 Answers2

2

Try the following approach:

with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Array_Printer is

   generic
      type Index is (<>);
      type Int_Array is array (Index range <>) of Integer;
   procedure Print (IA : Int_Array);

   procedure Print (IA : Int_Array) is
   begin
      Put ("[");
      if IA'Length > 0 then
         Put (Item => IA (IA'First), Width => 0);
         if IA'Length > 1 then
            for I in Index'Succ (IA'First) .. IA'Last loop
               Put (",");
               Put (IA (I), 0);
            end loop;
         end if;
      end if;
      Put ("]");
   end Print;

   type Int_Array is array (Positive range <>) of Integer;

   IA  : Int_Array          := (-3, -2, -1, 0, 1, 2, 3);
   IA2 : Int_Array          := (Positive'Last => 0);
   IA3 : Int_Array (2 .. 1) := (others => 10);

   procedure Print_Int_Array is new Print
     (Index => Positive, Int_Array => Int_Array);

begin
   Print_Int_Array (IA);
   New_Line;
   Print_Int_Array (IA2);
   New_Line;
   Print_Int_Array (IA3);
end Array_Printer;

This procedure properly handles single element arrays with their starting index value at any valid value in the range of the index type, as well as multi-value arrays and arrays with no elements.

The output of this program is:

[-3,-2,-1,0,1,2,3]
[0]
[]
Jim Rogers
  • 4,822
  • 1
  • 11
  • 24
1

The calculation of 'Pred doesn't fail for your instantiation with Positive because Positive'Pred gives a reesult of Positive'Base, or Integer. It is when you attempt to store this result in an object of subtype Positive that you have a problem. However, with other actuals for Index this calculation may fail (try Integer rather than Positive).

The problem is thinking that you need to be able to calculate this value, since clearly there are situations where you can't. Rather one needs to solve the problem with the information that is definitely available. Naturally the procedure should work properly for arrays of any length. That leads to a solution such as

generic -- Print
   type Index is (<>);
   type Int_List is array (Index range <>) of Integer;
procedure Print (List : in Int_List);

procedure Print (List : in Int_List) is
begin -- Print
   Put (Item => '[');

   Put_All : for I in List'range loop
      Put (Item => List (I), Width => 0);

      if I < List'Last then
         Put (Item => ", ");
      end if;
   end loop Put_All;

   Put (Item => ']');
end Print;

Note that Ada.Text_IO has a Put for Character as well as for String. The difference between them is completely irrelevant, but I've always preferred to use a Character literal to a length-1 String literal whenever possible. This is probably due to my first Ada compiler being the Rolm/Data General compiler in 1984, which took about 10 minutes to compile the null program, so anything that might save the compiler some effort seemed worthwhile.

The main uses of arrays are as maps, sequences, and mathematical matrices and vectors. Your use seems to be a sequence. In common use, sequences are discussed using numerical values starting with one for positions: the first element, the second element, the 23rd element, the last element. It therefore makes sense for arrays used as sequences to be indexed by a numeric subtype with a lower value of 1, and for sequences to have a lower bound of 1. Adding such restrictions makes code using sequences easier to read and easier to get right:

generic -- Print
   type Index is range <> with Dynamic_Predicate => Index'First = 1;
   type Int_List is array (Index range <>) of Integer;
procedure Print (List : in Int_List) with Pre => List'First = 1;
Jeffrey R. Carter
  • 3,033
  • 9
  • 10
  • @Jim, good point on the empty array case! It certainly needs one extra length check. The current approach can be coded with one conditional less so _may_ be more readable, but not a huge issue, I reckon. The trouble with the original approach though is that it doesn't actually work for Array`Length = 1 and an enumeration index type (which are legal to instantiate with, based on the generic definition) as the "Index'Pred(IA'Last)" bit will fail. - Thanks! – Sidisyom Mar 13 '23 at 22:49
  • yup, that's exactly what's happening. And yeah, the assignment does work for an Integer index type. Also, calculating "Index'Pred(IA'Last)" in the for-loop for Array`Length = 1 and an enumeration index type does fail too. Perhaps the original solution would work if Index were restricted to an integer type i.e. "type Index is range <>;" Yeah, my initial approach was similar to the one you mention but was simply experimenting with an approach that doesn't require evaluating the conditional on every iteration. - Thanks! – Sidisyom Mar 13 '23 at 22:59
  • constraining the index to have a lower bound of 1 makes sense, however "Dynamic Predicate" doesn't seem to be allowed for formal type declaration by the compiler. – Sidisyom Mar 13 '23 at 23:05
  • So, my take on this is that when the Index type `is (<>)` then when the procedure is instantiated with an Integer type the `Index'Pred(IA'Last)` evaluation in the for-loop for an array of length 1 will always work because the result will be _some_ Integer which will cause a null range to be calculated. If the type is an enumeration however, then `Index'Pred(IA'Last)` will _always_ fail as calling that will raise some kind of underflow exception. – Sidisyom Mar 13 '23 at 23:14
  • '"Dynamic Predicate" doesn't seem to be allowed for formal type declaration' Yes, sorry, that's correct. There are other ways to achieve this, but the precondition is probably sufficient. – Jeffrey R. Carter Mar 14 '23 at 09:33
  • "experimenting with an approach that doesn't require evaluating the conditional on every iteration" This is an _output_ procedure. I/O is so much slower than anything else that any difference will be irrelevant in practice – Jeffrey R. Carter Mar 14 '23 at 09:40
  • You will also get Constraint_Error for a signed integer type if `IA'Last = Index'Base'First`, though that takes a little more effort to achieve. You might also want to consider what happens with a modular integer type. – Jeffrey R. Carter Mar 14 '23 at 09:48
  • Yeah, a modular-type index type fails with a Constraint_Error too. It would feel a bit more consistent and intuitive if the range-type index failed as well. i.e. it fails for an invalid index calculation for an enumeration and modular type but doesn't with an index-type of say `is range 1 .. 10` but aren't all these effectively types with some boundary constraints? You could argue for example that the modular type shouldn't fail as well since when the value is `0` it just "calculates" `-1` and results in a null range in the same way it does for a range type even though that's outside bounds – Sidisyom Mar 14 '23 at 19:43
  • But I'm sure it's far more complex implementation-wise :) – Sidisyom Mar 14 '23 at 19:43