AI22-0026-1
!standard 4.6(24.13/2) 24-03-20 AI22-0026-1/06
!standard 4.8(10.1/3)
!class binding interpretation 22-01-21
!status Amendment 1-2022 23-10-05
!status WG9 Approved 24-06-10
!status ARG Approved 11-0-1 23-10-05
!status work item 22-01-21
!status received 22-01-21
!submitter Tucker Taft
!priority Low
!difficulty Easy
!qualifier Omission
!subject Problem with nested type extension check
An allocator for an anonymous access return type which designates a tagged type needs to make an accessibility check (at run-time) to ensure that the designated object's type lives at least as long as the result type. In addition, a compile-time check is added to disallow certain access type conversions that would otherwise be guaranteed to fail a run-time check whenever the value being converted is non-null.
The following test case which illustrates a way that you can, by conversion, create an access-to-class-wide value that designates an object of a tag is shorter-lived than the access-to-class-wide type:
type CCAccess is access all
C'Class;
function Nasty(N : Integer) return CCAccess is
type D is new C with null
record;
function NastyAllocator return access D is
begin
return new D'(null record);
end NastyAllocator;
begin
-- Create an access-to-C'Class value that
-- designates an object of type D, by conversion
-- of function result.
return CCAccess(NastyAllocator);
end Nasty;
A0 : CCAccess := Nasty(1);
Should this hole be plugged? (Yes.)
In the general case, plugging this hole requires a dynamic check. However, this particular example also demonstrates the need for an additional static (legality) check. In the case of a type conversion to a general access-to-class-wide type, if the accessibility level of the designated
type of operand type is statically deeper than that of the target type (as in the above example) then should the type conversion be disallowed? (Yes.)
(See Summary.)
Add after 4.6(24.13/2) (as another bulleted list item):
AARM Note: If the tagged type of a designated object does not live long enough for the access type, then necessarily the object also does not live long enough.
Modify 4.8(10.1/3):
For any allocator, if the designated type of the type of the allocator is class-wide, then a check is made that the master of the type determined by the subtype_indication, or by the tag of the value of the qualified_expression, includes the elaboration of the type of the allocator. {Similarly, for an allocator that defines the result of a function with an anonymous access-to-tagged-type result, a check is made that the master of the type determined by the subtype_indication, or by the tag of the value of the qualified_expression, includes the master of the object created by the allocator (see 3.10.2).} If any part of the subtype determined by the subtype_indication or qualified_expression of the allocator (or by the tag of the value if the type of the qualified_expression is class-wide) has one or more access discriminants, then a check is made that the accessibility level of the anonymous access type of each access discriminant is not deeper than that of the type of the allocator. Program_Error is raised if any of these checks fail.
This problem can also happen for an anonymous access to class-wide type. For instance, this same problem can happen if in the example, NastyAllocator has a result type of "access C'Class". The 4.8 wording attempts to cover both.
To see that the new compile-time check does not eliminate the need for the new run-time check, consider the following variation on the original example where we introduce a wrapper function with an anonymous result type:
type CCAccess is access all
C'Class;
function Nasty(N : Integer) return CCAccess is
type D is new C with null
record;
function NastyAllocator return access D is
begin
return new D'(null record);
end NastyAllocator;
function NA_Wrapper return access all C'Class
is
begin
return NastyAllocator;
end NA_Wrapper;
begin
-- Create an access-to-C'Class value that
-- designates an object of type D, by conversion
-- of function result.
return CCAccess(NA_Wrapper);
end Nasty;
A0 : CCAccess := Nasty(1);
The new compile-time check is not as restrictive as it could be in the case where the target type of the conversion is the anonymous access type of a stand-alone object. We could also reject (at compile-time) this variation on the above example
Obj : access all C'Class := Nasty(1);
but catching this particular problem statically as opposed to dynamically does not seem to be worth the extra complexity. If we did want to catch this corner case statically, then we could replace the proposed 4.6 wording change with something like
Add at the end of 4.6(24.17/4):
If the designated type of the target type is class-wide, then these requirements for the accessibility level of the operand type also apply to the accessibility level of the designated type of the operand type.
This wording seems less readable than the proposed 4.6 change.
(See issue and discussion.)
@dinsa
@xbullet{If the target designated type is tagged, then the operand designated type shall be convertible to the target designated type;}
@dinst
@xbullet{If the target designated type is class-wide, then the accessibility level of the operand designated (tagged) type shall not be statically deeper than the accessibility level of the target (access) type.}
@drepl
For any @fa{allocator}, if the designated type of the type of the @fa{allocator} is class-wide, then a check is made that the master of the type determined by the @fa{subtype_indication}, or by the tag of the value of the @fa{qualified_expression}, includes the elaboration of the type of the @fa{allocator}. If any part of the subtype determined by the @fa{subtype_indication} or @fa{qualified_expression} of the @fa{allocator} (or by the tag of the value if the type of the @fa{qualified_expression} is class-wide) has one or more access discriminants, then a check is made that the accessibility level of the anonymous access type of each access discriminant is not deeper than that of the type of the allocator. Program_Error is raised if either such check fails.
@dby
For any @fa{allocator}, if the designated type of the type of the @fa{allocator} is class-wide, then a check is made that the master of the type determined by the @fa{subtype_indication}, or by the tag of the value of the @fa{qualified_expression}, includes the elaboration of the type of the @fa{allocator}. Similarly, for an @fa{allocator} that defines the result of a function with an anonymous access-to-tagged-type result, a check is made that the master of the type determined by the @fa{subtype_indication}, or by the tag of the value of the @fa{qualified_expression}, includes the master of the object created by the @fa{allocator} (see @ref{3.10.2}). If any part of the subtype determined by the @fa{subtype_indication} or @fa{qualified_expression} of the @fa{allocator} (or by the tag of the value if the type of the @fa{qualified_expression} is class-wide) has one or more access discriminants, then a check is made that the accessibility level of the anonymous access type of each access discriminant is not deeper than that of the type of the @fa{allocator}. Program_Error is raised if any of these checks fail.
An ACATS C-Test should be created to check examples like the one in the !issue.
From: Tucker Taft
Sent: Friday, June 25, 2021 8:28 AM
Subject: Hole in check that global access-to-class-wide doesn't designate
a nested type extension
An AdaCore engineer (the same one who noticed the confusion with "innermost
master of the call") constructed the following test case which illustrates a
way that you can, by conversion, create an access-to-class-wide value that
designates an object of a tag is shorter-lived than the access-to-class-wide
type:
type CCAccess is access all C'Class;
function Nasty(N : Integer) return CCAccess is
type D is new C with null record;
function NastyAllocator return access D is
begin
return new D'(null record);
end NastyAllocator;
begin
-- Create an access-to-C'Class value that
-- designates an object of type D, by conversion
-- of function result.
return CCAccess(NastyAllocator);
end Nasty;
A0 : CCAccess := Nasty(1);
One way to fix this is to make the following modification to 4.8(10.1/3):
For any allocator, if the designated type of the type of the allocator is
class-wide, then a check is made that the master of the type determined by
the subtype_indication, or by the tag of the value of the
qualified_expression, includes the elaboration of the type of the allocator.
{Similarly, for an anonymous allocator that defines the result of a function
with an access-to-specific-tagged-type result, a check is made that the
master of the type determined by the subtype_indication, or by the tag of
the value of the qualified_expression, includes the master of the object
created by the allocator (see 3.10.2).} ...
From: Tucker Taft
Sent: Friday, June 25, 2021 8:59 AM
Actually, this same thing can happen if in the example, NastyAllocator has a
result type of "access C'Class" so the change should be more general,
handling any anonymous access-to-tagged type result:
For any allocator, if the designated type of the type of the allocator is
class-wide, then a check is made that the master of the type determined by
the subtype_indication, or by the tag of the value of the
qualified_expression, includes the elaboration of the type of the allocator.
{Similarly, for an anonymous allocator that defines the result of a function
with an anonymous access-to-tagged-type result, a check is made that the
master of the type determined by the subtype_indication, or by the tag of
the value of the qualified_expression, includes the master of the object
created by the allocator (see 3.10.2).} ...
From: Richard Wai
Sent: Saturday, June 26, 2021 9:57 AM
I’m struggling to see what I’m missing here...
To my eyes, 4.6-24.17/4 (type conversions) makes this situation illegal:
"If the target type is a general access-to-object type, then the operand type
shall be universal_access or an access-to-object type. Further, if the operand
type is not universal_access:"
...
"The accessibility level of the operand type shall not be statically deeper
than that of the target type, unless the target type is an anonymous access
type of a stand-alone object. If the target type is that of such a stand-alone
object, the accessibility level of the operand type shall not be statically
deeper than that of the declaration of the stand-alone object."
The target type is statically deeper than the operand type, and the target
type is not an anonymous access type, so this seems to violate that rule.
I note that GCC-10.3.0 refuses to compile the given example, and apparently
for that reason.
From: Tucker Taft
Sent: Saturday, June 26, 2021 12:01 PM
...
> The target type is statically deeper than the operand type, and the target
> type is not an anonymous access type, so this seems to violate that rule.
I presume you meant to say "The operand type is statically deeper than the
target type, ..."
But in fact, in this odd case, the accessibility level of the operand type is
"passed in" from above as a result of the rules about what is the "master of
the function call" which, via 3.10.2(10.3/5) inherits the rules from
allocators (3.10.3(14/3)) which says:
... For an anonymous allocator that defines the result of a function with an
access result, the accessibility level is determined as though the allocator
were in place of the call of the function; in the special case of a call that
is the operand of a type conversion, the level is that of the target access
type of the conversion. ...
This is exactly the situation we are in, so the accessibility level of the
allocator comes from the target type of the conversion (yes, it is effectively
a self-fulfilling prophecy so that the levels will match).
> I note that GCC-10.3.0 refuses to compile the given example, and apparently
> for that reason.
Accessibility associated with anonymous access types is an area where I know
GNAT has had some issues, so its rejection does not imply this new rule is
unneeded.
From: Claire Dross
Sent: Monday, June 28, 2021 3:23 AM
> An AdaCore engineer (the same one who noticed the confusion with "innermost
> master of the call") constructed the following test case which illustrates
> a way that you can, by conversion, create an access-to-class-wide value that
> designates an object of a tag is shorter-lived than the access-to-class-wide
> type:
Martin is a researcher in verification of programs who joined AdaCore for a
postdoctoral contract. His aim is to formalize the soundness of (a part of)
the SPARK language. You might hear from him again, as he is currently very
seriously trying to understand the Ada rules with respect to accessibility rules.
From: Richard Wai
Sent: Monday, June 28, 2021 10:52 AM
I’ve always felt a little nervous about the special rules for returning access
values from functions..
But GNAT itself has a bunch of concerning traits in this space anyways, so
often the rules don’t really “apply” anyways. It’s fairly common to cause a
segfault if you’re a fan of interface types.
From: Randy Brukardt (ARG Editor)
Sent: Saturday, March 2, 2024
The following are my editorial review changes on AI22-0026-1:
4.8(10.1/3) ends with “Program_Error is raised if either such check fails”. But, with the new rule, we have three checks defined in this paragraph. It is unusual to use “either” with more than 2 of anything. So I replaced this with:
Program_Error is raised if any of these checks fail.
AARM Note became AARM Reason, as we ought to identify the type of AARM note we are inserting. (Leaving the editor to guess is not fun and could lead to mistakes.)
The AARM Note includes a possessive on an inanimate object. In order to avoid the “wrath of John”, we do not use those in the AARM. Also, the term “short-lived” isn’t defined in the AARM (it is used in two other places); I think it is best to avoid it. Thus, I reworded the entire note:
If the tagged type of a designated object does not live long enough for the access type, then necessarily the object also does not live long enough.