AI22-0135-1
!standard 4.8(5.5/3) 25-05-23 AI22-0135-1/02
!class Binding Interpretation 25-05-21
!status work item 25-05-21
!status received 25-02-28
!assigned author Tucker Taft and Steve Baird
!submitter Steve Baird
!priority Medium
!difficulty Medium
!qualifier Clarification
!subject Coextensions, Function Calls, and Storage Pools
If a function call defines the value of an access discriminant, whether or not it is a coextension is determined by the storage pool in which the designated object is allocated.
The rules for determining whether the object created by an allocator is a coextension are unclear.
The definition of "coextension" in RM 3.10.2(14.4) refers back to
3.10.2(14.1), which says:
"for an allocator used to define the discriminant of an object".
It is not clear what the phrase "used to define the discriminant of an object" means in some cases. In particular, if a function returns an allocator of an anonymous access type, and a call on such a function is used to define the initial value of an access discriminant, is the designated object a coextension? (Yes)
If an object might have a coextension, then there should be a special access collection associated with this enclosing object to hold its coextensions, if any. This storage pool would be included in whatever finalization list or equivalent that includes this enclosing object. If an access discriminant is initialized by calling a function with an anonymous access type result (aka "access result"), this storage pool would be passed in to indicate what storage pool to use if the function returns an allocator. Any further coextensions of a coextension would use the special storage pool of the outermost enclosing object.
This approach of using a special access collection eliminates the need for having any sort of run-time flags indicating which access discriminants, if any, designate coextensions. This access collection contains all of the coextensions, if any, for the object "tree" rooted at the outermost object, and all objects within this access collection are finalized along with the root object.[a]
Modify 4.8(5.5/3):
If the designated type of the type of the allocator is limited, then the allocator shall not be {directly} used to define the value of an access discriminant, unless the discriminated type is immutably limited (see 7.5).{ Similarly, a call on a function with an access result with a limited designated type shall not be directly used to define the value of such an access discriminant.}
Modify 4.8(5.f/3):
Reason: [Because coextensions work very much like parts, we don't want users creating limited coextensions for nonlimited types. This would be similar to extending a nonlimited type with a limited component. We check this on the allocator.]{This means that a function returning a nonlimited class-wide object will never "carry along" a limited coextension, which might contain a task needing the complexities of task activation and waiting.}[b][c][d] Note that there is an asymmetry in what types are considered limited; this is required to preserve privacy. We have to assume that the designated type might be limited as soon as we see a limited partial view, but we want to ensure that the containing object is of a type that is always limited.{ Note that we assume the worst about a function with an access result, namely that it is directly returning an allocator using the storage pool provided by the caller.}
Coextensions have existed since Ada 95, because access discriminants were defined then, as a way of allowing "type parameters" that were more "interesting" than just discrete values, and allocators were allowed as a way to define the value of such a discriminant. Alas, it wasn't until Ada 2005 that we decided more attention was needed to define the lifetime-related issues of such allocators. The Ada 95 rule from 13.11 stated the following:
A storage pool for an anonymous access type should be created at the point of an
allocator for the type, and be reclaimed when the designated object becomes inaccessible.
AARM Implementation Note: Normally the "storage pool" for an anonymous access type would
not exist as a separate entity. Instead, the designated object of the allocator would be allocated,
in the case of an access parameter, as a local aliased variable at the call site, and in the case
of an access discriminant, contiguous with the object containing the discriminant. ...
This notion of a special storage pool is about as close to an implementation model as was ever provided for allocators of an anonymous access type, that are defining the value of an access discriminant. In this AI we talk about a special access collection, which is often one-to-one with a storage pool, though in some cases multiple access collections might share a single storage pool.
In Ada 2005 (and a bit of Ada 2012), 13.11(25) was expanded further, as follows:
The storage pool used for an allocator of an anonymous access type should be determined as follows:
(25.1.1/2) Implementation Advice: Usually, a storage pool for an access
discriminant or access parameter should be created at the point of an allocator, and be reclaimed
when the designated object becomes inaccessible. For other anonymous access types, the pool should
be created at the point where the type is elaborated and may have no mechanism for the deallocation
of individual objects.
(25.a/2) Implementation Note: {AI95-00230-01} For access parameters and access discriminants, the
"storage pool" for an anonymous access type would not normally exist as a separate
entity. Instead, the designated object of the allocator would be allocated, in the case of an
access parameter, as a local aliased variable at the call site, and in the case of an access
discriminant, contiguous with the object containing the discriminant. This is similar to the way
storage for aggregates is typically managed.
One important addition in Ada 2012 worth highlighting is this sentence from 25.3/3:
If the allocator defines the result of a function with an access result, the storage pool is determined as though the allocator were in place of the call of the function. ...
This seems to pretty directly address this part of the original ARG GitHub issue:
"There have been suggestions that perhaps if a call to a function with an anonymous access result type is used as a discriminant value in an object declaration, and if the function returns an allocator, then that allocated object is a coextension."
More generally, we see that this rather special storage pool[e] is part of the semantics of an allocator for an access discriminant (or an access parameter). Given that, we should focus on the properties of this special storage pool, or more correctly, this special access collection, and try to tie as many of the coextension semantics as possible to those of this access collection.[f][g]
If we presume that upon creation of such a special access collection we identify it as an access collection for any coextensions that might be created, this avoids having to have per-access-discriminant flags indicating that they are pointing at a coextension.[h] Instead, we simply make sure that the storage for coextensions is allocated out of the storage pool for this access collection, and then we only need to be able to find this special access collection when we deallocate a "root" object which might have coextensions, and perform finalization of all of the objects in this access collection, and reclaim their storage, which might be the entire associated storage pool. It is not unusual for heap objects to have a bit of extra storage if they have any finalization actions, so this means that a heap object with access discriminants would need room for a pointer to such a special access collection, which could be null if no coextensions were ever allocated.
Now a remaining question is whether the legality rule pertaining to coextensions identified above (4.8(5.5)) can be handled with this special access collection approach. Here is the wording of that legality rule:
(5.5/3) {AI05-0052-1} If the designated type of the type of the
allocator is limited, then the allocator shall
not be used to define the value of an access discriminant, unless the discriminated type is
immutably limited (see 7.5).
(5.f/3) Reason: Because coextensions work very much like parts, we don't want users creating
limited coextensions for nonlimited types. This would be similar to extending a nonlimited type
with a limited component. We check this on the allocator. Note that there is an asymmetry in what
types are considered limited; this is required to preserve privacy. We have to assume that the
designated type might be limited as soon as we see a limited partial view, but we want to ensure
that the containing object is of a type that is always limited.
This could pose a challenge for the 13.11(25.3/3) rule that says that a call on a function returning an allocator of an anonymous type is treated as though the allocator were at the point of call. This would arise if the function returns an access result whose designated type is limited, and its result is being used to initialize an access discriminant of a type that turns out to be nonlimited in some view. The danger is that the root object, being nonlimited, might be copied, which would produce two separate objects both presuming they "own" the same coextension, which could cause havoc when one of them is finalized and reclaimed.
There seem to be at least two solutions to this problem:
Note there is no change to the rules when an allocator is used directly to initialize the discriminant. Both suggested rules only apply if a call on a function returning an access result is used instead of an allocator. In this special case, solution (1) above is not strictly compatible, but can be enforced as a legality rule, whereas solution (2) moves the check to runtime for this special case. Note that if we were to say that such a function call special case does not produce a coextension, then it is always producing a storage leak, which does not seem like the right solution either.
For this AI, we have gone with solution (1), as it minimizes implementation burden, and it is relatively easy to workaround the problem by simply inserting an explicit conversion on the function call since the programmer presumably knows the function is not actually doing an anonymous allocator internally.
** TBD **[i]
In an ACATS test, coextensions that need finalization can be used to test whether finalization happens at the correct time. Testing whether storage is properly reclaimed could be tested by providing a user-defined storage pool for the outermost object.
This AI is based on ARG GitHub issue #127.
[a]Replacing single bits with a complex data structure only used for this coextension case does not seem to be a sensible trade-off. At best, it might make sense in some implementation model, but to make a claim that is somehow clearly better is nonsense.
[b]I don't understand the reasoning for the change here. This rewording seems to obscure the reason for the rule, concentrating on a single reason rather than the big picture - a coextension behaves like a "part" (maybe "component" would have been better), and we do not allow limited components in nonlimited types (for a variety of good reasons). The function case given is simply one of many such issues caused by limited components in nonlimited objects.
[c]I removed the mention of "parts" because the term brings in a lot of other semantics. For example an assignment affects all of the parts, whereas that would not apply to coextensions. So I tried to identify the critical issue, which I agree is related to the concern about "parts" but is focused on what matters for this particular rule.
[d]I believe I avoided this single case description because it needs a much more lengthy description than you have here. (Especially to understand why anyone would care about this special case.)
In particular, one has to mention that otherwise we would have a massive distributed overhead, because any nonlimited tagged type could be extended with an access discriminant that designates a limited type. The odds of anyone wanting to do that are very low, but the overhead of task termination would be needed anyway. The one sentence you have doesn't explain any of that, and it took me quite a bit of time thinking about it this morning to remember all of that.
Moreover, your replacement sentence provides no understanding at all as to why this is prohibited for untagged types (but the rule clearly does prohibit such coextensions for any nonlimited type). Even if my original discussion is intentionally vague about the issues, it definitely makes it clear that it is undesirable for any type.
Ergo, I remain convinced that my original description here is better. Perhaps it could be improved in some way (perhaps mentioning tasks?), but replacing a general explanation about a general rule with a poorly defined special case is unlikely to be helpful to anyone.
[e]I note that none of the above has anything to do with finalization.
[f]This therefore does not follow, as tying finalization to a storage pool inverts the normal organization of storage management.
[g]I should have called it a "special access collection" to tie it to finalization. As you point out, the storage pool might have been specified by the user, but the notion of an access collection (RM 7.6.1(11.1/3) is what is relevant for finalization.
[h]This implies "subpool" like properties for this pool, and implies a far more complex implementation than the bit-per-discriminant implementation that has previously been used. Thus, this interpretation is requiring a very complex implementation that may require massive compiler changes for what is best described as a pathology (an allocator in a function returning an anonymous access being used to create a coextension). This is a bridge too far for me. This question should be declared a pathology and nothing more should be said.
[i]This had the original template text here, which I've deleted. Some example would seem appropriate.