Ada-Rapporteur-Group / User-Community-Input

Ada User Community Input Working Group - Github Mirror Prototype
27 stars 1 forks source link

Restrictions on dynamic accessibility checks #31

Open sttaft opened 1 year ago

sttaft commented 1 year ago

This is a long discussion from the ARG mailing list, which seems more appropriate for an ARG "Issue" that is open to wider discussion. Bob kicked off the email chain by asking for a better subject line, which is now the Title of this Issue.


Bob Duff duff@adacore.com via ada-auth.org, Jan 2, 2022, 4:06 PM How about we use a more-informative subject than "First Homework reminder for electronic meeting on February 3rd."  ;-)

Tucker Taft taft@adacore.com, Jan 3, 2022, 1:08 PM [Moving to Bob's renamed thread]

Actually, after looking a bit at AI12-0016 and its scary complexity, I think we might want to consider reverse engineering the rules, and say that conversion from an access parameter to some named type will fail in cases where a "small integer" model won't work. GNAT has never fully implemented the non-small-integer rules, and I doubt if anyone else has either.

I think it might be possible to require that the "small integer" accessibility level passed along with an access parameter through an indirect call where the target subprogram could be arbitrarily deep, is either infinity, or a small integer that is known to be meaningful to both the caller and the callee.  For a dispatching call, it would be limited by the depth of the root of the class-wide type.  For a call through an anonymous access-to-subprogram value, this level would be limited by the deepest of the (named) types appearing in the parameter/result profile.  For example:

    type T is tagged ...     procedure Dispat (A : access T; ...);

    ...

         X.Dispat(...);          -- accessibility level associated with X is either infinity, or in range 0 to level of T.

===

    procedure Foo (P : access procedure (A : access T1; B : T2; C : T3)) is     begin          P.all(X, ...);          --  accessibility level associated with X is either infinity, or in range 0 to deepest(levels of T1, T2, T3)

===

Note that we are talking about the levels of the types of the parameters/result, not the subtypes, since subtypes from different levels can in general match, whereas when making an indirect call, types must match exactly.

-Tuck


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 3, 2022, 7:22 PM Tucker wrote:

Actually, after looking a bit at AI12-0016 and its scary complexity, I think we might want to consider reverse engineering the rules, and say that conversion from an access parameter to some named type will fail in cases where a "small integer" model won't work.  

  I believe that is already the case (or at least very close to it). The problem is how to make a check in the case that they fail. For instance, they don't work when one is dealing with "incomparable" accessibility -- where the two accessibilities come from different tasks. Obviously, the "small integers" are each relative to a task.   I suppose one could design a model using "small integers" + a task id, but that's still more expensive than a small integer alone. And I don't know if that is enough by itself.   Finally, the "small integer" model never worked in Ada (even Ada 95) without adding some hacks (special cases where one has to adjust the small integers). I'm dubious that there is much value to adding more hacks.  

GNAT has never fully implemented the non-small-integer rules, and I doubt if anyone else has either.   

I designed an implementation for Janus/Ada, but since it is tied to another memory management change, never implemented it. It's dubious that it is worth it.   Indeed, I'm dubious that any dynamic accessibility check is worth it. One could pretty much eliminate them if one had a restriction to prevent declaring access types and tagged types inside of subprograms. (You'd also need something to get rid of dynamic accessibility with anonymous access parameters and access returns -- or just ban anonymous access types.)  

I think it might be possible to require that the "small integer" accessibility level passed along with an access parameter through an indirect call where the target subprogram could be arbitrarily deep, is either infinity, or a small integer that is known to be meaningful to both the caller and the callee.  For a dispatching call, it would be limited by the depth of the root of the class-wide type.  For a call through an anonymous access-to-subprogram value, this level would be limited by the deepest of the (named) types appearing in the parameter/result profile.

  "Limiting the level" is a hack; it's done to make the mechanism work; it has no real justification.   In any case, the real problem cases are with "incomparable" levels, and I don't see how the above helps with that at all. The obvious way to deal with that is to ban the case (that is, no type extensions in tasks other than of types declared in the task). Statically, that could only be done by banning all nested extensions (thus, reverting to the Ada 95 rule that should never have been changed in the first place), but I suppose a less incompatible fix would be to have a dynamic check on the declaration of an extension.                              Randy.


Tucker Taft taft@adacore.com, Jan 5, 2022, 12:01 AM

On Mon, Jan 3, 2022 at 7:22 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote:

...

Actually, after looking a bit at AI12-0016 and its scary complexity, I think we might want to consider reverse engineering the rules, and say that conversion from an access parameter to some named type will fail in cases where a "small integer" model won't work.  

 

I believe that is already the case (or at least very close to it). The problem is how to make a check in the case that they fail. For instance, they don't work when one is dealing with "incomparable" accessibility -- where the two accessibilities come from different tasks. Obviously, the "small integers" are each relative to a task.

I wasn't worrying about the task issue, though I think the same rule could apply, since tasks are not significantly different from nested subprograms, except across task entry calls, where access parameters are not permitted in any case.  Tagged type issues can arise across task entry calls, but I think we begin to get into the noise there...  

I suppose one could design a model using "small integers" + a task id, but that's still more expensive than a small integer alone. And I don't know if that is enough by itself.

I think that would be overkill.     ...

Indeed, I'm dubious that any dynamic accessibility check is worth it. One could pretty much eliminate them if one had a restriction to prevent declaring access types and tagged types inside of subprograms. (You'd also need something to get rid of dynamic accessibility with anonymous access parameters and access returns -- or just ban anonymous access types.)

That would of course be nightmarishly incompatible, given that all container generics could now only be instantiated at top level.  

I think it might be possible to require that the "small integer" accessibility level passed along with an access parameter through an indirect call where the target subprogram could be arbitrarily deep, is either infinity, or a small integer that is known to be meaningful to both the caller and the callee.  For a dispatching call, it would be limited by the depth of the root of the class-wide type.  For a call through an anonymous access-to-subprogram value, this level would be limited by the deepest of the (named) types appearing in the parameter/result profile.   

"Limiting the level" is a hack; it's done to make the mechanism work; it has no real justification.

But it is also accepting reality that full and correct dynamic accessibility checking is unlikely ever to be properly implemented.  

In any case, the real problem cases are with "incomparable" levels, and I don't see how the above helps with that at all.

I don't see incomparable levels as a problem that comes up much in practice. 

The obvious way to deal with that is to ban the case (that is, no type extensions in tasks other than of types declared in the task). Statically, that could only be done by banning all nested extensions (thus, reverting to the Ada 95 rule that should never have been changed in the first place), but I suppose a less incompatible fix would be to have a dynamic check on the declaration of an extension.

I am hoping we can limit the incompatibility to cases where we are converting from an access parameter to a named access type.

AdaCore is honing in on a static-checks-only restriction for accessibility, and I think that is something we should definitely consider as a standard Ada restriction (currently dubbed No_Dynamic_Accessibility_Checks).

Take care, -Tuck


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 5, 2022, 8:43 PM Tucker Taft writes:

On Mon, Jan 3, 2022 at 7:22 PM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote:

Tucker wrote:

Actually, after looking a bit at AI12-0016 and its scary complexity, I think we might want to consider reverse engineering the rules, and say that conversion from an access parameter to some named type will fail in cases where a "small integer" model won't work. 

I believe that is already the case (or at least very close to it). The problem is how to make a check in the case that they fail. For instance, they don't work when one is dealing with "incomparable" accessibility -- where the two accessibilities come from different tasks. Obviously, the "small integers" are each relative to a task.

I wasn't worrying about the task issue, though I think the same rule could apply, since tasks are not significantly different from nested subprograms, except across task entry calls, where access parameters are not permitted in any case.  Tagged type issues can arise across task entry calls, but I think we begin to get into the noise there...

I strongly disagree that tagged type issues are "in the noise" here; they are the only realistic case for a dynamic check IMHO. If we were only talking about access types, I would advocate for a very simple model for any checks (library-level vs. non-library-level), as that is sufficient for almost all realistic examples.

As an aside here, I note that I did design ACATS tests for a variety of accessibility tests, especially including the ones for class-wide tagged types. I found that the access type tests require rather contrived circumstances, while the tagged type cases seemed more likely. Moreover, my original intent with tagged types was to only test static cases (in part because of AI12-0016-1), but I soon found that dynamic checks were needed in fairly natural cases (as soon as class-wide parameters got involved). In the end, I created dynamic check tests both for the usual nesting cases and the incomparable cases (not issuing the latter tests; I wrote them once I figured out how so I wouldn't have to remember that knowledge at some future point). I thought that all of those tests looked plausible (in the absence of a library-level only restriction on tagged types).

The thing with access types is that one rarely uses a named access type inside of a subprogram or other nested scope. Moreover, for many uses, accessibility checks simply don't work, and one has to manage lifetimes themselves (for instance, with controlled types) [Claw works that way, for instance.] So many uses only use 'Unchecked_Access and/or New for access types to start with. Many more uses involve library-level data structures, where the check is trivial (the object is either library-level or it is not).

So the remaining access cases are themselves almost completely "in the noise", while passing class-wide parameters is very natural and certainly is going to happen across tasks just as much as across anything else.

In the absence of compatibility concerns, dynamic checks (as defined in Ada, at least) have insufficient value to even bother with. But we have to waste time on them because of compatibility concerns.

I suppose one could design a model using "small integers" + a task id, but that's still more expensive than a small integer alone. And I don't know if that is enough by itself.

I think that would be overkill.

... Indeed, I'm dubious that any dynamic accessibility check is worth it. One could pretty much eliminate them if one had a restriction to prevent declaring access types and tagged types inside of subprograms. (You'd also need something to get rid of dynamic accessibility with anonymous access parameters and access returns -- or just ban anonymous access types.)

That would of course be nightmarishly incompatible, given that all container generics could now only be instantiated at top level.

It's somewhat off topic, but I've come to the conclusion that it rarely makes sense to declare any non-scalar type (as opposed to a constraint of an existed type) in a subprogram. From a language design perspective and especially an implementation perspective, it adds a lot of complexity for very little benefit. (And scalar types get excepted mostly because they can't depend on anything declared in the subprogram - it probably would make the most sense to not allow them, either.) From a usage/design perspective, declaring composite types in a subprogram generally means that a design hasn't been sufficiently abstracted. About the only case where it might help is in creating unit tests; but given that it is easy to introduce a package, it's doesn't buy a lot of additional value. (And if we could have gotten "integrated packages" to work, it would have been even easier to introduce a package.)

Moreover, in a template implementation, its terrible to instantiation generics willy-nilly -- you have to keep them in a few library-level packages in order to avoid code explosion. And even in a shared implementation, it's better to avoid lots of instantiations. So I think it would almost always be better if programmers didn't try to declare containers in nested scopes.

Having said that, I realize the level of incompatibility would likely be unacceptable. But I don't think it would be unacceptable as an option. It could make much more sense to virtually eliminate dynamic checks via restrictions, but still support the full nonsense for compatibility.

I think it might be possible to require that the "small integer" accessibility level passed along with an access parameter through an indirect call where the target subprogram could be arbitrarily deep, is either infinity, or a small integer that is known to be meaningful to both the caller and the callee.

The vast majority of access parameters are never converted to anything, and thus all of the dynamic accessibility associated with them is nonsense. And for the rest, it is a hazard. For Claw, I learned to always use "P.all'Unchecked_Access" rather than converting the access parameter P, since we certainly didn't want any check -- we remove objects from the global lists when they are finalized.

We tried (in Ada 2005) to eliminate the dynamic checks via preconditions (using the access membership), but so far as I can tell, that never happened in any implementation. (In part because GNAT was late to even implementing the membership, so it never got any uptake.)

For a dispatching call, it would be limited by the depth of the root of the class-wide type.  For a call through an anonymous access-to-subprogram value, this level would be limited by the deepest of the (named) types appearing in the parameter/result profile.

"Limiting the level" is a hack; it's done to make the mechanism work; it has no real justification.

But it is also accepting reality that full and correct dynamic accessibility checking is unlikely ever to be properly implemented.

Sure, but there is no evidence that the dynamic accessibility checking (at least for access types) has ever done anything other than unexpectedly raise an exception (and thus break otherwise working code). It just gets in the way most of the time, it's unclear that it is worth the complexity and cost at any time. The problem that it is trying to solve (dangling pointers) is well-understood, and the dynamic checks don't do a lot to prevent problems (as the major cause is the use of Unchecked_Deallocation).

It's more important for tagged types, as it is trivial to cause a dynamic check (just use an object of a class-wide type where the root isn't library-level). Here, the problem of a "dangling type extension" is not well-understood. Moreover, the result of using a dangling type extension could easily vary from nothing to disastrous depending on the implementation model and how much dynamic stuff is associated with the extension and it's subprograms. A problem could easily hide there.

Even so, I suspect we could easily adopt some rules that would fix any problems. (Don't allow extensions in a different task than the one that elaborated the original declaration would solve all of the incomparability issues, for instance.)

In any case, the real problem cases are with "incomparable" levels, and I don't see how the above helps with that at all.

I don't see incomparable levels as a problem that comes up much in practice.

It's the only problem that I know of that really does not work with the usual small integer implementation. (Steve's pathologies are just that, fine to call them erroneous.)

The obvious way to deal with that is to ban the case (that is, no type extensions in tasks other than of types declared in the task). Statically, that could only be done by banning all nested extensions (thus, reverting to the Ada 95 rule that should never have been changed in the first place), but I suppose a less incompatible fix would be to have a dynamic check on the declaration of an extension.

I am hoping we can limit the incompatibility to cases where we are converting from an access parameter to a named access type.

That almost never happens in practice, so it doesn't even matter if it works right or not. I'd simply make such conversion erroneous (maybe even completely removing the dynamic check). But that doesn't cause any real problems.

AdaCore is honing in on a static-checks-only restriction for accessibility, and I think that is something we should definitely consider as a standard Ada restriction (currently dubbed No_Dynamic_Accessibility_Checks).

Yes, of course. I hope that they make sure they aren't leaving holes in the tagged type cases. I don't care much about access cases, as I noted above, cases where a dynamic check would help are in the noise. I worry more about the tagged type cases because the effect of a dangling type is essentially unknowable and unbounded, potentially far worse than anything that could happen from a dangling pointer.

But, unfortunately, we still have to properly define the dynamic checks, since we can't require everyone to use such a restriction. (We should, but I don't think we would have the will.) The tagged type cases are many times more likely to show up in practice, just because they're fairly easy to create. So we need to come up with solutions for them. I don't see much point in making dynamic checks for access types more expensive, I'd just declare any cases that don't work erroneous and forget it. (Maybe with a restriction to ban any such cases? Dunno.)

                           Randy.


Tucker Taft taft@adacore.com, Jan 5, 2022, 9:05 PM

As far as "incomparable levels" I believe they can only arise in accept statements.  Accept statements have largely disappeared ever since protected types came along.  And they can only happen if a class-wide object is passed as a parameter to the accept statement, and if inside the accept statement we do an allocator for an access type declared inside the task or call a function declared inside the task that takes and returns the class-wide object that was passed as a parameter.

To me this seems to be all in the noise.  Did you have a coding pattern that would actually do any of the above things in accept statements?

I agree that other cases of tagged-type level checks are relevant, but the "incomparable" cases seem to be of little consequence.  Am I missing something obvious?

-Tuck


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 5, 2022, 9:35 PM

...

I strongly disagree that tagged type issues are "in the noise" here; they are the only realistic case for a dynamic check IMHO....

My guess is that our relative probabilities on these occurrences differ. I think that most new types (especially ADTs) should be tagged, and as a consequence, passing of tagged parameters (and class-wide parameters) are relatively common. And the use of functions returning things is relatively common.   I don't have much information about the relative use of accept statements vs. protected types, as they both have uses. Accept statements are still useful for getting the initial data into a "worker" task -- perhaps worker tasks will disappear in favor of parallel constructs, but that probably won't happen before GNAT implements parallel constructs.   So the use case I'm imagining is passing in a class-wide object to an entry (and accept statement) for a worker task. The worker will have to keep a copy of it somehow, and using an allocator certainly makes sense as part of that. Putting an object into an indefinite container can also trigger an accessibility check (similar to the allocator one), by A.18(7-10/4), and that might be an even more reasonable case. (Why wouldn't one try to put a class-wide parameter into a holder container to keep a copy???)   Now, I don't think any dynamic accessibility check is particularly likely, but I do think that the tagged one is more important since it is hard to describe what might happen if it isn't made. And this worker task case seems to have a reasonable probability of happening (I have tasks like this, but no nested tagged types to cause trouble).                                  Randy.

sttaft commented 1 year ago

Tucker Taft taft@adacore.com, Jan 6, 2022, 12:01 AM

OK, I did another round of thinking about this, and pretty much convinced myself that an approach using "master" records might ultimately be acceptably efficient.  So here is a proposal for effectively a second version of AI12-0016, but might as well give it an AI22 number... -Tuck

================

The basic idea is to create a "nested-extension" master record in any scope where a nested type extension is declared, and a "nested-access-type" master record in any scope where an access type is declared that is more nested than its designated type.  (By "nested type extension" we mean a tagged type declared in a scope more nested than its ultimate ancestor.)  Each master record would point to the innermost enclosing master record of the same kind. These two kinds of master records could be merged, and could even be merged with "normal" master records used for finalization and/or task waiting, but it seems cleanest to separate them for the purposes of this discussion.  Each kind of master record would need its own thread-local pointer to keep track of the "current" innermost master record of the given kind for each Ada task.  Thread-local storage is pretty efficient these days, and is available on almost every target.  An alternative is using slots within the task control block.  The "current" master record pointers would be null for anything at the library level.

Examples:     type T2 is new T1 with ...     --  Scope of T2 will get a "nested-extension" master if T1 or     --  any of its ancestors are declared at an outer scope.  The     --  nested-extension master would point to the innermost     --  (dynamically) enclosing such master, or null if there isn't     --  one (effectively representing library level).

   type A is access T;    --  Scope of A will get a "nested-access-type" master if    --  it is nested within the scope of T.  The "nested-access-type"    --  master will point to the innermost (dynamically) enclosing    --  such master.

Each tagged type that is a nested extension of some other tagged type would point from its (dynamic) descriptor to the "nested-extension" master declared in the same scope.  A tagged type that is not a nested extension would have a null pointer.  Elaborating a scope (other than library-level) containing the declaration of one or more functions that return class-wide types would cause a pointer to the current "nested-extension" master to be saved for later run-time checks.  The declaration of one or more access-to-class-wide types in a scope (other than library level) would similarly save a pointer to the current "nested-extension" master.  As mentioned above, these pointers might be null, and will be null for library-level declarations.

Examples:     --  (Dynamic) type descriptor for T2 (above)     --  would point to its nested-extension master.

    function CW return T1'Class;     --  The scope where this function that returns a class-wide result     --  is declared would save a pointer to the current     --  nested-extension master.

   type ACW is access T1'Class;    --  The scope where this access-to-class-wide type is declared would save    --  a pointer to the current nested-extension master

The check to ensure that the tag of a returned class-wide object outlives the function return involves walking the chain of enclosing "nested-extension" master records starting at the value saved when the function was elaborated, looking for a match to the value associated with the nested-extension master of the tag of the object being returned.  A quick check to see if the tag of the object has a null "nested-extension-master" pointer catches the simple case where the object's tag is for a type that is not a nested extension.  Similarly, when doing an allocator for an access-to-class-wide type, a walk of the chain is performed starting at the saved nested-extension master associated with the access-to-class-wide type, looking for the master for the tag of the object being allocated.  Again, if that tag has a null nested-extension-master pointer, we know the allocator is safe.  By contrast, if the access type has a null nested-extension-master pointer, and that of the allocated object is non-null, then the check immediately fails.

Examples:

    function CW return T1'Class is     begin          return X;          --  Start from nested-extension-master pointer for CW and look for          --  nested-extension-master pointer for X'Tag.  Check fails if          --  nested-extension-master pointer for X'Tag not found in walk of          --  enclosing nested-extension masters. Check succeeds immediately          --  if n-e-m pointer for X'Tag is null, implying it is not a nested extension.     end CW;

    S : constant ACW := new T1'Class (X);     --  Start from nested-extension-master pointer of ACW and look for     --  nested-extension-master pointer for X'Tag.  Check fails if     --  nested-extension-master pointer for X'Tag not found in walk of     --  enclosing nested-extension masters.  Check succeeds immediately     --  if n-e-m pointer for X'Tag is null, implying it is not a nested extension.

A similar approach can be used for "normal" dynamic accessibility checks, where we create a "nested-access-type" master record in any scope where a nested access type is declared, and we save a pointer to it from every such nested access type (for an access type declared in the same scope as its designated type, this pointer is defined to be null).  We also save a pointer from any scope where any aliased objects are declared, with one additional bit to indicate whether or not the scope with aliased objects is an exact match for the scope where the innermost nested-access-type master is also declared.  The pointer for a given aliased object is defined to be null if it is declared in the same scope as its type, and the extra bit would indicate it is an exact match for that "null" master.

An accessibility check involves starting at the saved pointer for the target access type, following the enclosing-master chain, looking for the master associated with the aliased object.  A quick check to see if the master associated with the object is an exact match for "null" means the check passes.  If the two are the same pointer to begin with, then the check succeeds if the object is an exact match; otherwise it fails.

Examples TBD (sorry!).

=========

I think this mechanism can handle essentially all existing cases (with some inevitable refinements ;-), including "incomparable" cases.  Because of the various special cases, there should be very little overhead for code that avoids nested extensions, and access types more nested than their designated type, since we only create master records when we have a nested extension or an access type more nested than its designated type.

Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 7, 2022, 12:43 AM

This approach is fine. But it seems to be the same as the one that Steve described in the original AI12-0016-1 (probably more concisely), and essentially the same as the approach that I planned on since the original AI12-0016-1 came around. (I was going to provide a detailed description in the paper Steve was tasked to write, but since he never did that, I never did either.)   There are a few detail differences (I was planning to use a storage pool object as what you call the "master record", as those are needed anyway in many of the same scopes), and one somewhat significant difference. That is, I was planning to keep a task id (really a stack id in this context) in each "master record"; then for an accessibility check if the task ids of each master record are the same, then one just needs to compare the addresses of the master records to determine the lifetime ordering. Ergo, one only needs to walk chains (which can get fairly long because of recursion) in the rare case that multiple tasks are involved.   Indeed, if one could somehow figure out whether the two accessibilities are on the same stack, any reproducible pointer to the stack could be used (in Janus/Ada, the thumb pointer for each scope would work fine; one could also use the frame pointer if that is per-master rather than per-subprogram). One only really needs "master records" if one is dealing with incomparable accessibility.   That's why I suggested a task-id/ptr pair for accessibility checks; that would be sufficient IFF we simply said that accessibility always fails when comparing items that belong to different tasks (regardless of nesting) -- of course, treating library-level as always working/failing (depending on the comparison). That would avoid any distributed overhead (there is a small distributed overhead from "master records" as they have to be initialized in any scope where something might need one [you explained that well, so I'll be handwavey here]). The check then is:      if Source'Accessibility_Location = null then -- Source is library-level         null; -- OK.      elsif Target'Accessibility_Location = null then -- Target is library-level         raise Program_Error; -- The source isn't library-level, so this always fails.      elsif Source'Task_Id = Target'Task_Id then          if Source'Accessibility_Location > Target'Accessibility_Location then              raise Program_Error; -- The souce has a shorter lifetime than the target.                  -- Note: I'm assuming the Intel downward growing stack. Reverse if                  -- if the stack grows upward.          -- else OK.          end if;      else          raise Program_Error; -- Belongs to two different tasks -- this would require some rule changes.      end if;   I note that if this is done in-line, optimization may be able to remove most of the branches depending on the origin of the source and target.   Not sure if there is any important case where cross-task accessibility is important (I doubt it), but that would simplify the model and check. I don't see any way to make the model simple without involving the task-id or something similar, because doing a ordering check on two different stacks would give bogus answers. And if you can't use ordering, you always end up running a chain (and more importantly), causing overhead by constructing that chain.                            Randy.

Tucker Taft taft@adacore.com, Jan 7, 2022, 1:46 PM


On Fri, Jan 7, 2022 at 12:43 AM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote: This approach is fine. But it seems to be the same as the one that Steve described in the original AI12-0016-1 (probably more concisely), and essentially the same as the approach that I planned on since the original AI12-0016-1 came around. (I was going to provide a detailed description in the paper Steve was tasked to write, but since he never did that, I never did either.)   There are a few detail differences (I was planning to use a storage pool object as what you call the "master record", as those are needed anyway in many of the same scopes),

Certainly an implementation could piggy-back on any structure that is already in the relevant scopes.     and one somewhat significant difference. That is, I was planning to keep a task id (really a stack id in this context) in each "master record"; then for an accessibility check if the task ids of each master record are the same, then one just needs to compare the addresses of the master records to determine the lifetime ordering.

This presumes that stacks are contiguous, which is perhaps a safe assumption these days.  In the "old" days, stacks were sometimes broken up into chunks, which were linked together.  I suppose you could have a chunk number + stack offset, such that chunk numbers are monotonically increasing or decreasing in a given task.   Ergo, one only needs to walk chains (which can get fairly long because of recursion) in the rare case that multiple tasks are involved.

Or perhaps you could walk task dependency chains, presuming a task identifies upon creation the stack/scope pointer at its moment of creation.

  Indeed, if one could somehow figure out whether the two accessibilities are on the same stack, any reproducible pointer to the stack could be used (in Janus/Ada, the thumb pointer for each scope would work fine; one could also use the frame pointer if that is per-master rather than per-subprogram). One only really needs "master records" if one is dealing with incomparable accessibility.

Or as mentioned, one might walk the task dependency tree.

I would be suggest we separate the general accessibility checking from that for nested type extensions (as I did in my proposal), because we could over time move toward static accessibility checking, but I don't see (at least at this moment!) any easy way to move toward static nested-extension checking.   That's why I suggested a task-id/ptr pair for accessibility checks; that would be sufficient IFF we simply said that accessibility always fails when comparing items that belong to different tasks (regardless of nesting) -- of course, treating library-level as always working/failing (depending on the comparison). That would avoid any distributed overhead (there is a small distributed overhead from "master records" as they have to be initialized in any scope where something might need one [you explained that well, so I'll be handwavey here]).

Or we could store the task-id inside some sort of master record, and point to that -- in some implementations, it might be more expensive to pass around a pointer plus a task id than to pass around a simple pointer to a record that included the task-id.   The check then is:      if Source'Accessibility_Location = null then -- Source is library-level         null; -- OK.      elsif Target'Accessibility_Location = null then -- Target is library-level         raise Program_Error; -- The source isn't library-level, so this always fails.      elsif Source'Task_Id = Target'Task_Id then          if Source'Accessibility_Location > Target'Accessibility_Location then              raise Program_Error; -- The souce has a shorter lifetime than the target.                  -- Note: I'm assuming the Intel downward growing stack. Reverse if                  -- if the stack grows upward.          -- else OK.          end if;      else          raise Program_Error; -- Belongs to two different tasks -- this would require some rule changes.      end if;   I note that if this is done in-line, optimization may be able to remove most of the branches depending on the origin of the source and target.   Not sure if there is any important case where cross-task accessibility is important (I doubt it),

You provided an interesting use-case of this for the "worker task" situation, but that was just for the nested-extension check, I believe.  I would agree that for normal accessibility checking, we could impose restrictions about cross-task checks.   but that would simplify the model and check. I don't see any way to make the model simple without involving the task-id or something similar, because doing a ordering check on two different stacks would give bogus answers. And if you can't use ordering, you always end up running a chain (and more importantly), causing overhead by constructing that chain.

Note that in my recent proposal, you don't have to create an entry in the chain in every scope.  Only the scopes that declare nested extensions (which seems pretty rare!) for the nested-extension check, and in scopes where you declare nested access types (admittedly probably more common), in the accessibility checking case.  In any case, ideally the various approaches could be shown to be equivalent, so implementors could make their own efficiency tradeoffs.                            Randy.

-Tuck


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 8, 2022, 2:02 AM

Tucker wrote:

...

Ergo, one only needs to walk chains (which can get fairly long because of recursion) in the rare case that multiple tasks are involved.

Or perhaps you could walk task dependency chains, presuming a task identifies upon creation the stack/scope pointer at its moment of creation.

Right, something like that would work.

...

I would be suggest we separate the general accessibility checking from that for nested type extensions (as I did in my proposal), because we could over time move toward static accessibility checking, but I don't see (at least at this moment!) any easy way to move toward static nested-extension checking.

The RM calls them both "accessibility" checks, so I think this attempt to split them would be confusing, at least to anyone parachuting into the discussion.

And static checks (really no checks) for nested extensions are easy -- don't allow nested extensions! Janus/Ada has never supported them, and I've never had anyone complain. (And I've had people complain about lots of other things that aren't implemented.)

But I definitely agree with the former as well -- as I noted in an earlier message, dynamic accessibility checks for access types almost never do anything useful. That's why it's hard to justify much overhead.

That's why I suggested a task-id/ptr pair for accessibility checks; that would be sufficient IFF we simply said that accessibility always fails when comparing items that belong to different tasks (regardless of nesting) -- of course, treating library-level as always working/failing (depending on the comparison). That would avoid any distributed overhead (there is a small distributed overhead from "master records" as they have to be initialized in any scope where something might need one [you explained that well, so I'll be handwavey here]).

Or we could store the task-id inside some sort of master record, and point to that -- in some implementations, it might be more expensive to pass around a pointer plus a task id than to pass around a simple pointer to a record that included the task-id.

Right, that was my previous plan. But I was trying to find a solution that had no distributed overhead unless you actually have to store a level or make check. Extra writes always slow things down (how much depends on what master they're in in a program). I would prefer to put all of the cost on anything that needs a dynamic accessibility check (there's not that much of it in most programs). YMMV.

...

Not sure if there is any important case where cross-task accessibility is important (I doubt it),

You provided an interesting use-case of this for the "worker task" situation, but that was just for the nested-extension check, I believe.  I would agree that for normal accessibility checking, we could impose restrictions about cross-task checks.

My concern is that an "incomparable level" not be allowed by some sloppy algorithm, as the effects are almost unbounded and would be hard for anyone to detect. I have much less concern that nested extensions work across tasks, because sane people are mostly going to do library-level extensions in the first place. So it's likely that one or the other will be library-level, and the check will be simple. If we could have a zero distributed overhead algorithm to make the checks (and avoid erroneous execution), I'll be happy, even if it rejects a few cases that probably ought to have worked.

but that would simplify the model and check. I don't see any way to make the model simple without involving the task-id or something similar, because doing a ordering check on two different stacks would give bogus answers. And if you can't use ordering, you always end up running a chain (and more importantly), causing overhead by constructing that chain.

Note that in my recent proposal, you don't have to create an entry in the chain in every scope.  Only the scopes that declare nested extensions (which seems pretty rare!) for the nested-extension check, and in scopes where you declare nested access types (admittedly probably more common), in the accessibility checking case.  In any case, ideally the various approaches could be shown to be equivalent, so implementers could make their own efficiency tradeoffs.

Humm. I agree about the nested extension case (since every level is associated with an extension, either that of the class-wide type or of the one associated with the tag of the object), but it would seem that you would need a "master record" for every scope that declared as aliased object. Otherwise, what would you pass to make checks for anonymous access parameters? (When you take 'Access of an object/component, you have to be able to describe its dynamic accessibility level if it is passed to an anonymous access parameter, and probably for a SAOAAT as well.) Perhaps you wouldn't need the task id in the access parameter/SAOAAT cases (I'm not certain, Steve had some wild examples), but you would need the stack ptr (in order to have it to compare to that of the access type in a check). Depending on the program, that could be quite a few scopes.

Note that my proposed algorithm only depends on allocating memory (so that each master has a unique stack address) but not necessarily writing into it. Generally, stack allocation is free at runtime, as each task subtracts some amount from the stack pointer for the frame, and allocating an extra word or two isn't going to change anything. (And that is only necessary for a master that doesn't have any memory allocation of it's own, which is fairly unlikely for a master that includes some type declaration, and impossible for a master that includes an aliased object declaration.

Anyway, I think we are converging (at least somewhat) on how this can/should be implemented. It doesn't seem bad to me, except in unusual cases. It just can't use statically determined integers (always a bit dubious for a dynamically described check).


Richard Wai richard@annexi-strayline.com via ada-auth.org, Jan 8, 2022, 10:12 AM


And static checks (really no checks) for nested extensions are easy -- don't allow nested extensions! Janus/Ada has never supported them, and I've never had anyone complain. (And I've had people complain about lots of other things that aren't implemented.)

I have used a pattern that does this in one case. The general idea was to use the nested extension of a tagged type to implement some very specific extra data and some specific operation to complement that extra data. You can then implement this in the subprogram that needs to do said specific thing. In this case the goal was to have a single task sequentially "executing" each object of said type 'Class. Those subprograms would create this type extension, and then pass an object of that type to the task via a rendezvous. The point was that all (simple) operations of a certain kind needed to happen sequentially, but these operations could originate from essentially "any" subprogram throughout the subsystem. In this case, it made a lot of sense to use nested type extensions to keep the relevant code contiguous, which vastly improved maintainability and readability.

This was all a very specific, and albeit rare situation, but it was nonetheless an effective and elegant solution to the particular problem at hand.

To me though, this is really about something much larger. It is about the consistency in design. Ada has a beautifully regular and concise progression of package -> subprogram -> sequence of statements with these very regular notions of declarative parts and how they interact with scope. The concept that all declarative parts are essentially indistinguishable (ignoring private) is very powerful. I'm of the position that this regularity is so important that we ought to do everything we can to protect it.

For high integrity applications, it seems reasonable to have a new restriction that would prohibit "nested" (non-library-level) type extensions, but banning them to avoid complexity of implementation seems, to me, a regrettable approach.

Richard


Tucker Taft taft@adacore.com, Jan 8, 2022, 2:53 PM

On Sat, Jan 8, 2022 at 2:02 AM Randy Brukardt [randy@rrsoftware.com](mailto:randy@rrsoftware.com) wrote: Tucker wrote:

...

Note that in my recent proposal, you don't have to create an entry in the chain in every scope.  Only the scopes that declare nested extensions (which seems pretty rare!) for the nested-extension check, and in scopes where you declare nested access types (admittedly probably more common), in the accessibility checking case.  In any case, ideally the various approaches could be shown to be equivalent, so implementers could make their own efficiency tradeoffs.

Humm. I agree about the nested extension case (since every level is associated with an extension, either that of the class-wide type or of the one associated with the tag of the object), but it would seem that you would need a "master record" for every scope that declared as aliased object. Otherwise, what would you pass to make checks for anonymous access parameters?

You don't need to create a nested-access-type master record for each aliased object presuming you have a thread-local-storage pointer to the innermost such master record for the current task.  You use that master record pointer as is if the aliased object is declared in the same scope as the innermost nested access type.  If it is in a scope more nested than that, you just conceptually add/subtract 1 from the pointer to the master record to indicate it is a deeper value.  It will compare as deeper, and then if you mask off the extra bit you can still retrieve the task ID from the master record.   ...

Anyway, I think we are converging (at least somewhat) on how this can/should be implemented. It doesn't seem bad to me, except in unusual cases. It just can't use statically determined integers (always a bit dubious for a dynamically described check).

I think we should try to describe it in a way that makes minimal assumptions about how stack frames are laid out, how heap storage is managed, etc. and then give examples of how "actual" implementations might implement the proposed approach.  

                              Randy.

-Tuck  ...

Arnaud Charlet charlet@adacore.com via ada-auth.org, Jan 9, 2022, 4:54 AM

I think we should try to describe it in a way that makes minimal assumptions about how stack frames are laid out, how heap storage is managed, etc. and then give examples of how "actual" implementations might implement the proposed approach.

So if I summarize this thread: it started with: the current rules are too complicated and basically impossible to implement in practice (and no compiler will ever implement them), so: let's try to find a simpler solution. But this simpler solution turns out to be insufficient, so let's complexity it again and finally we're back to where we were initially: a complex solution which works on paper (we had that already 10 years ago) but which is just too complex and will never be implemented by any compiler in practice (and so we'll never even know if the solution itself is correct and complete, it likely isn't given the overall complexity, I'm ready to bet there are holes in what's being proposed that would require even more complexity).

The dynamic accessiblity checks have two major flaws:

Arno


Arnaud Charlet charlet@adacore.com via ada-auth.org, Jan 9, 2022, 4:57 AM

That's why I suggested a task-id/ptr pair for accessibility checks; that would be sufficient IFF we simply said that accessibility always fails when comparing items that belong to different tasks (regardless of nesting) -- of course, treating library-level as always working/failing (depending on the comparison). That would avoid any distributed overhead (there is a small distributed overhead from "master records" as they have to be initialized in any scope where something might need one [you explained that well, so I'll be handwavey here]). The check then is:       if Source'Accessibility_Location = null then -- Source is library-level          null; -- OK.       elsif Target'Accessibility_Location = null then -- Target is library-level          raise Program_Error; -- The source isn't library-level, so this always fails.       elsif Source'Task_Id = Target'Task_Id then           if Source'Accessibility_Location > Target'Accessibility_Location then               raise Program_Error; -- The souce has a shorter lifetime than the target.                   -- Note: I'm assuming the Intel downward growing stack. Reverse if                   -- if the stack grows upward.           -- else OK.           end if;       else           raise Program_Error; -- Belongs to two different tasks -- this would require some rule changes.       end if;

The above is a perfect illustration of my point: are you really finding it completely fine and appropriate to design a language requiring such complex checks (and part of the complexity is hidden in the required infrastructure behind it, in particular behind the magic "Accessibility_Location") to finally give a false sense of safety since most of the complexity is deferred at runtime and full of false positive exception raising?

I don't.

Arno


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 10, 2022, 7:33 PM

 ...

You don't need to create a nested-access-type master record for each aliased object presuming you have a thread-local-storage pointer to the innermost such master record for the current task.  You use that master record pointer as is if the aliased object is declared in the same scope as the innermost nested access type.  If it is in a scope more nested than that, you just conceptually add/subtract 1 from the pointer to the master record to indicate it is a deeper value.  It will compare as deeper, and then if you mask off the extra bit you can still retrieve the task ID from the master record.    Humm. Maybe that would work (I'd have to think a lot harder than I want to right now). But careful: You can't "mask off the bit" if the stack is growing downward (as in Intel processed) and you subtracted 1 rather than adding it. (You also are presuming master record addresses are word-aligned, probably a reasonable assumption but one that ought to be mentioned in a formal write-up).    ...

I think we should try to describe it in a way that makes minimal assumptions about how stack frames are laid out, how heap storage is managed, etc. and then give examples of how "actual" implementations might implement the proposed approach.   Makes sense. Steve will be happy to know that you've taken over this action item (was AI12-0016-1, new number to come) from him. :-)                                                        Randy.

Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 10, 2022, 8:24 PM

 Arnaud Charlet writes:

I think we should try to describe it in a way that makes minimal assumptions about how stack frames are laid out, how heap storage is managed, etc. and then give examples of how "actual" implementations might implement the proposed approach.

So if I summarize this thread: it started with: the current rules are too complicated and basically impossible to implement in practice (and no compiler will ever implement them), so: let's try to find a simpler solution. But this simpler solution turns out to be insufficient, so let's complexity it again and finally we're back to where we were initially: a complex solution which works on paper (we had that already 10 years ago) but which is just too complex and will never be implemented by any compiler in practice (and so we'll never even know if the solution itself is correct and complete, it likely isn't given the overall complexity, I'm ready to bet there are holes in what's being proposed that would require even more complexity).

We don't have an alternative, unless we are willing to completely abandon any safety and just make any place where an Ada 2005 accessibility check would fail erroneous. I doubt that would fly with the community as a whole (it might make sense to ask).

No one has proposed any true simplification that would work without substantial incompatibility, and I rather doubt that one exists. (Based on what Tucker has said, I think he feels the same way.)

The dynamic accessiblity checks have two major flaws:

Dynamic accessibility checks are clearly nonsense, and no one really wants them. But we need an alternative that doesn't break lots of existing code.

...

Having safe compile time rules (with some level of false positives) is fine, people can deal with early compile errors and once the compile compiles, you're safe ...

This of course is a motherhood-type statement; everybody wants the checks at compile-time. But we also have a body of Ada 95 and Ada 2005 code out there (Ada 2012 not having made much difference in this area), and generally we have not been allowed to introduce incompatibilities which mean that significant amounts of real code will not compile.

To eliminate the problematic dynamic checks, we'd have to put substantial restrictions on the usage of anonymous access parameters and stand-alone objects of an anonymous access type (SAOAATs) - both of which are defined to mostly use dynamic checks; and on nested extensions of tagged types. Given the Ada 95/2005 use of anonymous access parameters as a stand-in for in out parameters, it's likely that many of the possible restrictions would make a lot of existing code illegal. (Parts of Claw, for instance, would probably be illegal if static checks were required on anonymous access parameters.) There's also likely to be a lot of code that is formally buggy (as it could fail a dynamic accessibility check) that works fine in practice. The same is likely true with SAOAATs, but they're probably not used that much so I'm not particularly concerned with them.

For nested extensions of tagged types, the situation is even worse: eliminating dynamic checks would require never converting any object of the nested extension to the less-nested parent type or a class-wide version thereof (which would eliminate any dispatching or inheritance from that parent type). That is seems to be an impractical restriction (although it might allow Tucker's primary concern of allowing containers to be instantiated in nested scopes).

As such, I think it would be reasonable to define a restriction to eliminate dynamic checks, but it cannot be the only option -- people have to be able to use the dynamic checks. (This is similar to the situation for overriding indicators and the nonblocking contract, both of which should be required in general.) And that means we need a definition of the dynamic checks.

Do you have an alternative? Are you willing to introduce significant incompatibilities with earlier versions of Ada to solve this problem? If not, we have to properly define the dynamic checks, and the only simplification that has been proposed is to not allow cross-task accessibility. Which doesn't seem to be the part that bothers you.

                           Randy.


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 10, 2022, 8:48 PM

Arnaud Charlet writes:

That's why I suggested a task-id/ptr pair for accessibility checks; that would be sufficient IFF we simply said that accessibility always fails when comparing items that belong to different tasks (regardless of nesting) -- of course, treating library-level as always working/failing (depending on the comparison). That would avoid any distributed overhead (there is a small distributed overhead from "master records" as they have to be initialized in any scope where something might need one [you explained that well, so I'll be handwavey here]). The check then is:       if Source'Accessibility_Location = null then -- Source is library-level          null; -- OK.       elsif Target'Accessibility_Location = null then -- Target is library-level          raise Program_Error; -- The source isn't library-level, so this always fails.       elsif Source'Task_Id = Target'Task_Id then           if Source'Accessibility_Location > Target'Accessibility_Location then               raise Program_Error; -- The souce has a < > shorter lifetime than the target.                   -- Note: I'm assuming the Intel downward growing stack. Reverse if                   -- if the stack grows upward.           -- else OK.           end if;       else           raise Program_Error; -- Belongs to two different tasks -- this would require some rule changes.       end if;

The above is a perfect illustration of my point: are you really finding it completely fine and appropriate to design a language requiring such complex checks ...

If I was designing a new Ada-like language, it wouldn't need any dynamic checks, because I would require all types and packages to be declared at library-level. (I'd also extend overloading to all entities, require declaration of variables, blocking entities, and overriding, eliminate all anonymous types, eliminate Ada arrays and strings in favor of proper container and string abstractions, make protected types much more like records, and a number of other changes to reduce or eliminate painful rules/implementation, as well to generalize capabilities.)

But that's not what we're doing here. We are simply trying to figure out the simplest implementation for the language that was described 16 years ago and has barely changed (for this purpose) since. Reverting the rules to those of Ada 95 doesn't really fix anything (the Ada 95 rules had plenty of dynamic checks with all of the problems you've repeated [I mentioned a lot of them at the outset of the thread]). Fully static checks would seem to break lots of existing code, since they would require major restrictions on access parameters, SAOAATs, and a ban on extensions more nested than the parent type, all things added in Ada 2005 or before. It also probably would mean a severe breakage of the generic contract model (since one would have to make accessibility checks in generic bodies at instantiation time). If we're willing to take that level of incompatibility, then we might as well fix other Ada nonsense at the same time, and realize that we're making an Ada successor rather than another update.

But even if we're going to do that down the road, the current charge is to refine the current Ada standard, not to make major changes to it. So we can either decide on how dynamic accessibility should work, or continue to decide to not decide. Anything more significant is out-of-bounds (especially before we have set up our process for future enhancements).

                                           Randy.


Tucker Taft taft@adacore.com, Jan 10, 2022, 8:49 PM As far as dynamic accessibility checks, I think the approach AdaCore has been prototyping with a Restriction No_Dynamic_Accessibility_Checks is a good start.  We should be able to write up an AI on it in the next few months, I would hope.

We (AdaCore) have not made any effort to prototype eliminating dynamic nested-extension checks.  I would claim they are fundamentally a different problem, even if the notion of accessibility levels are also used for them.  I also think they are somewhat simpler to understand, and more likely to be implemented correctly.

-Tuck

...

Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 10, 2022, 9:21 PM

Tucker writes:

As far as dynamic accessibility checks, I think the approach AdaCore has been prototyping with a Restriction No_Dynamic_Accessibility_Checks is a good start.  We should be able to write up an AI on it in the next few months, I would hope.

I don't see how that helps for this discussion; an implementation still needs a (correct, hopefully) implementation of dynamic checks in the case when the restriction isn't used. And I think it is likely that there will be plenty of existing code (and ACATS tests!) that would be rejected if the restriction is in force, so it is not the sort of thing that could be treated as a replacement. (I'll be very happy to be proved wrong in this area!!)

We (AdaCore) have not made any effort to prototype eliminating dynamic nested-extension checks.  I would claim they are fundamentally a different problem, even if the notion of accessibility levels are also used for them.  I also think they are somewhat simpler to understand, and more likely to be implemented correctly.

The only ways to eliminate them is to either eliminate the nested extensions themselves (which is simple to describe and implement, but of course puts us back to the Ada 95 situation which was considered a problem), or only allow such extensions to use new code (no inherited routines, no explicit or implicit conversions to the parent type, probably any ancestor) -- which is essentially the same other than allowing some possible containers implementations. The root problem is that one cannot allow any object to have Parent or Parent'Class if its lifetime is shorter than Parent's. Otherwise, one can save such an object in a place that could outlive the shorter lived type. The dynamic checks allow that restriction to be deferred until someone tries to store the object in a place that might live too long (since the actual tag is used for a class-wide object, even if one is in an inherited routine that doesn't know anything about the actual tag).

As far as thinking that this is a simpler problem, I'm skeptical. It might be an easier problem in the sense that the accessibility can be part of the tag, so it's always obvious where to find it to test it, but that doesn't make the checks any easier (especially when one combines this with the coextension, build-in-place, and anonymous access rules).


Tucker Taft taft@adacore.com, Jan 10, 2022, 9:45 PM

On Mon, Jan 10, 2022 at 9:21 PM Randy Brukardt randy@rrsoftware.com wrote: Tucker writes:

As far as dynamic accessibility checks, I think the approach AdaCore has been prototyping with a Restriction No_Dynamic_Accessibility_Checks is a good start.  We should be able to write up an AI on it in the next few months, I would hope.

I don't see how that helps for this discussion; an implementation still needs a (correct, hopefully) implementation of dynamic checks in the case when the restriction isn't used. And I think it is likely that there will be plenty of existing code (and ACATS tests!) that would be rejected if the restriction is in force, so it is not the sort of thing that could be treated as a replacement. (I'll be very happy to be proved wrong in this area!!)

I guess the hope is that code that is performance sensitive could avoid use of cases that might involve dynamic checks, and that we could provide a canonical algorithm for the dynamic checks so implementations would not have to invent their own implementation from scratch.

We (AdaCore) have not made any effort to prototype eliminating dynamic nested-extension checks.  I would claim they are fundamentally a different problem, even if the notion of accessibility levels are also used for them.  I also think they are somewhat simpler to understand, and more likely to be implemented correctly.

The only ways to eliminate them is to either eliminate the nested extensions themselves (which is simple to describe and implement, but of course puts us back to the Ada 95 situation which was considered a problem),

I am not suggesting we completely disallow them.  Rather I believe the dynamic nested-exception checks might be tamed without major incompatibilities.

...

As far as thinking that this is a simpler problem, I'm skeptical. It might be an easier problem in the sense that the accessibility can be part of the tag, so it's always obvious where to find it to test it, but that doesn't make the checks any easier (especially when one combines this with the coextension, build-in-place, and anonymous access rules).

The checks are only on class-wide allocators (and the equivalent used for indefinite containers) and function return.  Function return is clearly simpler, because the tagged type has to outlive the function's declaration, which is known statically.

Class-wide allocators are the challenge, so the question is how easy would it be to tame them.  One simple restriction would be that anyplace where the level of the access type of the class-wide allocator is dynamic, the actual type must be at the same level as the root of the (designated) class-wide type.  My guess is that such a restriction would be relatively rarely violated in existing code, though of course some research would be needed.  And as hopefully illustrated in my proposed algorithm for a full implementation of nested-extension checks, they have fewer special cases so the canonical full implementation algorithm could also be simpler.

-Tuck 


Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 10, 2022, 10:58 PM

Tucker wrote:

...

As far as thinking that this is a simpler problem, I'm skeptical. It might be an easier problem in the sense that the accessibility can be part of the tag, so it's always obvious where to find it to test it, but that doesn't make the checks any easier (especially when one combines this with the coextension, build-in-place, and anonymous access rules).

The checks are only on class-wide allocators (and the equivalent used for indefinite containers) and function return.  Function return is clearly simpler, because the tagged type has to outlive the function's declaration, which is known statically.

Right (well, if "outlive" = "live as long as", since it's hard to imagine how anything could outlive a library-level function. and surely that can return an object of a library-level type), although the level of the tagged type is not necessarily known (if, for instance, it is from a class-wide parameter).

Class-wide allocators are the challenge, so the question is how easy would it be to tame them.

Careful: one can't adopt anything that would break a reasonable implementation of indefinite containers. We certainly want to be able to store objects in those, assuming that the type of the object lives as long as the container instance.

One simple restriction would be that anyplace where the level of the access type of the class-wide allocator is dynamic, the actual type must be at the same level as the root of the (designated) class-wide type.  My guess is that such a restriction would be relatively rarely violated in existing code, though of course some research would be needed.

Probably, but I don't see how that would help with any of the complex cases (like incomparable levels). Those can happen with allocators of statically declared types (as in a container instance inside a task), or of a function declared inside a task. And I don't see much point in "simplifying" rules just for the sake of doing so (and possibly causing pain for users at the same time).

And as hopefully illustrated in my proposed algorithm for a full implementation of nested-extension checks, they have fewer special cases so the canonical full implementation algorithm could also be simpler.

The special cases generally have to do with what accessibility level to use, which doesn't have much to do with how one needs to represent levels. While the problem has primarily been with the more complex representation of an accessibility level needed for Ada 2005 descriptions. (I've never been comfortable with the so-called "small integer" model, since it requires a series of hacks to make it work; I feel there is much less chance of confusion with a true dynamic level based on stack frames, as it HAS to match reality.) So I'm skeptical that this provides much useful simplification. (As always, feel free to prove me wrong!)

                             Randy.


Claire Dross dross@adacore.com via ada-auth.org, Jan 11, 2022, 3:23 AM

We don't have an alternative, unless we are willing to completely abandon any safety and just make any place where an Ada 2005 accessibility check would fail erroneous. I doubt that would fly with the community as a whole (it might make sense to ask).

I think we could propose 2 things together. We could have a pragma Extended_Accessibility_Checks or something like that which would apply to the previously dynamic accessibility checks. We could let users choose between 2 values, either Static or None. Users who are doing safety critical code would hopefully be able to use Static most of the time. None would be retro-compatible, in the sense that it would accept all the code previously accepted. Of course, the compiled code would no longer potentially raise an exception because of a failed dynamic accessibility check. But I think it is a benefit. From my experience with GNAT, whether or not some code will fail a dynamic accessibility check is rather random currently from a user perspective, and pretty loosely linked to what is required in the Ada RM. I remember writing a bunch of very simple test cases for dynamic accessibility checks for SPARK. I had to ask Tuck for the validity of each of them, and his answer did not match the implementation in GNAT half of the time...

--

Claire Dross Senior Software Engineer, AdaCore

Randy Brukardt randy@rrsoftware.com via ada-auth.org, Jan 12, 2022, 11:58 PM

Claire Dross writes:

We don't have an alternative, unless we are willing to completely abandon any safety and just make any place where an Ada 2005 accessibility check would fail erroneous. I doubt that would fly with the community as a whole (it might make sense to ask).

I think we could propose 2 things together. We could have a pragma Extended_Accessibility_Checks or something like that which would apply to the previously dynamic accessibility checks. We could let users choose between 2 values, either Static or None. Users who are doing safety critical code would hopefully be able to use Static most of the time. None would be retro-compatible, in the sense that it would accept all the code previously accepted. Of course, the compiled code would no longer potentially raise an exception because of a failed dynamic accessibility check. But I think it is a benefit. From my experience with GNAT, whether or not some code will fail a dynamic accessibility check is rather random currently from a user perspective, and pretty loosely linked to what is required in the Ada RM. I remember writing a bunch of very simple test cases for dynamic accessibility checks for SPARK. I had to ask Tuck for the validity of each of them, and his answer did not match the implementation in GNAT half of the time...

This could be a promising approach, but we'd have to work out a number of issues:

(1) Granulatity. Dynamic accessibility checks are used for a number of rather unrelated purposes. We all know about the access type checks, but they're also used in generic instances and for nested extensions. I think it is likely that some users would get different answers about the different purposes.

For instance, for me personally, 99% of my access types use either 'Unchecked_Access or Unchecked_Deallocation, so dangling pointers are always possible and the dynamic accessibility checks serve no purpose than to cause a hazard. So the increased erroneousness caused by setting this (presumably configuration) pragma to None would not really matter.

On the other hand, I don't use nested type extensions in normal practice, but it is easy for one to get used in a unit test or similar scenario. And it would be unlikely that I would even be thinking about the possibility of keeping an object of some test subprogram too long. Such a thing could cause false failures of a (buggy) unit test, which could be really hard to track down (and a waste of time, obviously). So for nested extensions I would certainly want Static checks.

So it would seem that we would need a number of choices in order to keep incompatibility under control. Of course, that would depend on the exact rules.

(2) Location of static errors. It wouldn't make sense to implement this exactly as you described it here, as that would ban all functions returning class-wide types and all allocators of types whose designated type is class-wide (since those are the places where there are dynamic checks). Similarly, you'd also be banning 'Access in all circumstances -- since every place that there is a static check there is a matching dynamic check (which is expected to always succeed in most cases). At a minimum, we'd have to figure out a definition of cases where the dynamic check cannot fail so that the safe cases could be allowed. For class-wide operations, that still would ban a lot of typical uses; it would be better to move the check to the point of the declaration.

My point here is that we'd have to figure out a detailed definition for the static checks; this isn't as easy as just saying that "every place were a dynamic check happens is illegal". We'd also have to define that every case that currently fails a dynamic accessibility check is erroneous; so this would not simplify the language description much (it would just push rules that are currently dynamic into Erroneous Execution paragraphs).

(3) Portability. For nested type extensions in particular, I worry that portability would a problem. Whether there was a problem with a use of a particular out-of-scope type depends on what the type uses from the nested scope, how tags are implemented in that compiler, how finalization affects the type, and more. It's quite possible that some compilers would generate code that works in these formally erroneous cases. That would be a portability problem, as they could very well not work on other compilers (especially those that have a dynamic component to their tag definition). That could effectively prevent some implementation techniques, and also could prevent detecting the problems (should an implementer want to retain the dynamic checks as a back-up in critical cases).

(4) Default. It would seem to be difficult to decide (both for the language definition and for a compiler implementer) what the default for this pragma should be, since this is essentially a choice between serious incompatibilities or serious chances of erroneous execution. It would be nice to always force the user to choose, but that itself would be a serious incompatibility (all existing code would need this new pragma added). It might be easier for an implementer to decide, but that too would be a portability problem (just like the default Assertion_Policy is a portability problem).

I think all of these could be worked out and/or mitigated. But it will be important to do so.

If we want to use this technique on existing compilers (rather than for Ada 2029 or whatever it will be), I think we'd have to have a third choice which represents the current (if badly implemented) situation. While we could introduce a pragma in a Binding Interpretation, I don't think we could have one that cancels a substantial part of the language. It also would be less controversial if we included dynamic checks as an option. With a pragma like this, we could agree that we will not make further ACATS tests of those checks (meaning that if your implementation passes the current ACATS tests, you are golden; note that the current ACATS tests only deal with relatively straightforward checks, none of the truly problematic cases are included). The effect would be to leave the current issues undecided and not worry about the (unlikely) cases that can't be handled with simple checks.


Claire Dross dross@adacore.com via ada-auth.org, Jan 13, 2022, 4:39 AM

I agree with your concerns. For (1), it needs to be investigated, but I hope we can find something meaningful. For (2), we have discussed this a lot at AdaCore recently, you could take a look at what we are currently prototyping in GNAT. The main advantage is that it is simple, but we did try to minimize incompatibilities when we could. For (4), I think it would be a shame to retain the (broken) dynamic rules if we can manage to avoid it. I think maybe None would be a good default as it would minimize compatibility issues, possibly with a warning if the default mode is used and the user has some code where a check would be issued with Static?

ARG-Editor commented 1 year ago

AI22-0076-1, AI22-0077-1, and AI22-0034-2 were all constructed in response to this issue.