Bug: Aggregate with iterator over empty array not assignable

edit: fixed a copy-paste mistake in code

Looking at ways for making loops that work on slices more concise, such as:

for I in Input'Range loop
   A (Offset + I - 1) := Input (I) * 2;
end loop;

I arrived at the following formula:

A (Offset .. Offset + Input'Length - 1) := [for X of Input => X * 2];

I find the second form more to the point.

Ignoring efficiency differences (favouring loops), the primary practical difference is that the aggregate formula raises a Constraint_Error when the Input array is empty.

There is no point in describing the problem in words as there’s more to it, so I’ll just post the sample code and a link to Compiler Explorer.
I looked at the GCC Bug List and found one open issue that’s somewhat, if not entirely, related [1].
I’ll wait a while before reporting the bug in case someone comes and explains to me that this is expected behaviour.

Compiler Explorer, old
Compiler Explorer, modified line 55

Code
with Ada.Assertions;

procedure Example is

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

   A : Arr := [1, 999 ,3,4,5,6,7,8,9];

   Non_Empty : constant Arr (1 .. 1) := [1];

   Empty : Arr (1 .. 0);

   Pos : constant Positive := 2;  --  arbitrary position for insertion

begin
   --  Copying array elements into a slice of another array
   --   using an aggregate with iterator.


   --  OK: non-empty "input"
   A (Pos .. Pos + Non_Empty'Length - 1) := [for X of Non_Empty => X * 2];
   --  A (2 .. 2) := [1 * 2];

   Ada.Assertions.Assert (A (2) = Non_Empty (1) * 2);
   --  --------------------------------------------------------------------


   --  BAD: empty input
   begin

      A (Pos .. Pos + Empty'Length - 1) := [for X of Empty => X * 2];
      --  A (2 .. 1) := [aggregate iterating over an empty array]
      
      Ada.Assertions.Assert (False, "unreachable section");
   exception
      when Constraint_Error => Null; -- expected
   end;
   --  --------------------------------------------------------------------


   --  OK: empty input without iterator (although with spurious warning message?)
   begin

      A (Pos .. Pos + Empty'Length - 1) := [for I in Empty'Range => Empty (I) * 2];

   end;
   --  --------------------------------------------------------------------


   --  OK: empty input through a temporary
   declare
      Maybe_Empty : constant Arr := [for X of Empty => X * 2];
   begin

      A (Pos .. Pos + Maybe_Empty'Length - 1) := Maybe_Empty;
      --  A (2 .. 1) := []
   
   end;
   --  --------------------------------------------------------------------


   --  OK: empty input with a loop equivalent
   for I in Empty'Range loop
      A (Pos + I - 1) := Empty (I) * 2;
   end loop;
   --  --------------------------------------------------------------------

end Example;

[1] GCC : Bug 106168 - Errors with empty array aggregate.

1 Like

You do not show the corresponding type declaration.
type Arr is array (Integer range <>) of Anything;

This declaration,
Empty : constant Arr:= [];
must raise Constraint_Error because the empty index range is Integer'First .. Integer'First-1.
Empty: constant Arr = [I .. I-1 => Val],
should work for any I > Integer'First.

The RM examples 3.6(26) and 3.3.3(47/5) are unfortunate.
I raised a corresponding GitHub issue: #164

The first two snippets are for comparison between two forms.

The type declaration along with the complete program is below in the Code section and in compilable form on

This is the contents of the GCC bug I mentioned.
I suppose you may be referring to this line in my code:

Empty : Arr (1 .. 0); -- index is Positive range <>

I don’t know the intricacies of array / range semantics, but the above indeed does not raise a Constraint_Error.
Using a Positive range, as I do, also happens to be the working solution suggested in the GCC issue.
Regardless, changing the indexing of the empty array to be within Positive does not change the flow of the program.

Empty : Arr (2 .. 1);

It’s the aggregate that raises the exception. Declaring the object with index in range does not help. The ranges slide on assignment, but the aggregate to be assigned cannot be created.

You’re effectively saying that the Constraint_Error in

--  BAD: empty input
begin

  A (Pos .. Pos + Empty'Length - 1) := [for X of Empty => X * 2];
  --  A (2 .. 1) := [aggregate iterating over an empty array]
  
  Ada.Assertions.Assert (False, "unreachable section");
exception
  when Constraint_Error => Null; -- expected
end;

is valid, and it follows that the non-raising workarounds below should also raise:

--  OK: empty input without iterator (although with spurious warning message?)
begin

  A (Pos .. Pos + Empty'Length - 1) := [for I in Empty'Range => Empty (I) * 2];

end;

--  OK: empty input through a temporary
declare
  Maybe_Empty : constant Arr := [for X of Empty => X * 2];
begin

  A (Pos .. Pos + Maybe_Empty'Length - 1) := Maybe_Empty;
  --  A (2 .. 1) := []
end;

Correct?

This is incorrect.
Iterating over the null range is allowed, and yields the expected no-op.

This is because, by definition, the null range does not.
Part of the idea is to make unconstrained arrays recursively usable; consider the following:

Generic
  Type Element is limited private;
Package Optional_Vector is
  Type Index    is new Boolean range True..True;
  Type Optional is array (Index range <>) of Element;
  --…
  Empty : Constant Optional;
  --…
Private
  Empty : Constant Optional:= (True..False => <>);
End Optional_Vector;

Here we have an unconstrained array, it could be of length 0 to Index'Pos(Index'Last) - Index'Pos(Index'First). The Index is constrained to (True), but we don’t need True if the array is empty, hence why the aggregate (True..False => <>) does not raise Constraint_Error: There is no Empty(False), and because it is empty there is no Empty(True) either.

type Index    is new Boolean range True..True;
type Optional is array (Index range <>) of Element;
Empty : constant Optional:= (True..False => <>);

This is possible because the value False still exists. It’s in the subtype Index’Base.

3.5(4) If R is less than L, then the range is a null range.
If L is Integer’First, R cannot be less.

type My_String is array (Integer range <>) of Character;
Y: My_String := "";

This fails:
error: null string literal not allowed for type “My_String”
error: static expression fails Constraint_Check

Hm - I do not find a statement in the RM how R and L are computed for []. However [] is equivalent to "" for strings.
(And my conpiler does not yet accept brackets.)

So it appears that there is an agreement there’s an asymmetry in how the compiler behaves, but there is no agreement on what’s at fault for the asymmetry.

Which —while slightly surprising— is not strictly equivalent to what I posted.
If you initialize it differently, Y: My_String := (1..0 => <>);, it is perfectly fine.

This is not a positional aggregate - it does not slide in this case.

It’s quite simple: Positional aggregates start with Index’First and [] is a positional aggregate.

BTW: "" and [] are identical for empty strings, but not for other positional aggregates: "ABC" vs. ['A', 'B', 'C']. Both start with Index’First.

Summary:

type Arr is array (Integer range <>) of Anything;
Empty: constant Arr := [];                       -- CE
Empty: constant Arr := (1..0 => <>);             -- OK, does not slide
Empty: constant Arr (4 .. 3) := [];              -- CE
Empty: constant Arr (4 .. 3) := [1 .. 0 => <>];  -- OK, slides

For an aggregate for an unconstrained 1D array type T with index subtype S:

Positional aggregates’ bounds start with S’First. A string literal is a special form of positional aggregate for string types.

A null range has its upper bound < its lower bound. The bounds need not be in S, but must be in S’Base.

"” and [] are both positional aggregates with null ranges, so their bounds are both S’First .. S'Base'Pred (S’First). If S’First = S’Base’First, these null-range positional aggregates are not allowed.

Explicit null ranges may be given for S if S’Base’First < S’Base’Last.