AI22-0124-1

!standard 4.3(4/6)                                        25-08-24  AI22-0124-1/08

!standard 4.3.3(10/5)

!standard 4.3.3(11/4)

!standard 4.3.3(12)

!standard 4.3.3(13)

!standard 4.3.3(14)

!standard 4.3.3(14.1/6)

!standard 4.3.3(15/3)

!standard 4.3.3(15.1/5)

!standard 4.3.3(15.2/5)

!standard 4.3.3(16)

!standard 4.3.5(4/5)

!standard 4.3.5(6/6)

!standard 4.3.5(8/6)

!standard 4.3.5(12/5)

!standard 4.3.5(35/6)

!standard A.18.2(13/5)

!standard A.18.2(14.1/5)

!standard A.18.2(101/2)

!standard A.18.19(5.2/5)

!standard A.18.19(6.3/5)

!standard A.18.20(5.2/5)

!standard A.18.21(5.2/5)

!standard A.18.22(5.2/5)

!standard A.18.23(5.2/5)

!standard A.18.24(5.2/5)

!class amendment 25-01-02

!status Revision-202Y  25-05-30

!status WG9 Approved 25-10-08

!status ARG Approved  14-0-0  25-05-28

!status work item 25-01-02

!status received 24-10-23

!assigned author Randy Brukardt

!submitter Randall Brukardt

!priority Medium

!difficulty Medium

!subject Aggregates and capacity of bounded containers

!summary

Container aggregates optionally can get their initial capacity from a discriminant provided by the context in which the aggregate is used. This makes container aggregates more useful for bounded containers.

!issue

Bounded containers have a Capacity discriminant (and sometimes other discriminants). Some Ada operations require discriminants to match, and that makes using bounded containers more difficult than unbounded containers, especially when aggregates are involved.

Assume we have the following declarations:

package BVI is new Ada.Containers.Bounded_Vectors
   (Index_Type => Natural, Element_Type => Integer);
A : BVI.Vector(100);

 

If we wanted to assign a non-empty value to A, we could do it in multiple ways:

A := [1, 2]; -- (1)
BVI.Assign (Target => A, Source => [1, 2]); -- (2)
A := BVI.Copy (Source => [1, 2], Capacity => 100); -- (3)

   

Unfortunately, as the language is currently defined, (1) always will raise Constraint_Error. The aggregate will have Capacity set to 2, and that does not match the Capacity of A (which is 100). One always has to use (2) or (3) to do this assignment.

Similarly, if we wanted to initialize an object to this value:

A1 : BVI.Vector(100) := [1, 2]; -- (4)
A2 : BVI.Vector(100) :=
     BVI.Copy (Source => [1, 2], Capacity => 100); -- (5)

 

Again, (4) will always raise Constraint_Error. One has to write (5), and note doing so requires duplicating the capacity value.

   

One way to mitigate the effects of container bound mismatches is to try to ensure that all containers use the same capacity. Once an appropriate bound is determined, we could try to use a subtype to ensure that objects and parameters have the appropriate capacity.

This could look something like:

package BVI is new Ada.Containers.Bounded_Vectors
       (Index_Type => Natural, Element_Type => Integer);
Max_Elements : constant Natural := 100;
subtype My_Vector is BVI.Vector(Max_Elements);

 

If we also have a directive to initialize all objects (a commonly used style rule), declarations become challenging:

B : My_Vector := []; -- (6)
C : My_Vector := My_Vector'[]; -- (7)
D : My_Vector := BVI.Copy (Source => [], Capacity => Max_Elements); -- (8)
E : My_Vector := BVI.Empty(Capacity => Max_Elements); -- (9)

 

(6) always raises Constraint_Error. (7) also always raises Constraint_Error, as the qualification does not affect the capacity, and the aggregate does not have the same constraints as the qualifying subtype. (8) and (9) have the correct effect, but note that we have to repeat the capacity value in these calls. That somewhat defeats the purpose of giving the subtype a name in the first place.

If we use this subtype in parameters as well, then passing aggregates as parameters have the same challenges.

It would be better if bounded container aggregates got the correct capacity for their intended usage.

!recommendation

We avoided this problem for array aggregates by having an "applicable index constraint" determined from context to set the bounds. We can do something similar to determine the initial capacity of a bounded container aggregate.

We can generalize the applicable discriminant constraint to just “applicable constraint”. This follows the same set of rules as 4.3.3(10-16) in order to keep consistency between the kinds of aggregates.

We then add an additional choice to the Aggregate aspect to specify the “count discriminant”, which provides the value for discriminated types. The value of this discriminant is provided to the initializing functions rather than the number of elements that is calculated for the aggregate when the aggregate has an applicable constraint.

Finally, we add these new features to the existing bounded containers. We also have to add a New_Vector routine with a Capacity parameter to the Vector types, as this model requires that.

[Aside: The definition of New_Vector in A.18.2(14.1/5) is dubious; it requires the lower bound to be exactly Index_Type’First. It would make sense to allow any arbitrary lower bound, with the upper bound controlling the length of the resulting vector. However, it’s possible that this more flexible model was rejected during the development of container aggregates (perhaps having empty elements coming along was considered a problem).]

!wording

Add after 4.3(4/6):

[Editor's note: The following is a lightly modified version of the rules formerly found in

4.3.3(10-15.2) for array aggregates, including the associated AARM notes.]

Static Semantics

Some contexts provide an applicable constraint for an aggregate[Redundant:, which can provide values for bounds, discriminants, or other parts of an aggregate.] Each of the following contexts (and no other) define an applicable constraint for an aggregate:

[Editor’s notes: I simplified this wording from its original version, which also required that the target was a variable -- but as the target of a legal assignment is always a variable, that was just noise.

An alternative that was suggested was that an unconstrained object of a mutable type not provide an applicable constraint. I don’t believe that is a good idea, as many such objects will actually be constrained (whether a parameter is constrained depends on the actual parameter). For those, we would be re-introducing the original problem. (There is more on this topic in the AARM notes below). ]

AARM Reason: This case is broken out because the constraint comes from the actual bounds or discriminants of the variable (which always exist) rather than its nominal subtype (which might be unconstrained). We do not mention the “constraint” of the variable as that is not defined for mutable discriminated types.

AARM Ramification: This means that an assignment provides an applicable constraint even when the nominal subtype of the target is unconstrained. We use the discriminants of an object of a mutable type in this case, even though it is possible that they could have been changed by an assignment (it is also possible that they could not be changed, if the underlying object is constrained). Unfortunately, whether the discriminants of a mutual object can be changed is only determined at runtime (for parameters, it depends upon whether the actual object is constrained), so we cannot base the applicable constraint on that, as it feeds into Legality Rules.

AARM Discussion: Note that if the user wants to ensure that the aggregate has no applicable constraint, they can qualify the aggregate with the unconstrained type. This could be useful, for instance, to assign an aggregate with a different count discriminant to an unconstrained mutable object.

AARM Discussion: Here, the aggregate is being used within a larger aggregate.

AARM Discussion: Here, the base_expression is itself an aggregate used within a larger delta_aggregate.

AARM Discussion: RM83 omitted this case, presumably as an oversight. We want to minimize situations where an expression becomes illegal if parenthesized.

In the case of an explicit_actual_parameter (or default_expression) for a call on a generic formal subprogram, no applicable constraint is defined.

AARM Reason: This avoids generic contract model problems, because only mode conformance is required when matching actual subprograms with generic formal subprograms.

Modify 4.3.3(10/5):

An others choice is allowed for an array_aggregate only if an {applicable constraint (see 4.3) that provides an index constraint} [applicable index constraint] applies to the array_aggregate. {Such an applicable constraint is known as an applicable index constraint.} [Redundant: An applicable index constraint is a constraint provided by certain contexts that can be used to determine the bounds of the array value specified by an array_aggregate.][ Each of the following contexts (and none other) defines an applicable index constraint:]

Delete 4.3.3(11-15.2) along with the contained AARM notes.

Modify 4.3.3(16):

The applicable index constraint applies to an array_aggregate that appears in such a context, as well as to any subaggregates thereof.[ In the case of an explicit_actual_parameter (or default_expression) for a call on a generic formal subprogram, no applicable index constraint is defined.]

Delete AARM 4.3.3(16.a).

Replace 4.3.5(4/5) with:

   (Empty => name[,
    Count_Discriminant => discriminant_name][,
    Add_Named => procedure_name][,
    Add_Unnamed => procedure_name][,
    New_Indexed => function_name,
    Assign_Indexed => procedure_name])

Modify 4.3.5(6/6):

The name specified for Empty for an Aggregate aspect shall denote exactly one function with a result type of the container type that has no parameters, or that has one in parameter of a signed integer type{, called the element count type of the container type; the parameter is called the element count parameter of the function}.

AARM Reason: The {element count} parameter of the function, if present, {can}[may] be used to specify an initial size for the container, in anticipation of adding elements to it. {For an aggregate with an applicable constraint, the value of the element count discriminant in that constraint will be used for this parameter. Otherwise, for}[For] a positional aggregate, or a named aggregate that doesn't use an iterator, it will be initialized with the number of elements. For a named aggregate that uses an iterator, the implementation is permitted to estimate the number of elements that the iterator will produce, but it is not required to do so.

{The discriminant_name specified for Count_Discriminant shall denote a discriminant of the container type; this discriminant is known as the count discriminant for the container type.}

[Editor's notes: We don't need the "exactly one" wording since discriminants of a type cannot be overloaded. This wording is rather different from the wording for the Implicit_Dereference aspect that forms the model since this is not a stand-alone aspect, and I wanted to match the form of the existing wording in 4.3.5. We need to name this discriminant to simplify later wording. (Trust me, it's a tongue-twister without the name.)]

{AARM Discussion: The value of this discriminant will be passed to the element count parameter of the container creation functions.

For the language-defined bounded containers, this discriminant represents the capacity of the container. The element count parameter is used to specify the needed capacity of the newly constructed container. This will help the resulting aggregate match the enclosing applicable constraint, reducing constraint mismatches. End AARM Discussion.}

Modify 4.3.5(8/6):

The function_name specified for New_Indexed for an Aggregate aspect shall denote exactly one function with a result type of the container type, and two {in } parameters of the same type, called the key type of the container type{, and optionally a third in parameter of a signed integer type, called the element count type of the container type; the parameter is called the element count parameter of the function}.

[Editor's notes: the fact that the two existing parameters have to be "in" parameters was missing from this wording. Since the Dynamic Semantics is going to pass some constants (in most cases) to the parameters, some other mode will fail. All of the other rules here mention the required mode, so this is an obvious oversight, fixed above.

We're adding a parameter to this routine similar to the one for the Empty routine, so we can pass it when necessary.]

AARM Reason: See above for the purpose of the element count parameter.

Add after 4.3.5(11.2/6): [Legality Rules]

If the Aggregate aspect includes a Count_Discriminant choice, then the specified Empty and New_Indexed functions shall have an element count parameter, and the parameters shall have the same type. Additionally, the type of the specified discriminant shall be the same as the element count type of the container.

Add after 4.3.5(12/5): [Static Semantics]

If a descendant type of the container type T constrains the value of the count discriminant of T by a new discriminant, that new discriminant is the count discriminant of the descendant. If the descendant type constrains the value of the reference discriminant of T by an expression other than the name of a new discriminant, the value of this constraining expression is considered that of the count discriminant of this descendant in any applicable constraint.

[Editor's note: This is typical Bairdian nonsense, copied from the rules in 4.1.5. ;-)

The second part is needed because the "applicable constraint" of such a descendant does not nominally include any value for the count discriminant. So we say that it does, in order to keep the rest of the rules the same.]

Modify 4.3.5(35/6): [Dynamic Semantics]

if the aggregate is not an indexed aggregate, from a call on the Empty function specified in the Aggregate aspect.[ In the case of an Empty function with a formal parameter, the actual parameter has the following value:]

{If the Empty or New_Indexed function has an element count formal parameter, and T has a count discriminant and the context identifies an applicable constraint (see 4.3), the value of the count discriminant in the applicable constraint is used as the actual parameter for the element count parameter. Otherwise, the actual parameter for the element count parameter has the following value:}

[Editor’s note: The Dynamic Semantics section starts by defining T. It looks odd here, but would look OK in context.]

Add after A.18.2(13/5):

function To_Vector (Length, Capacity : Count_Type) return Vector
    with Pre  => (Capacity <= Maximum_Length
                     or else raise Constraint_Error) and then
                 (Length <= Maximum_Length
                     or else raise Constraint_Error),
         Post =>
            To_Vector'Result.Length = Length and then
            To_Vector'Result.Capacity >= Capacity and then
            To_Vector'Result.Capacity >= To_Vector'Result.Length
               and then
            not Tampering_With_Elements_Prohibited (To_Vector'Result)
               and then
            not Tampering_With_Cursors_Prohibited (To_Vector'Result);

Replace A.18.2(14.1/5):

function New_Vector (First, Last : Index_Type) return Vector is
     (To_Vector (Count_Type (Last - First + 1)))
   with Pre => First = Index_Type'First;

with:

function New_Vector (First, Last : Index_Type;
   Capacity : Count_Type := <implementation-defined>) return Vector is
      (To_Vector (Count_Type (Last - First + 1)), Capacity)
   with Pre => First = Index_Type'First;

Add after A.18.2(101/2):

   function To_Vector (Length, Capacity : Count_Type) return Vector
      with Pre  => (Capacity <= Maximum_Length
                       or else raise Constraint_Error) and then
                   (Length <= Maximum_Length
                       or else raise Constraint_Error),
          Post =>
            To_Vector'Result.Length = Length and then
            To_Vector'Result.Capacity >= Capacity and then
            To_Vector'Result.Capacity >= To_Vector'Result.Length
                and then
            not Tampering_With_Elements_Prohibited (To_Vector'Result)
                and then
            not Tampering_With_Cursors_Prohibited (To_Vector'Result);

Returns a vector created by calling Empty (Count_Type'Max (Length, Capacity)),

and then filling the vector with Length empty elements.

[Note: We allow the Capacity to be less than the Length, so that the default capacity value will always work for the regular vector container.]

Add after A.18.19(5.2/5):

The aspect_definition for aspect Aggregate for type Vector is changed to:

Aggregate      => (Empty              => Empty,
                   Count_Discriminant => Capacity,
                   Add_Unnamed        => Append,
                   New_Indexed        => New_Vector,
                   Assign_Indexed     => Replace_Element),

Add after A.18.19(6.3/5):

In function To_Vector with a Capacity parameter, the postcondition is altered

to:

      To_Vector'Result.Length = Length and then
      To_Vector'Result.Capacity = Capacity and then
      To_Vector'Result.Capacity >= To_Vector'Result.Length and then
      not Tampering_With_Elements_Prohibited (To_Vector'Result)
         and then
      not Tampering_With_Cursors_Prohibited (To_Vector'Result);

 

The description of function To_Vector with a Capacity parameter is replaced with:

Returns a vector with a capacity of Capacity and length of Length, filled with empty elements.

Add after A.18.20(5.2/5):

The aspect_definition for aspect Aggregate for type List is changed to:

Aggregate        => (Empty              => Empty,
                     Count_Discriminant => Capacity,
                     Add_Unnamed        => Append),

Add after A.18.21(5.2/5):

The aspect_definition for aspect Aggregate for type Map is changed to:

   Aggregate    => (Empty              => Empty,
                    Count_Discriminant => Capacity,
                    Add_Named          => Insert),

Add after A.18.22(5.2/5):

The aspect_definition for aspect Aggregate for type Map is changed to:

   Aggregate    => (Empty              => Empty,
                    Count_Discriminant => Capacity,
                    Add_Named          => Insert),

Add after A.18.23(5.2/5):

The aspect_definition for aspect Aggregate for type Set is changed to:

   Aggregate     => (Empty              => Empty,
                     Count_Discriminant => Capacity,
                     Add_Unnamed        => Include),

Add after A.18.24(5.2/5):

The aspect_definition for aspect Aggregate for type Set is changed to:

   Aggregate     => (Empty              => Empty,
                     Count_Discriminant => Capacity,
                     Add_Unnamed        => Include),

!discussion

One has a similar problem with functions that return bounded containers (such as Empty when a capacity is not given). We do not try to propose a solution for those, as we do not want “magic” behavior for ordinary function calls.

Constants defined in the container packages, such as Empty_Vector, also have this problem. Indeed, Empty_Vector is a worse problem as neither A.18.2 nor A.18.9 define the capacity of the constant - it is left unspecified. As such, the constant can never be used in a portable assignment without using either the Copy or Assign subprograms (and thus should never be used at all, using the Empty function with a Capacity parameter instead). We do not propose to fix this problem as an empty aggregate is more convenient than any of these other alternatives (assuming the proposed fix is applied).

Specifically (using the declarations in the !issue):

   A := [];            -- (11)
   A := BVI.Empty;     -- (12)
   A := BVI.Empty_Vector;  -- (13)
   BVI.Assign (Target => A, Source => []); -- (14)
   A := BVI.Empty (Capacity => 100);       -- (15)
   A := BVI.Copy (Source => [], Capacity => 100); -- (16)

 

(11) will always raise Constraint_Error with the current rules, but it will work with the proposed fixes. (12) probably will raise Constraint_Error, as the capacity of the result is implementation defined. (13) also probably will raise Constraint_Error, as the capacity is unspecified. Neither (12) nor (13) can be used safely in portable code.

(14), (15), and (16) all work currently and will continue to work. But these are all much less readable and intuitive.

Even if we did specify the capacity to be used in cases (12) and (13), it is highly probable that Constraint_Error would be raised, as it is unlikely that the choice made would match the capacity being used. So there doesn't seem to be much value to making these operations completely portable.


 

This solution is technically inconsistent. If there is code that depends on Constraint_Error being raised for aggregates in some circumstances, that code might fail after the proposed fix is applied. Either the code will work as expected, or perhaps Capacity_Error would be raised instead. The latter case might be a bit unexpected:

          A3 : BVI.Vector(1) := [1, 2]; -- (20)

         

(20) will raise Constraint_Error with the current rules (since the capacity of 2 for the aggregate does not match the capacity of 1 for the object). But with the proposed rules, it will raise Capacity_Error when the aggregate is constructed, since the temporary object for the aggregate will have capacity of 1, and that will mean the aggregate is full when the second value is added, raising Capacity_Error.

These changes are very unlikely to break any code, and it is much more likely to fix bugs in code, along with enhancing readability.

The solution also is very slightly incompatible for all of the versions of the language-defined Vectors container. The routine New_Vector is altered to have a Container parameter. That parameter has a default, so calls remain fine, but uses like passing to formal subprograms, creating an access-to-subprogram value, and renaming will fail because of the extra parameter. As the purpose of this routine is solely to support the creation of aggregates, it is unlikely to be used directly and even more unlikely to be used in one of the problematic uses. The addition of a new overloading of To_Vector is also incompatible for the usual “new declaration” issues, which are also very unlikely.


 

This solution does not interfere with any clever uses of the extra parameter to Empty. While we intended the parameter to be used to set the capacity of the aggregate, it’s possible that some users have (or will) find other uses for that parameter. We avoid causing issues with those uses by making this new mechanism be triggered by the Count_Discriminant choice in the Aggregate aspect. If that is not given, the current Ada 2022 semantics will be used for the aggregate creation (even if the container type has discriminants). Similarly, if the context does not provide an applicable discriminant constraint, then the Ada 2022 semantics will be used to set the capacity.


 

This solution does not attempt to provide a solution for container types with multiple discriminants. For instance, the Hashed_Map type has both Capacity and Modulus discriminant. The Modulus discriminant is usually chosen by a call to Default_Modulus given the Capacity value. If the user sticks to that throughout their program, this solution will work perfectly. OTOH, if they choose their own Modulus values, the mismatch problem will reappear.

One could imagine extending this solution to allow the parameter list on Empty to match the discriminants on the container type. Additionally, there would be a mechanism to map a function to each discriminant, each taking the estimated number of elements in the aggregate. When there is an applicable constraint, it would be used to get values for all of the parameters to Empty. When there is not such a constraint, the appropriate function would be called to provide the appropriate answer to be passed to Empty.

We didn’t pursue this idea as it is substantially more complex than the proposal, and would only be helpful for a few kinds of language-defined bounded containers (Bounded_Hashed_Maps and Bounded_Hashed_Sets).

!ACATS test

ACATS C-Tests will be needed to check that the context is properly used for bounded container aggregates in constrained contexts. Existing ACATS C-Tests should be checked to see if any corrections are needed.

!appendix

This issue was originally identified in Github Issue #112 (https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/112).


 

This proposal provides an alternative solution to the problem posed in Github Issue #110

(https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/110). In particular, since the capacity of bounded container aggregates comes from context in many cases, the fact that it is not calculated for iterators becomes irrelevant in those cases. Indeed, the capacity associated with the context is better, since if the iterator generates too many elements, the resulting aggregate won’t be useful anyway.