Open sttaft opened 1 month ago
Rather than adding the "Bounded" indicator, the decision to use the two-pass algorithm could be based simply on whether the type has discriminants without defaults. If there are defaults, it would be important that the anonymous object does not take its discriminants from the result of calling the Empty function, but rather that the anonymous object is created as an unconstrained object. It would also be reliant on Add_Unnamed being smart enough to change the discriminants as a side-effect, presuming 'Constrained is False for the (in-out) Container parameter.
For efficiency, one could argue that the two-pass algorithm should be used any time there are discriminants, even if they have defaults, to avoid creating an unconstrained anonymous object for the aggregate, which could be significantly bigger than a properly-sized constrained anonymous object, in implementations that use the "max size" approach for unconstrained objects with defaulted discriminants.
Since deciding whether to use the two-pass algorithm is part of dynamic semantics, there should be no issue of privacy breaking even if the discriminants are not visible outside the defining package.
A bunch of us (including you) identified and discussed this problem and a number of others associated with container aggregates for bounded containers back in February 2023. This was one of many issues raised privately that are sitting in an e-mail folder to have some AI or Issue created when I get time. (That was supposed to happen in September, but as many of you know, life intruded.)
Container aggregates don't work very well with bounded containers as they currently are designed. Something as simple as:
A := [1, 2];
will almost always raise Constraint_Error, as the capacity discriminants don't match. You can work around this problem with constructs like:
Assign (Target => A, Source => [1, 2]); or
A := Copy ([1, 2], @.Capacity);
but it should be obvious that these are far less natural than the initial expression.
It seems to me that it is necessary to get the capacity for an aggregate from the context (just like we do with array bounds), and then natural expressions like the first one above always work. I've written this up in Issue #112 .
I don't have any real objection to using a two pass algorithm, but it seems barely necessary to me. If we've fixed the underlying usability problem with bounded container aggregates with a solution similar to the one proposed in Issue #112 , then we hardly need to fix the iterator one at all. If the iterator is in an assignment statement or a constrained context, then the capacity will be the correct one for the usage. The only time an exception would be raised is if there are more items than allowed by the target, and we shouldn't care about that (it's failing in either case). [The solution is slightly inconsistent, but that seems unimportant; this is discusssed in the other Issue.]
If the bounded aggregate using an iterator is in an unconstrained context, then the compiler ought to give a warning that the aggregate won't work. But the fix is simple: qualify the aggregate with a constrained subtype giving the needed capacity. In a lot of uses, we'd prefer to use that defined capacity anyway.
Given that both of these solutions are a lot of implementation work, and the capacity from context solution fixes a lot more problems, I think we should do that one first, and only resort to the two-pass algorithm if users still are running into issues often.
Randy.
There is an annoying non-portability when using a container aggregate with a bounded container, when the aggregate is of the form:
X : Bounded_Vec := [for E of My_Container => E];
This is created by calling the Empty function with an implementation-defined Capacity followed by a call on Add_Unnamed for each element produced by the "for ... of ..." clause.
Bounded containers are quite useful in safety-critical environments, and container aggregates are quite useful in general. The question is do they play well together. For positional aggregates (see RM 4.3.5(33/5,37/5)) there is no problem coming up with the correct size. For indexed aggregates (see RM 4.3.5(25/5)), they also work well together, because the call on New_Indexed can provide the low and high bound, so the bounded container can be properly sized.
On the other hand, for a non-indexed aggregate, where Empty and Add_Unnamed are used to build up the aggregate (as illustrated above), the implementation must choose the Capacity to pass to the Empty function, and if the number of components is determined by one or more iterated_element_associations that use iterator_specifications (see RM 4.3.5(21/5)), the rules say that an "implemented-defined value" is used (see RM 4.3.5(40/5)). That is merely a performance issue for unbounded containers, but for bounded containers, that value can determine whether or not the aggregate can be constructed without hitting a Capacity_Error. That means that a program with such an aggregate for a bounded container is not generally portable across implementations.
This nonportability seems like it should be fixed. The most natural answer would seem to carry over the two-pass rule used for array aggregates if the container type is bounded. That would argue for having an additional Boolean aspect Bounded, or a perhaps an additional (optional) Boolean element of the existing Aggregate aspect, which when True, would mean that the two-pass array-aggregate mechanism is used for determining the correct size for the Capacity to be passed to the Empty function used to create the anonymous result object for the aggregate.