AI22-0076-1
!standard 3.10.2(12/2) 25-07-25 AI22-0076-1/03
!standard 4.6(48/3)
!class Amendment 23-06-05
!status No Action 9-0-0 26-02-17
!status work item 23-06-05
!status received 23-06-05
!assigned author Steve Baird and Tucker Taft
!priority Medium
!difficulty Medium
!subject Restricting Dynamic Accessibility Checks
We introduce the Restriction No_Dynamic_Accessibility_Checks which restricts features of Ada that lead to run-time checks on accessibility levels of access types. We do not propose here any restrictions on nested extensions of tagged types, which can require run-time checks on the accessibility of tags (tagged type descriptors).
Over the years, the complexity of the rules that govern accessibility in Ada, that is, what operations on pointers are allowed, has grown to a point where the rules are not fully understood by implementers and by users. In particular, a failure of a dynamic accessibility check is often difficult to debug and diagnose.
We propose a set of restrictions that attempt to eliminate dynamic accessibility checks on access types, while being compatible with, or requiring modest updates to, many existing Ada programs that make use of pointers.
The complexity of the rules that govern accessibility in Ada rest largely on the use of anonymous access types. These were [originally introduced in Ada 95] (https://www.adaic.org/resources/add_content/standards/95rat/rat95html/rat95-p2-3.html)
for access parameters and access discriminants, then [used more widely in Ada 2005] (https://www.adaic.org/resources/add_content/standards/05rat/html/Rat-3-3.html),
and [further refined in Ada 2012] (http://www.ada-auth.org/standards/12rat/html/Rat12-6-4.html) to reduce the likelihood of certain accessibility check failures.
We propose a set of restrictions that avoid run-time accessibility checks on access types. These can be imposed by using the restriction No_Dynamic_Accessibility_Checks with the Restriction pragma.
We propose to distinguish the various different uses of anonymous access types.
Var : access T := ...
Var_To_Cst : access constant T := ...
Cst : constant access T := ...
Cst_To_Cst : constant access constant T := ...
We propose to define the accessibility levels of standalone objects of anonymous access type (whether constants or variables) to be that of the level of the type of their initial value (if an explicit initial value is given; otherwise, the level of their object declaration). This has the feature of allowing many common use-cases without the employment of Unchecked_Access, while still removing the need for dynamic checks.
The major benefit of this change would be compatibility with standard Ada rules.
For example, the following would be legal:
type T is null record;
type T_Ptr is access all T;
Obj : aliased T;
Anon : access T := Obj’Access;
Named : T_Ptr := T_Ptr (Anon); -- Allowed
procedure P (V : access T; X : access constant T);
We propose the following in terms of subprogram parameters:
When the type of a formal parameter is of anonymous access then, from the caller's perspective, its level is seen to be at least as deep as that of the type of the corresponding actual parameter (whatever that actual parameter might be) - meaning any actual can be used for an anonymous access parameter without the use of 'Unchecked_Access.
declare
procedure Foo (Param : access Integer) is ...
X : aliased Integer;
begin
Foo (X'Access); -- Still allowed
Foo (X'Unchecked_Access); -- Not necessary (though legal)
end;
From the callee's perspective, the level of anonymous access formal parameters would be
between the level of the subprogram and the level of the subprogram's locals. This has the effect of formal parameters being treated as local to the callee except in:
Note that with these more restricted rules we lose track of accessibility levels when assigned to local objects after their initialization, thus making (in the example below) the assignment to Node2.Link from Temp below compile-time illegal.
type Node is record
Data : Integer;
Link : access Node;
end record;
procedure Swap_Links (Node1, Node2 : in out Node) is
Temp : access Node;
Begin
Temp := Node1.Link; – assignment does not change level of type of Temp
Node1.Link := Node2.Link; -- Allowed
Node2.Link := Temp; -- Not allowed
end;
function Identity (N : access Node) return access Node is
Local : constant access Node := N;
begin
if True then
return N; -- Allowed
else
return Local; -- Not allowed
end if;
end;
function Get (X : Rec) return access T;
We propose making the accessibility level of the result of a call to a function that has an anonymous access result type defined to be as whatever is deepest out of the following:
NOTE: We would need to include an additional item in the list if we were not to enforce the below restriction on tagged types:
Function result example:
declare
type T is record
Comp : aliased Integer;
end record;
function Identity (Param : access Integer) return access Integer
is
begin
return Param; -- Legal
end;
function Identity_2 (Param : aliased in out Integer)
return access Integer is
begin
return Param'Access; -- Legal
end;
X : access Integer;
begin
X := Identity (X); -- Legal
declare
Y : access Integer;
Z : aliased Integer;
begin
X := Identity (Y); -- Illegal since Y is too deep
X := Identity_2 (Z); -- Illegal since Z is too deep
end;
end;
However, an additional restriction that falls out of the above logic is that tagged type extensions *cannot* allow additional anonymous access discriminants in order to prevent upward conversions potentially making such "hidden" anonymous access discriminants visible and prone to memory leaks.
Here is an example of one such case of an upward conversion which would lead to a memory leak:
|
declare |
In order to prevent upward conversions of anonymous function results (like below), we
also would need to ensure that the level of such a result (from the callee's perspective)
is statically deeper:
declare
type Ref is access all Integer;
Ptr : Ref;
function Foo (Param : access Integer) return access Integer
is
begin
return Result : access Integer := Param; do
Ptr := Ref (Result); -- Not allowed
end return;
end;
begin
declare
Local : aliased Integer;
begin
Foo (Local'Access).all := 123;
end;
end;
Access discriminants were introduced in Ada 95 as a way to tie accessibility levels to the scope where the object is declared, in order to allow code that would be otherwise illegal.
Any access discriminant will have the accessibility level of its enclosing object, identical to the standard Ada model therefore ensuring maximum compatibility. However, in the presence of the restriction No_Dynamic_Accessiblity_Checks, such access discriminants cannot be initialized directly via an allocator - eliminating the notion of coextensions. They can be initialized by an allocator so long as it is qualified to be of a named access type, or is passed through a local constant with a well-defined (static) accessibility level.
procedure M is
type T (X : access Integer) is null record;
Disc : access Integer := new Integer'(1);
Obj_1 : T (new Integer'(1)); -- Illegal
Obj_2 : T (Disc); -- Legal
begin
null;
end;
Membership tests that would normally include a dynamic accessibility level test (see RM 4.5.2(30.3)) are disallowed by this restriction.
**TBD
(See !recommendation..)
(See !recommendation.)
**TBD
[This is based on the RFC from https://github.com/AdaCore/ada-spark-rfcs/blob/master/prototyped/rfc-simpler-accessibility.md?plain=1]
[a]Why do we need the type conversion here? Param is already of this type.
Shouldn't this example be illegal already with the current rules?
[b]Agreed, the conversion is unnecessary.
This currently involves a run-time check against the master of the function call, based on 6.5(21/3) and 3.10.2(10.3/5), and should raise Program_Error.
I am unconvinced we need these extra tagged type rules. @baird@adacore.com and I will need to discuss further.