Version 1.1 of ai05s/ai05-0142-2.txt

Unformatted version of ai05s/ai05-0142-2.txt version 1.1
Other versions for file ai05s/ai05-0142-2.txt

!standard 3.10(6)          09-04-03 AI05-0142-2/01
!standard 3.10(12/2)
!standard 3.10.2(13.1/2)
!standard 3.10.2(18.1/2)
!class Amendment 09-04-04
!status work item 09-04-04
!status received 09-03-19
!priority Medium
!difficulty Medium
!subject Limited access types
(See proposal.)
Modifying a portion of a larger opaque object (such as a container) is not well-supported in Ada. The Ada.Containers packages provide a procedure Replace_Element for this purpose. But this procedure requires copying the element (potentially in both directions). That could be very expensive if the element is large.
The Ada.Containers packages also provide an procedure Update_Element for this purpose; this provides a writable object as a parameter to a subprogram passed into the procedure. This procedure avoids the need to copy the element, but it is hardly convenient to define a procedure for every component of the element that needs changing. The extra syntax needed obscures the real meaning of the program.
An option that was rejected for the Ada.Containers packages was to return an access to the element type. However, this is problematic as it is difficult to control the accessibility and lifetime of the returned access. If the element is removed from the container, the returned access could become dangling; continued use of the access would make the program erroneous. Moreover, the accessibility of the returned object (and thus what could be done with it) would depend on the actual implementation of the container. Bounded containers would typically only return access values with a very short lifetime, while unbounded containers would typically return access values with a much longer lifetime. Converting from an unbounded to bounded form could thus introduce new runtime errors - a serious maintenance hazard.
Add "limited" to anonymous access types. This would prevent the access value from being copied to another access type -- essentially, it can only be dereferenced.
We accomplish this by defining that limited anonymous access types have the same sort of accessibility as an anonymous access-to-subprogram parameter.
limited anonymous access types always have Storage_Size = 0.
Special case: The master of the parameters of a function with a limited access result would have be the same as the call as a whole. That would allow returning part of one of the parameters as the access result (without the parameter disappearing prematurely).
For the purposes of converting to a limited access type, accessibility would be checked as follows: [I don't know off-hand how to word this, the only existing cases that I know of allow anything to be converted to the type. - RLB]
* For a limited access parameter, any accessibility is allowed.
* For a limited access result, the converted access would have to have
a lifetime at least as long as the function call. This would include its parameters, but not any local object. Special case: A limited access parameter (or part thereof) can be returned as a limited access result. [This works because the parameter has to exist at least as long as the call does: recall the special master rule noted above. And it can't be assigned or converted beyond that.]
[RLB: We would like to allow a function with a limited access result to return the result of a call to another function with a limited access result. But that is not safe given the rules as given here. Imagine:
function B (P : access Something) return limited access Something is begin return P; end B;
function A return limited access Something is begin declare Local : aliased Something; begin return Local'access; -- Illegal, local object. return B (Local'access); -- Better not be legal! end; end A;
The problem here is B has "filed-off" the accessibility of the parameter, and we now have to assume the worst. Note that making P a limited access doesn't change anything. I don't see any solution to this problem, and since it is not critical to solving the original problem, I simply didn't try.]
* For a limited access type used as the type of a component or stand-alone object,
the converted access would have to have the same or greater lifetime than the object. Special case: For a limited access type used as a component of a return object, we use the accessibility that the object will have after the return (not the one that is current inside of the return statement). Otherwise, we'd again allow local objects. Note that this is the same rule as used for limited access results. [RLB: An alternative would be to recheck the accessibility when the accessibility of the return object changes. But that sounds like a pain to implement.] [RLB: Note that 3.10.2(10/2) means that we could initialize a stand-alone object with a function returning a limited access type. But that isn't allowed "naturally" because a limited access has an infinite accessibility. Do we need a special-case rule to handle that case?? OTOH, we surely do not want to allow any other initialization of a stand-alone object with a limited access.]
* For a limited access type used as the type of a formal object, the converted
access would have to have the same or greater lifetime than the instance. [Not sure if this is actually different than the above.]
* For a limited access type used as the type of an object renaming, the converted
access would have to have the same or greater lifetime than the renaming. In the case where a function call with a limited access result is being renamed, we would not be any accessibility check. (In this case, the master of the function call is that of the renames, so everything will be OK.)
* For a limited access type used as the type of an access discriminant, ????
[Do we need the entire mess of accessibility for access discriminants, or is the admittedly messy rules for limited access components already cover what we need to allow? That is, we "just" use the accessibility of the object. I lean toward unifying them, but I'm not certain. Note that no coextensions are allowed, because there aren't any allocators.]
Replace 3.10(6) by:
access_definition ::= [null_exclusion] [limited] access [constant] subtype_mark [null_exclusion] [limited] access [protected] procedure parameter_profile [null_exclusion] [limited] access [protected] function parameter_and_result_profile
Add after 3.10(12/2):
An access_definition that contains the reserved word *limited* is called a *limited access type*. In addition, the anonymous access subtype defined by the access_definition of a parameter is a limited access type if the access_definition defines an access-to-subprogram type.
[RLB: should this term include "anonymous" since we are only going to define these for anonymous access types? That is, "limited anonymous access type".]
The Storage_Size of a limited access type is defined to be zero. [RLB: This is intended to trigger 4.8(5.3/2). Note that we can't actually write the Storage_Size attribute for these types, so I can't steal the wording of E.2.2(17/2).]
Replace 3.10.2(13.1/2):
* The accessibility level of a limited access type is deeper than that of any master;
all limited access types have this same level.
[RLB: We probably ought change all of the other existing uses of "anonymous access type" in this list of bullets to say "non-limited anonymous access type" to avoid ambiguity, and also in 3.10.2(19/2). That's annoying.]
Replace 3.10.2(18.1/2):
* The accessibility level of a limited access type is statically deeper than that
of any master; all limited access types have this same level,
** Rest TBD **
This proposal grew out of discussions about the problems noted in AI05-0142-1; what usually is wanted is an access value that can be dereferenced but not converted to some other type.
We only allow anonymous limited access types as it would be barely useful to allocate objects for a type that couldn't be copied. Moreover, the implicit conversions of anonymous access is exactly what is needed: conversions in are automatic, and conversions out are illegal anyway, so there is no problem that they are harder than usual to define.
We do include anonymous access-to-subprogram types, as it seems that they could be useful as well -- besides, we invented the idea for them originally.
Allocators are not allowed for limited anonymous access types. We could try to define their accessibility (it would be similar to what is allowed to be converted), but it doesn't seem very useful, as the object could only be dereferenced afterwards. Moreover, it seems difficult to avoid storage leaks. Finally, user experience with the existing allocators of anonymous access types is that they are usually exactly the wrong thing, so we don't see any compelling reason to make this problem worse with a new feature.
The intent of this feature is to use it in cases where it is desired to be able to return a writable reference to an object, but where creating a first-class access type is undesirable (because of lifetime issues, perhaps).
This feature directly cannot solve all dangling access issues, but it can be combined with other existing Ada features to do so.
For instance, in the containers libraries, it would be tempting to define:
function Modifiable_Element (Container : in out Vector; Position : in Cursor) return limited access Element_Type;
However, this would not be safe if the element was deleted from the container while the limited access object still exists. It might appear that this is unlikely, but it would be easy to rename the result of one of these calls or pass it's designated object as the parameter of a complex subprogram. Once the usage is sufficiently separated from the original call, it no longer would be unlikely for some modification to be performed to the container. (Recall that since we allow "sliding" of elements in vectors, deleting any element from a vector would change the element accessed, possibly leading to erroneous execution if a discriminant constraint had been applied somewhere.)
But we can combine this feature with finalization of objects to handle the problem properly (see the examples below).
In the existing containers libraries, we need to be able to treat the entire existence of a limited access to an element to be program text where tampering with elements is prohibited. The tricky part is getting a callback to clear the tampering state when the limited access no longer exists, since we don't have (non-trivial) finalization of access values.
We can accomplish this callback by wrapping the limited access in a controlled object. This requires two types in Ada as we currently have it defined, one for the private tampering mechnism, and one for the public limited access:
type I_Tamper_With_Elements is tagged limited private;
type Element_Accessor is limited new I_Tamper_With_Elements with record Element : limited access Element_Type; end record;
function Modifiable_Element (Container : in out Vector; Position : in Cursor) return Element_Accessor;
Note that the name "Element" has already been used by the containers libraries, and it is fact was used for a function with a type-conforming profile, so using that name would be inadvisable. Moreover, the fact that the container is "in out" when it can be modified (a standard design feature of the containers libraries) suggests that a different name be used so that it is still possible to read "in" parameter containers.
This could be used (based on a recent example from comp.lang.ada):
with Ada.Containers.Vectors; procedure Check is package Integer_Vectors is new Ada.Containers.Vectors (Index_Type => Natural, Element_Type => Integer);
package Nested_Vectors is new Ada.Containers.Vectors (Index_Type => Natural, Element_Type => Integer_Vectors.Vector, "=" => Integer_Vectors."=");
IV : Integer_Vectors.Vector; NV : Nested_Vectors.Vector; begin IV.Append(42); NV.Append(IV); NV.Modifiable_Element(0).Append(43); end Check;
The dereference does not need to be explicitly given in this case, and the fact that the object returned by Modifiable_Element is limited and (probably) controlled is not visible in the code.
You could also save the entire object as long as needed:
declare My_IV : Nested_Vectors.Element_Accessor := NV.Modifiable_Element(0); begin
(since My_NV would be built-in-place).
Note that this design still works as intended even if the user renames only the limited access value:
declare My_IV : limited access Integer_Vectors.Vector renames NV.Modifiable_Element(0).all; begin
Even in this case, the wrapping finalizable object still is protecting the container, because the master of the returned object is the master of the renames: it will stick around as long as the renames does.
Moreover, splitting the access from the object usually isn't possible:
declare My_IV : limited access Integer_Vectors.Vector := NV.Modifiable_Element(0).all; -- Illegal! begin
This is illegal because the accessibility check fails. (And that's intentional!)
It is OK to pass this element as a parameter, as again the wrapping object will live as long as the parameter:
Operate (NV.Modifiable_Element(0));
Munge (NV.Modifiable_Element(0).all);
Claw uses a very similar scheme to ensure that an object which has been returned cannot become dangling because of the actions of another task. Since Claw is written in Ada 95, it could not prevent the client from copying the access and defeating the purpose of the lock object. But the limited access type would prevent that problem.
Aside: It would be nice in this case if we could avoid the extra type in order to have a partially visible type. But I don't think this case is common enough to justify the violence to the language semantics that would be needed. Especially given the confusion that would ensue given that a partial view is of a private type, so some other description would have to be given to a partially private type.
!ACATS test
ACATS tests would be needed for this feature.


Questions? Ask the ACAA Technical Agent