Bug or correct? Access discriminant check

This code fails starting with GNAT 11 up to 14 with the error below:

with Ada.Containers.Indefinite_Holders;

package Bug_Anon_Access is

   type Base is tagged limited null record;

   type Ref (Ptr : access Base) is null record;

   package Ref_Holders is new Ada.Containers.Indefinite_Holders (Ref);
   --  Warning is for the previous line

end Bug_Anon_Access;

bug_anon_access.ads:10:04: warning: in instantiation at a-coinho.adb:425 [enabled by default]
bug_anon_access.ads:10:04: warning: anonymous access discriminant is too deep for use in allocator [enabled by default]
bug_anon_access.ads:10:04: warning: Program_Error will be raised at run time [enabled by default]

Indeed at runtime in a program with a similar structure I see the Program_Error.

Is this a proper diagnostic? Simon Wright pointed me to:

--  Element allocator may need an accessibility check in case actual type
--  is class-wide or has access discriminants (RM 4.8(10.1) and
--  AI12-0035).

But without understanding it properly, it looks suspicious that such a simple construct isn’t allowed.

I think it is correct. Here is my thought process:
In the general case, any object passed there will potentially / most likely be statically deeper than library level. Since this holder is allocating Ref on the heap, then it technically is at library level, meaning the object pointed to by Ref.Ptr could be deallocated before Ref itself is destroyed, leaving a potentially dangling pointer.

Imagine:


function Bad return Ref_Holders.Holder is
   B : aliased Base;
   R : Ref(Base'Access);
begin
   return Ref_Holders.To_Holder(R);
end Bad;

Thing : Ref_Holders.Holder := Bad; -- Dangling reference

I don’t know if the anon access rules are specific enough to distinguish between the general case and the specific case.

That said, I am not a language lawyer, so I don’t know for sure.

I noticed that if I changed the discriminant to a named access type, the issue goes away (since named access types assume library level).

Yes, but named access types don’t allow the use of implicit dereference (which I omitted to reduce the reproducer to the minimum, but is part of the reasons behind that type in the real use case).

I follow your reasoning that Ref.Ptr may become dangling. But then, this will be true for any type declared outside of the generic, right? So it’s basically impossible to have an anon access discriminant in a collection? I’m also a non-lawyer.

I think when they introduced implicit_dereference it was not intended for long lived objects (which would preclude saving them to a collection if so). They even go as far in the standard library (see the vectors package) to ensure you cannot easily have a collection of container element Reference_Type objects by making them default initialize to an exception being raised.

I’m not saying I am philosophically opposed to what you are looking to do. I can see applications as well. This is more of my impression of what the intent of the ARG was (which could be wrong).

I think it’s less about where the type is declared and more about where the object of type Ref is created and where the object the Ref.Ptr points to is created (there is some relationship between where the type is declared and where the object can be created for sure). If I understand correctly the object pointed to by Ref.Ptr can never be allocated statically deeper than Ref itself to ensure that Ref.Ptr cannot dangle.

Since Ref would be allocated on the heap (library level) in an Indefinite_Holder, in general it could only point to objects created at library level. I just don’t know if the Ada rules allow the compiler to reason that in general or not.

The only scenario I could come up with that would work in general is if the anonymous access discriminant pointed to something in the Ref object itself, but that limits its use to things like the Rosen Technique, so not helpful for your case.

Again, I’m not against what you are looking for. I’m more just working out my reasoning of why I think it does what it does. In theory everything you are wanting to do should be able to be done (other languages like Rust have solved it by bounding the problem differently), but I don’t know if the Ada rules are mature enough to cover it right now. And I could be wrong in my reasoning, so there is that.

I didn’t know this, but in a way it’s an orthogonal problem, as without Implicit_Dereference the problem still exists.

Actually there’s Rosen Technique at play here, in that the object pointed to in my real case is a limited record using the Rosen technique, so it’s the same access that will end in the access discriminant. Those are manually allocated in the heap.

Anyway. Thanks a lot for your thoughtful reply, but this goes well over my head. Even if I managed to completely understand it, I would forget the details and what’s safe/unsafe in a month. I guess I will live with the explicit dereference and, once again, lesson learned: don’t attempt fancy things with access types.

Containers are meant to be a way to manage memory. (See the JP Rosen talk/video on managing memory in Ada 2012; FOSDEM, 2014, IIRC.)

The [anonymous] access discriminant binds the lifetime of the access to the object itself, while Rosen’s technique allows for defaults to be taken from the object itself, meaning you can have safe self-referential constructs.

So, while your example does generate a dangling reference, it is because it is operating on a local-object rather than being about any of the types you’ve presented.

IIUC, moving the container’s instantiation to a child of the package of the one wherein the discriminated-type is introduced should be perfectly fine: the lifetime of all container-objects becomes strictly less-than-or-equal-to the parent package wherein the discriminated type is defined, meaning that there is no way the lifetime can exceed it.

This is news to me, I would have expected that, with everything at library level and non-generic, that wouldn’t make a difference. I will try it.

I meant more that an object using the Rosen technique could safely be created on the heap since it always points to itself. In your case you are creating the handle (Ref) on the heap instead (and not the object using the Rosen technique itself), so the problem still remains. That’s why I was leaning towards it not being helpful for your case.

If this does work out, definitely let us know. I hope you get a working solution for sure.

I don’t know if this is helpful, but I have a Shared_Access type that can be used to hold a reference in another container (supports limited and indefinite). bullfrog/src/bullfrog-access_types-smart_access.ads at 0420184ead40fe6ce418d14d738887d9fc779807 · jere-software/bullfrog · GitHub

You just need to allocate your limited type using the Make.Shared_Access function Ref : Shared_Access := Make.Shared_Access(new My_Type); then you can copy that into a container and it will handle reference counting. You access the container via the Reference function: Ref.Reference.Some_Container_Operation;

It would be something you would use in place of Ref instead of building ref on it.

Also, I don’t know if it is helpful to your main use case or not, but I have a basic limited holder type: bullfrog/src/bullfrog-containers-limited_indefinite_holders.ads at 0420184ead40fe6ce418d14d738887d9fc779807 · jere-software/bullfrog · GitHub

It didn’t work, the same warning is produced when compiling the child package.

Ah, that’s right.

Thanks for the pointers (ha) to those libraries, I will definitely check them if only for general use. For this case, this is a large codebase, partially autogenerated from C headers, that I maintain only from time to time, so any major refactorings give me pause. I don’t even remember why I have this type except for some vague notion that the base type is limited and I needed to store the reference for a while in a callback, and I wanted to retain the original interface (hence the implicit dereference).

Were you using the same method of constructing the reference though?
If you have:

Type T is null record;
Type A is access T;
Function Make return A is
  X : aliased T;
begin
   Return Result : A:= X'Access;
end Make;

This will violate the dangling pointer rules, precisely because you’re referring to X, which is a local object.

There’s nothing else but what you see at the initial example: type declarations and one instantiation. I just moved the instantiation to a child package.