!standard 4.8(10/1) 06-11-13 AI05-0024-1/01 !standard 3.10.2(14-14.3/2) !class binding interpretation 06-11-13 !status work item 06-11-13 !status received 06-08-29 !priority High !difficulty Hard !qualifier Error !subject Run-time accessibility checks !summary TBD. !question (1) In Ada 95, accessibility checks for access parameters could be implemented by passing as implicit parameter which was a small integer representing the static nesting level of the actual parameter. This model is explained in an Implementation Note in AARM 3.10.2(22.u/2-22.tt). In Ada 2005 however, it seems that things are more complicated and that a static nesting level is not enough. Apparently you need to pass an indication of dynamic nesting, roughly equivalent to a stack nesting depth. I suppose that a frame pointer corresponding to the master where the actual parameter was created could be used for that purpose, although the check seems rather nasty in the presence of tasking and multiple stacks. For instance, consider: declare type T1 is tagged null record; task T is entry E (X : T1'Class); end T; task body T is type Ref2 is access T1'Class; R2 : Ref2; begin accept E (X : T1'Class) do R2 := new T1'Class'(X); end; -- Delay for a while and then do nasty things with R2. end; procedure Proc is type T2 is new T1 with null record; X2 : T2; begin T.E (X2); end; begin Proc; end; This piece of code is squirreling away an object of type T2 into a task that outlasts the type. We don't want this. The problem appears to be that the accessibility levels of Ref2 and T2 are incomparable. Statically, the nesting levels are both 2, so the check of 4.8(10.1/2) succeeds. Dynamically, the situation is muddled because "run-time nesting of masters" doesn't seem to apply here: both T and Proc are (dynamically) nested in the outer block, but it's not like one of them is (dynamically) into the other. --- (2) The accessibility rules for objects created by allocators in 3.10.2(14-14.2) don't seem to cover derived types: type T (D : access Integer) is null record; type D is new T (new Integer'(3)); The allocator here is not quite used to define the constraint in a subtype_declaration so 3.10.2(14.1/2) doesn't apply. There also appears to be a rule missing for the following case: type A is access T; X : A (new Integer'(4)); It seems clear that the allocator is *not* being used to define the discriminant of an object, so the wording of 3.10.2(14.3/2) does not apply. !recommendation (See Summary.) !wording TBD. !discussion --!corrigendum A.18.2(239/2) !ACATS test !appendix From: Stephen W. Baird Date: Tuesday, August 29, 2006 2:51 PM Given two masters neither of which is statically nested within the other, the language does not define whether the accessibility level of one is deeper than that of the other. One implementation might assign a deeper accessibility level to the first master while another might not. This does not introduce portability problems as long as these values are never compared as part of a run-time accessibility check. Unfortunately, it appears that such a comparison is possible. RM 4.8(10.1/2) states: For any allocator, if the designated type of the type of the allocator is class-wide, then a check is made that the accessibility level of the type determined by the subtype_indication, or by the tag of the value of the qualified_expression, is not deeper than that of the type of the allocator. Consider the following example: declare type T1 is tagged null record; procedure Proc_1 (P : access T1'Class) is type Ref is access T1'Class; X : Ref := new T1'Class'(P.all); begin null; end; procedure Proc_2 is type T2 is new T1 with null record; X2 : aliased T2; begin Proc_1 (X2'access); declare type T3 is new T1 with null record; X3 : aliased T3; begin Proc_1 (X3'Access); end; end; begin Proc_2; end; There is an accessibility check associated with the evaluation of the allocator. Am I right in thinking that this check might either pass or fail the first time Proc_1 is called and, if the check passes the first time, it might pass or fail the second time Proc_1 is called? If so, then is this a problem? This check was introduced in order to prevent an allocated object from outliving its type. Note that there is no danger of that happening in this case. Similarly, RM 6.5(8/2) states: If the result type is class-wide, the tag of the return object is that of the value of the expression. A check is made that the accessibility level of the type identified by the tag of the result is not deeper than that of the master that elaborated the function body. We have essentially the same situation here, as illustrated by the following example: declare type T1 is tagged null record; function Func_1 (P : access T1'Class) return T1'Class is begin return P.all; end; procedure Proc_3 is type T2 is new T1 with null record; X2 : aliased T2; Y2 : T1'Class := Func_1 (X2'Access); begin declare type T3 is new T1 with null record; X3 : aliased T3 Y3 : T1'Class := Func_1 (X3'Access); begin null; end; end; begin Proc_3; end; The check was introduced in order to prevent a function from returning a value of a locally declared (specific) type. Again, there is no danger of that happening in this case. Is this just a case where the language is "insufficiently broken", or is some corrective action needed here? **************************************************************** From: Tucker Taft Date: Tuesday, August 29, 2006 5:38 PM > Given two masters neither of which is statically nested within the > other, the language does not define whether the accessibility level of > one is deeper than that of the other. I haven't investigated your example in detail, but the above statement is false, I believe. The language defines run-time accessibility checks in terms of *run-time* (dynamic) nesting, not static nesting. There are compile-time accessibility checks which are defined in terms of static nesting, but these are always backed up with run-time accessibility checks as well. > ... > ... One implementation might assign > a deeper accessibility level to the first master while another might > not. This does not introduce portability problems as long as these > values are never compared as part of a run-time accessibility check. From the language point of view, accessibility level checks are always related to dynamic nesting level. If implementations choose to use static nesting levels to simplify the checking, that is their business, but if true dynamic nesting levels need to be maintained, that is not a problem from the language correctness point of view (though admittedly it could be a performance issue). In at least one case in Ada 95, we already know some additional tricks need to be played to properly implement the run-time accessibility checks associated with access parameters using static nesting levels. See 3.10.2(22.dd/2). **************************************************************** From: Tucker Taft Date: Tuesday, August 29, 2006 7:45 PM In looking at your examples in more detail, the first example should never fail, and the second example should always fail. The access type of Proc_1's allocator in the first example is necessarily deeper (dynamically) than any type identified by the tag of its actual parameter. In the second example, it doesn't seem to matter whether you consider static or dynamic levels, since T2 and T3 are clearly deeper by both measures than Func_1. Unless I am missing something... (which is always possible when it comes to accessibility checks!). **************************************************************** From: Brad Moore Date: Friday, September 8, 2006 10:43 AM I have tried compiling and running the code in the original example and have noted an unexpected behavior. In the first example, the behavior is as was suggested. That is, the first call to Proc_1 does indeed does not fail, while the second nested call to Proc_1 does. In the second example using functions, the first call to Func_1 fails the accessibility check and generates a program_error. This was unexpected, as I don't see a significant difference here from the first example. Should this call have failed? **************************************************************** From: Pascal Leroy Date: Monday, September 11, 2006 4:29 AM As Tuck pointed out, both calls to Proc_1 should succeed, so the behavior of the first example is in fact incorrect. Note that the accessibility checks in question are new to Ada 2005, and that this is a very complicated part of the language, and one that we kept changing fairly late in the game. So I would not be surprised if there were still wrinkles to iron out in compilers in this area. What you report just looks like a bug to me, and your vendor will probably be interested to hear about it. It doesn't invalidate Tuck's analysis. **************************************************************** From: Brad Moore Date: Monday, September 11, 2006 1:41 AM Thanks for the explanation. I see now why the function returns failed in the second example. As to why the second nested call to Proc_1 in the first example might have failed, I note the following; RM 4.8 (5.1/2) states {AI95-00344-01} If the designated type of the type of the allocator is class-wide, the accessibility level of the type determined by the subtype_indication or qualified_expression shall not be statically deeper than that of the type of the allocator. Type T3 would be statically deeper than the Type Ref referenced in Proc_1, so according to this check, it seems the second call should fail. Type T2 however, would be statically just as deep as the Ref Type, (both are defined in procedures declared in the same block, even though hey are different procedures) so that test passes. Presumably the check described in RM 4.8(10.1/2), namely; For any allocator, if the designated type of the type of the allocator is class-wide, then a check is made that the accessibility level of the type determined by the subtype_indication, or by the tag of the value of the qualified_expression, is not deeper than that of the type of the allocator. is a dynamic accessibility check. It seems that a conservative approach is applied here and that both static and dynamic checks need to pass for such allocators. Does this make sense, or am I missing something? **************************************************************** From: Gary Dismukes Date: Tuesday, September 12, 2006 4:24 PM You're missing something. Your statement: > It seems that a conservative approach is applied here and that both > static and dynamic checks need to pass for such allocators. is correct, that both static and dynamic checks must pass, but your interpretation of how those checks are applied is flawed. Here's how these rules work. The rule 4.8(5.1/2) is a compile-time rule, and it applies to the types as named or resolved in the allocator, not to any types determined at run-time by the argument passed into Proc_1. In this case we have an initialized allocator (new T1'Class'(P.all)), where the type of P.all is T1'Class, and the accessibility of T1'Class of course matches that of the designated type since they're the same type. For the run-time check, what matters is the accessibility level of the type determined by the tag of the run-time value P.all, which in the example is either type T2 or type T3. Now accessibility levels are defined according to run-time "nesting", not static nesting. Therefore the level of the type of the allocator (Proc_1.Ref) is necessarily deeper than that of a type declared in any caller of Proc_1, so the run-time accessibility check also passes. **************************************************************** From: Brad Moore Date: Wednesday, September 13, 2006 9:04 AM Thanks again for the further clarification. I think I understand the rule now based on what you've said. RM 4.8 (5.1/2) is a subtle rule. Note, I think your explanation was on track but not quite accurate. > In this case we have an initialized allocator (new T1'Class'(P.all)), > where the type of P.all is T1'Class, and the accessibility of T1'Class > of course matches that of the designated type since they're the same type. The type of the left hand side really does not match the type as the right hand side of the assignment. I believe the types we are comparing is Ref vs T1'Class. My understanding now is that what we are comparing statically is the accessibility of the type of Ref vs the accessibility of the type of T1'Class To illustrate the subtlety consider a slight modification to example 1: In this case, the first allocation allocates a T2'Class and assigns to a T1'Class. The type T2 is still a statically a deeper type than T1, but the test passes. If I understand things correctly now, we still expect the assignment to compile and pass at run time because what we are really checking is that the accessibility of type T2'Class is not statically deeper than type Ref, which it isn't. The second allocation does not compile because the accessibility of T2'Class is statically deeper than the type XRef. Hopefully I've got it right now. Thanks again to all for looking at this. Example_3_Block : declare type T1 is tagged null record; type XRef is access T1'Class; Y1 : XRef := null; begin -- Example_3_Block Inner_Block : declare type T2 is new T1 with null record; procedure Proc_1 (P : access T2'Class) is -- Note P is now of type access to T2'Class instead of T1'Class -- type Ref is access T1'Class; X : Ref := new T2'Class'(P.all); -- Passes at run time in my -- compiler, which is OK I believe. begin -- Proc_1 Text_IO.Put_Line ("Doing Proc_1"); Y1 := new T2'Class'(P.all); -- Does not compile end Proc_1; procedure Proc_2 is X2 : aliased T2; begin -- Proc_2 Proc_1 (X2'Access); end Proc_2; begin -- Inner_Block Proc_2; end Inner_Block; end Example_3_Block; **************************************************************** From: Pascal Leroy Date: Wednesday, September 13, 2006 9:20 AM > The type of the left hand side really does not match the type as > the right hand side of the assignment. > I believe the types we are comparing is Ref vs T1'Class. > My understanding now is that what we are comparing statically > is the accessibility of the type of Ref vs the accessibility of > the type of T1'Class Yes, that's correct. The rules are fairly complicated, but their purpose is quite simple, and is explained in the AARM. For instance, AARM 4.8(5.1/2): "This prevents the allocated object from outliving its type". If T1'Class were deeper than Ref, an object in the storage pool associated with Ref could stay around while it type would have "disappeared" and that would cause all sorts of anomalies. **************************************************************** From: Gary Dismukes Date: wednesday, September 13, 2006 12:59 PM > The type of the left hand side really does not match the type as the > right hand side of the assignment. > I believe the types we are comparing is Ref vs T1'Class > My understanding now is that what we are comparing statically is the > accessibility of the type of Ref vs the accessibility of the type of > T1'Class You're correct, a slip in my description, the type that matters of course is the type of the allocator, not its designated type. > To illustrate the subtlety consider a slight modification to example 1: > In this case, the first allocation allocates a T2'Class and assigns to a > T1'Class. > The type T2 is still a statically a deeper type than T1, but the test > passes. > If I understand things correctly now, we still expect the assignment to > compile and pass at run time because what > we are really checking is that the accessibility of type T2'Class is not > statically deeper than type Ref, which it isn't. Almost right. The compile-time check is satisfied based on Ref vs. T2'Class, but the run-time check is that the accessibility level of the type denoted by P.all's tag is not deeper than Ref. In your latest test case the run-time succeeds for the call with X2'Access, but note that the value of P.all could originate from some type T3 declared at a deeper level than Ref, in which case the check would fail. > The second allocation does not compile because the accessibility of > T2'Class is statically deeper than the type XRef. Right, in that case the static check fails. **************************************************************** From: Pascal Leroy Date: Tuesday, September 19, 2006 3:40 AM > I haven't investigated your example in detail, but the above > statement is false, I believe. The language defines run-time > accessibility checks in terms of *run-time* (dynamic) > nesting, not static nesting. There are compile-time > accessibility checks which are defined in terms of static > nesting, but these are always backed up with run-time > accessibility checks as well. I agree that this is how the language has to work, but it has consequences that I, for one, had not noticed when we were designing these things. In Ada 95, accessibility checks for access parameters could be implemented by passing as implicit parameter which was a small integer representing the static nesting level of the actual parameter. This model is explained in an Implementation Note in AARM 3.10.2(22.u/2-22.tt). We were certainly using this model in our Ada 95 implementation. In Ada 2005 however, it seems that things are more complicated (surprised?) and that a static nesting level is not enough. Apparently you need to pass an indication of dynamic nesting, roughly equivalent to a stack nesting depth. I suppose that a frame pointer corresponding to the master where the actual parameter was created could be used for that purpose, although the check seems rather nasty in the presence of tasking and multiple stacks. It may be that this was obvious for everyone else, but I had not realized (1) that implementations needed to change in this area or that (2) the dynamic accessibility checks might be significantly more expensive in Ada 2005 than they were in Ada 95. Apparently the Editor didn't realize this either, because the above-mentioned implementation note should have been deleted. Am I confused or what? **************************************************************** From: Tucker Taft Date: Tuesday, September 19, 2006 8:31 AM ... > In Ada 2005 however, it seems that things are more complicated > (surprised?) and that a static nesting level is not enough. Apparently > you need to pass an indication of dynamic nesting, roughly equivalent to a > stack nesting depth. You might be right, though as I always say, it takes me a while to get my head around any question that relates to accessibility. Can you illustrate a case where you need to do something more than what we already need to do for access parameters, where there is a subtle adjustment made in the value passed at certain points (as explained in AARM 3.10.2 (22.dd/2))? I didn't think that Steve's example created such a situation. This presumes that the implementation doesn't consider the level passed in with an access parameter if static nesting already ensures the check couldn't fail, as suggested in AARM 3.10.2(22.ee/2). > ... > I suppose that a frame pointer corresponding to the > master where the actual parameter was created could be used for that > purpose, although the check seems rather nasty in the presence of tasking > and multiple stacks. > > It may be that this was obvious for everyone else, but I had not realized > (1) that implementations needed to change in this area or that (2) the > dynamic accessibility checks might be significantly more expensive in Ada > 2005 than they were in Ada 95. Apparently the Editor didn't realize this > either, because the above-mentioned implementation note should have been > deleted. > > Am I confused or what? I think (hope?) you might be confused. A couple of specific examples could determine it one way or the other. Do you have some? I'll set aside some quiet hours to ponder them if you think you do... **************************************************************** From: Robert A. Duff Date: Wednesday, September 20, 2006 7:17 AM >... I suppose that a frame pointer corresponding to the > master where the actual parameter was created could be used for that > purpose, although the check seems rather nasty in the presence of tasking > and multiple stacks. That way lies madness. If the RM requires that, then the RM is wrong. As you say, multiple task stacks make it "rather nasty" -- somewhat of an understatement, I'd say. ;-) I remember discussing such an implementation model with Tucker during the Ada 9X project. We both agreed that the task issue ruled it out. **************************************************************** From: Pascal Leroy Date: Thursday, September 21, 2006 7:51 AM > I think (hope?) you might be confused. A couple of specific > examples could determine it one way or the other. Do you > have some? I'll set aside some quiet hours to ponder them if > you think you do... I do hope that I am confused, but while trying to build various examples involving accessibility checks, I ran into the following: declare type T1 is tagged null record; task T is entry E (X : T1'Class); end T; task body T is type Ref2 is access T1'Class; R2 : Ref2; begin accept E (X : T1'Class) do R2 := new T1'Class'(X); end; -- Delay for a while and then do nasty things with R2. end; procedure Proc is type T2 is new T1 with null record; X2 : T2; begin T.E (X2); end; begin Proc; end; This piece of code is squirreling away an object of type T2 into a task that outlasts the type. We don't want this. The problem appears to be that the accessibility levels of Ref2 and T2 are incomparable. Statically, the nesting levels are both 2, so the check of 4.8(10.1/2) succeeds. Dynamically, the situation is muddled because "run-time nesting of masters" doesn't seem to apply here: both T and Proc are (dynamically) nested in the outer block, but it's not like one of them is (dynamically) into the other. Interestingly enough, this situation doesn't arise with access parameters (no such parameters for task entries) or for function returns (entries are not functions). So allocators seem to be the only problematic case. **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 9:08 AM We went out of our way to disallow access parameters in task entries, specifically because of this problem. But I wonder whether some kind of "trick" like that described in 3.10.2(22.dd/2) might have solved the task entry problem, and might also work here. Actually, the adjustment suggested in 3.10.2(22.dd/2) is to solve a different kind of problem, where an access parameter looks like it is too *deeply* nested. But perhaps some adjustment would accomplish the goal. The important thing is that a static accessibility level passed to an entry must make sense in the context of the entry. The only real problem is with levels associated with scopes inside the task body but outside the accept statement. I think a caller from outside the task body has to adjust the static accessibility level to be essentially infinitely deep if it doesn't outlive the task object whose entry is being called. The level isn't looked at at all on conversion to a type local to the accept statement, and the infinite level will clearly fail any accessibility check on conversion to a type local to the task body, but outside the accept statement. If the call comes from a task nested *inside* the task body, and it refers to a scope of the enclosing task body, then the "normal" static level can be passed. If it refers to a level inside the nested task, then it should be passed as infinity. Another way to put this is that if the level passed in refers to a scope shared with the called task, then no adjustment is necessary. If it refers to a scope that is not shared with the called task, then it should be set to infinity. Now how does this translate to passing objects of type blah'class? Normally I would think that we store an accessibility level inside the object itself. However, it seems that when calling a task entry, we will have to pass a separate accessibility level along with every class-wide parameter. Alternatively, we could move the check to the point of the entry call, so you wouldn't be allowed to pass an object of a type that doesn't outlive the task. That is probably preferable, since the caller is much more likely to know the level of the actual parameter passed in to an entry, and it avoids having to add implicit parameters to task entries. Of course it clearly requires a "binding interpretation," since we are adding a new place for an accessibility check. Also, it may interact badly with synchronized interfaces with procedures implemented as entries. We can put whatever adjustment is required into the compiler-generated wrapper, but it would be weird to insert an accessibility check into the compiler-generated wrapper, or to move the check to all calls on procedures of synchronized interfaces. Hmmmmm... **************************************************************** From: Pascal Leroy Date: Thursday, September 21, 2006 9:49 AM > Also, it may interact badly with synchronized interfaces with > procedures implemented as entries. We can put whatever > adjustment is required into the compiler-generated wrapper, > but it would be weird to insert an accessibility check into > the compiler-generated wrapper, or to move the check to all > calls on procedures of synchronized interfaces. That crossed my mind, and my brain started to ache. Since the problem is with allocators, I would try to fix it on the allocator, not on the subprogram call. In other words, complement the first sentence of 4.8(10.1/2) by something like: "Furthermore, if the allocator appears in a task that is deeper than the class-wide type, a check is made that the task is also deeper than the type determined by the subtype_indication, or by the tag of the value of the qualified_expression." In other words, the object created by the allocator must be of a type that outlives the task. This is a bit pessimistic, but what the heck! It's not worse than making access parameters illegal for entries. **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 11:05 AM Can't we limit this rule to allocators inside of accept statements where the initializing value is a formal parameter of an enclosing accept statement? **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 12:56 PM I can answer my own question: probably not, since you could pass the formal parameter to a subprogram nested inside the task. But that leads me to worry about generics. Suppose you instantiate a nice generic package inside a task body, and it declares extensions of types declared outside the task body. We don't want the generic to become unusable because of these required checks. I also think we can't ignore the function return case, since there could be a function called from the accept statement which turns around and returns the passed in class-wide object. My preference at this point would be to leave the rules as is, and let implementors experiment with solutions that get the *right* answer. This may mean that the run-time accessibility check associated with allocating and returning class-wide objects *is* more complex than a simple level comparison. Perhaps whenever a nested type extension is elaborated, some additional work needs to be performed to create a relative *dynamic* accessibility level (relative to the ultimate ancestor). For types that are at the same level as their ultimate ancestor, this level would always be zero. Even with this extra implementation work, I wonder whether the RM rules correctly cover the case of objects passed to task entries. Are even the *dynamic* accessibility levels comparable? Perhaps this notion of "infinitely deep" needs to be enshrined in RM words as well. Perhaps something analogous to what we do for anonymous access-to-subprogram should apply when passing an object of a nested type extension to a task entry if the type is declared outside of any scope that dynamically encloses the accept statement. Something like: When the accessibility level of an entity is determined from that of a master M1, then the level is considered deeper than that of any other master M2 that is not dynamically enclosed by M1, and whose enclosing task does not depend, directly or indirectly, on M1. AARM NOTE: This handles the case when M1 and M2 are in different "branches" of the overall task/master tree. Essentially if two masters are "incomparable" then the entities of one master are considered deeper than the other master, and vice-versa, so accessibility checks associated with allocators and function returns will fail. This is important because either master might outlive the other. **************************************************************** From: Randy Brukardt Date: Thursday, September 21, 2006 1:36 PM > My preference at this point would be to leave the > rules as is, and let implementors experiment with > solutions that get the *right* answer. This may > mean that the run-time accessibility check associated > with allocating and returning class-wide objects > *is* more complex than a simple level comparison. > Perhaps whenever a nested type extension is > elaborated, some additional work needs to be > performed to create a relative *dynamic* accessibility > level (relative to the ultimate ancestor). > For types that are at the same level as their ultimate > ancestor, this level would always be zero. While it might be the only answer, it certainly strikes me as yucky. > Even with this extra implementation work, I wonder > whether the RM rules correctly cover the case of > objects passed to task entries. Are even the > *dynamic* accessibility levels comparable? No, of course not. We're talking two different trees in this case. > Perhaps this notion of "infinitely deep" needs > to be enshrined in RM words as well. Perhaps > something analogous to what we do for anonymous > access-to-subprogram should apply when passing > an object of a nested type extension to a task > entry if the type is declared outside of any scope that > dynamically encloses the accept statement. > > Something like: > > When the accessibility level of an entity is determined > from that of a master M1, then the level is considered deeper > than that of any other master M2 that is not dynamically enclosed > by M1, and whose enclosing task does not depend, directly or > indirectly, on M1. > > AARM NOTE: This handles the case when M1 and M2 are in different > "branches" of the overall task/master tree. Essentially > if two masters are "incomparable" then the entities of one master > are considered deeper than the other master, and vice-versa, > so accessibility checks associated with allocators and function > returns will fail. This is important because either master > might outlive the other. Something like this seems to be necessary, but it really gives me indigestion. It means that you need to call the task supervisor in order to make an accessibility check (since that is where the relationships between task masters lives). Humm, I'm not sure it is right anyway. Do you really want this to work in nested tasks? It would be easier (both to describe and implement) if we simply defined the accessibility of masters that belong to different tasks to be incomparable, and that checks on incomparable items always fail. That would keep the task supervisor (or equivalent) out of it. (Saying M1 > M2 and M2 > M1 violates basic mathematics, and this stuff is hard enough to understand as it is without being "tricky") **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 2:04 PM ... > Humm, I'm not sure it is right anyway. Do you really want this to work in > nested tasks? It would be easier (both to describe and implement) if we > simply defined the accessibility of masters that belong to different tasks > to be incomparable, and that checks on incomparable items always fail. That > would keep the task supervisor (or equivalent) out of it. This is incompatible on face value, since the library level objects and types are all associated with the environment task, and we don't want to make the environment task's outermost master incomparable with every other task. But something like this might be possible. I think we only need to be talking about function return and allocators and nested type extensions. Perhaps nested type extensions of a given task can only be allocated for an access type declared in the same task. No, that doesn't really work. Suppose you have a generic containing a task body that declares an access-to-classwide type. It surely should be able to do allocators for its own local access type, even if the specific type is declared outside the task body. > (Saying M1 > M2 and M2 > M1 violates basic mathematics, and this stuff is > hard enough to understand as it is without being "tricky") I tried to be careful to say that the entities of one master are deeper than the other master, but I agree it is not obvious what is the best way to get what we want. **************************************************************** From: Pascal Leroy Date: Thursday, September 21, 2006 3:21 PM > My preference at this point would be to leave the > rules as is, and let implementers experiment with > solutions that get the *right* answer. Surely you are kidding. It's already hard enough to implement the language when it's correctly defined, we cannot expect implementer to design the language themselves. Observe that it's not only a matter of choosing a more-or-less efficient implementation strategy: it's really unclear what the "right" answer ought to be, how pessimistic we should be, how we can avoid incompatibilities. We have established that we have a nasty hole here, and that it seems to pervade the language (because as you point out pretty much anything can show up in a task body). We cannot just throw up our hands in despair: we have to do serious design work. Expect some headaches during the Atlanta meeting. > Even with this extra implementation work, I wonder > whether the RM rules correctly cover the case of > objects passed to task entries. Are even the > *dynamic* accessibility levels comparable? No, I was trying to point this out in my original message: the masters form a tree and a tree doesn't give you a total ordering. **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 3:55 PM >> My preference at this point would be to leave the >> rules as is, and let implementers experiment with >> solutions that get the *right* answer. > > Surely you are kidding. ... I originally thought you were pointing out an implementation problem only, namely that simple static accessibility levels weren't enough. It wasn't until later in writing my response that I realized there was a language hole. I agree that we don't want implementors to fix a hole by trial and error. What I meant was that if we had what we believe to be the "right" rules, then I am reluctant to try to change them if we don't know for sure we have an implementation problem. > We have established that we have a nasty hole here, and that it seems to > pervade the language (because as you point out pretty much anything can > show up in a task body). We cannot just throw up our hands in despair: we > have to do serious design work. Expect some headaches during the Atlanta > meeting. > >> Even with this extra implementation work, I wonder >> whether the RM rules correctly cover the case of >> objects passed to task entries. Are even the >> *dynamic* accessibility levels comparable? > > No, I was trying to point this out in my original message: the masters > form a tree and a tree doesn't give you a total ordering. Sorry, I didn't pick up on that initially. Yes I agree we need to fix this hole. I think it only affects the function return and allocator checks, and only on nested type extensions. Although I don't love the wording I suggested, we all seem to agree that these checks need to fail if they correspond to masters in separate branches of the task/master tree. I would hope if either master is "above" the other, then the checks should work the normal way, even if the masters happen to be in separate tasks. The implementation of the above rules are left as a detail to be worked out by the reader... ;-) **************************************************************** From: Tucker Taft Date: Thursday, September 21, 2006 5:00 PM If we take a cue from what we did with 'Class'Input and 'Class'Output, then a simple rule would be that you can't pass to a task entry with a class-wide parameter, an object whose tag identifies a type more nested than that of the task type. I think this may be roughly what Randy and/or Pascal have already suggested. This would have to be a run-time check when calling through a synchronized interface implemented by a task type (and it wouldn't apply to any "controlling" parameters since they can't fail). Presumably we would also define a legality rule when calling an entry "directly" when the actual is not of the same type as the formal. **************************************************************** From: Pascal Leroy Date: Friday, September 22, 2006 10:36 AM > This would have to be a run-time check when calling > through a synchronized interface implemented by > a task type (and it wouldn't apply to any "controlling" > parameters since they can't fail). But then the check would have to happen in the wrapper, right? We certainly don't want a distributed overhead on all calls through limited interfaces. Probably a dumb idea but... how about a post-compilation rule: if T'Class is used as a parameter of a task entry, there shall be no descendant of T more nested than the task unit. Not exactly a pretty rule, and probably a bit awkward for implementations, but then I would argue that a post-compilation check is more user-friendly than a run-time check that fails once in a blue moon. Also note that we probably have a similar problem for (named) access types designating T'Class: they too could be used to squirrel away an object that would outlive its type. Finally, am I right in believing that there no reason for disallowing access parameters of an access-to-subprogram type in entries, other than the fact that we forgot to relax this restriction? **************************************************************** From: Tucker Taft Date: Friday, September 22, 2006 11:47 AM >> This would have to be a run-time check when calling >> through a synchronized interface implemented by >> a task type (and it wouldn't apply to any "controlling" >> parameters since they can't fail). > > But then the check would have to happen in the wrapper, right? We > certainly don't want a distributed overhead on all calls through limited > interfaces. Hmmm, good point. I didn't think it was so bad to impose it on all calls through synchronized interfaces, but I agree it is overkill to impose it on all calls through limited interfaces. So I guess it would need to be in the wrapper, which I admit isn't desirable. > Probably a dumb idea but... how about a post-compilation rule: if T'Class > is used as a parameter of a task entry, there shall be no descendant of T > more nested than the task unit. Not exactly a pretty rule, and probably a > bit awkward for implementations, but then I would argue that a > post-compilation check is more user-friendly than a run-time check that > fails once in a blue moon. I would give this a triple "g" disgggusting. How about the following: perform an accessibility check when passing/copying a class-wide object whose root type is declared outside the task body, from within an accept statement to anything outside the accept statement. Require that the tag identify a type declared outside the task body. This check can use a static accessibility level. (By "copying" a task-wide object I mean using it as the initial value in an allocator.) > Also note that we probably have a similar problem for (named) access types > designating T'Class: they too could be used to squirrel away an object > that would outlive its type. Can you elaborate? The existing rules are designed to ensure that a named access type never outlives an object designated by one of its values. That's the point of the check on allocators. > Finally, am I right in believing that there no reason for disallowing > access parameters of an access-to-subprogram type in entries, other than > the fact that we forgot to relax this restriction? Yes, I think you are right; we could relax the restriction. But we might want to allow access parameters in general, if we end up solving the class-wide object problem, by imposing a similar limitation on converting/passing access parameters from inside an accept statement to something declared outside the accept statement. In both cases, we want to "carve out" the scopes within the task body, and require that once an object that might have come from the entry caller "escapes" from the static scope of the accept statement, it is guaranteed to live at least as long as the task type. I think this rule would be relatively straightforward to implement, and would almost certainly impose no significant limitation on the user, given the tendency to have very simple parameters to an accept statement. **************************************************************** From: Pascal Leroy Date: Tuesday, September 26, 2006 6:34 AM > > Probably a dumb idea but... how about a post-compilation rule: if > > T'Class is used as a parameter of a task entry, there shall be no > > descendant of T more nested than the task unit. > > I would give this a triple "g" disgggusting. I knew you would say this, but then I'm not in love with dynamic accessibility checks. They have the potential to fail infrequently, and they are not something that users are aware of. So they are not ideal for testability. > How about the following: perform an accessibility > check when passing/copying a class-wide object whose root > type is declared outside the task body, from within an accept > statement to anything outside the accept statement. Require > that the tag identify a type declared outside the task body. > This check can use a static accessibility level. Sounds reasonable. Of course, it's not really "within an accept statement", it's within anything that can be called from an accept statement, and that can see an access-to-T'Class declared in the task body. So pretty much any code in the task, as soon as the task plays games with class-wide types. By the way, don't we have a similar problem with coextensions? I believe that the following is legal with the RM as written, and that it can also cause trouble: type T1 is tagged null record; task type T (D : access T1'Class); type A is access T; procedure P is type T2 is new T1 with null record; X : A := new T (new T2)); begin null; end; It appears that D.all can outlive T2. > > Also note that we probably have a similar problem for (named) access > > types designating T'Class: they too could be used to squirrel away an > > object that would outlive its type. > > Can you elaborate? I was confused. > Yes, I think you are right; we could relax the restriction. > But we might want to allow access parameters in general, > if we end up solving the class-wide object problem, by > imposing a similar limitation on converting/passing access > parameters from inside an accept statement to something > declared outside the accept statement. That's an interesting idea, although it looks like a big change now that we are in Corrigendum mode. On the other hand, it would be silly to force implementers to perform checks for class-wide parameters without taking advantage of these checks for access parameters. **************************************************************** From: Tucker Taft Date: Tuesday, September 26, 2006 9:35 AM > ... >> How about the following: perform an accessibility >> check when passing/copying a class-wide object whose root >> type is declared outside the task body, from within an accept >> statement to anything outside the accept statement. Require >> that the tag identify a type declared outside the task body. >> This check can use a static accessibility level. > > Sounds reasonable. Of course, it's not really "within an accept > statement", it's within anything that can be called from an accept > statement, and that can see an access-to-T'Class declared in the task > body. So pretty much any code in the task, as soon as the task plays > games with class-wide types. I actually meant that the check would occur when calling a subprogram or allocating for an access type declared lexically outside of the accept statement. So I really meant "within an accept statement." Once you got lexically outside the accept statement, I didn't want any special rules to apply. Accept statements tend to be relatively short, and it seemed OK to impose fairly severe restrictions on what they could do with classwide parameters. > By the way, don't we have a similar problem with coextensions? I believe > that the following is legal with the RM as written, and that it can also > cause trouble: > > type T1 is tagged null record; > > task type T (D : access T1'Class); > type A is access T; > > procedure P is > type T2 is new T1 with null record; > X : A := new T (new T2)); > begin > null; > end; > > It appears that D.all can outlive T2. I'm not sure why you think it is legal. It seems to be a relatively clear (as these things go ;-) violation of 4.8(5.1): If the designated type of the type of the allocator is class-wide, the accessibility level of the type determined by the subtype_indication or qualified_expression shall not be statically deeper than that of the type of the allocator. In this case, "new T2" violates this rule, because its access type has the accessibility of the object allocated by the "new T" (per 3.10(14.3/2)), which in turn has the accessibility of "A" (per 3.10(14/2)), and T2 is statically deeper than A. ... >> Yes, I think you are right; we could relax the restriction. >> But we might want to allow access parameters in general, >> if we end up solving the class-wide object problem, by >> imposing a similar limitation on converting/passing access >> parameters from inside an accept statement to something >> declared outside the accept statement. > > That's an interesting idea, although it looks like a big change now that > we are in Corrigendum mode. On the other hand, it would be silly to force > implementers to perform checks for class-wide parameters without taking > advantage of these checks for access parameters. That was my feeling. Access parameters and nested extensions should sink or swim together. **************************************************************** From: Pascal Leroy Date: Tuesday, September 26, 2006 11:15 AM > I actually meant that the check would occur when calling a > subprogram or allocating for an access type declared > lexically outside of the accept statement. So I really meant > "within an accept statement." Once you got lexically outside > the accept statement, I didn't want any special rules to > apply. Accept statements tend to be relatively short, and it > seemed OK to impose fairly severe restrictions on what they > could do with classwide parameters. Hmm, but then these are indeed pretty severe restrictions. Preventing users from calling a (local) subprogram from within an accept statement on the odd chance that this subprogram would try to squirrel away the parameter of the entry is unappealing, to say the least. The only work-around would be for the user to replicate the code of the subprogram in the accept statement. Yuck. I absolutely hate it when we make subprograms unusable. After all subprograms are one of the most useful constructs in programming languages. So I'd rather perform the check at the point of the allocator, always, even if that means imposing a distributed overhead outside of accept statements. The overhead is not so bad because the checks are only needed if (1) the task has an entry with a parameter of type T'Class and (2) the allocator is for an access-to-T'Class type declared in the task body. > I'm not sure why you think it is legal. Because my favorite compiler thought it was legal, maybe? ;-) I agree that it's illegal, and my favorite compiler has now changed its mind. **************************************************************** From: Randy Brukardt Date: Tuesday, September 26, 2006 12:01 PM > Hmm, but then these are indeed pretty severe restrictions. Preventing > users from calling a (local) subprogram from within an accept statement on > the odd chance that this subprogram would try to squirrel away the > parameter of the entry is unappealing, to say the least. The only > work-around would be for the user to replicate the code of the subprogram > in the accept statement. Yuck. I absolutely hate it when we make > subprograms unusable. After all subprograms are one of the most useful > constructs in programming languages. > > So I'd rather perform the check at the point of the allocator, always, > even if that means imposing a distributed overhead outside of accept > statements. The overhead is not so bad because the checks are only needed > if (1) the task has an entry with a parameter of type T'Class and (2) the > allocator is for an access-to-T'Class type declared in the task body. Now I'm confused. What's "the task" here? You can't mean to require full program analysis, and this problem occurs for any classwide type. Besides, I have a hard time believing that there is anything special about a parameter of a classwide type: typically, it is a completely normal parameter -- I have to think that there are other ways to create this problem. Beyond that, I would be strongly opposed to any requirement that depended on the sort of parameters in otherwise unrelated subprograms/entries -- we have no such rules now, and for good reason: changing an unrelated parameter could suddenly make your program illegal, and far away from the parameter. So any rule would have to simply be an accessibility rule -- which is where we started this discussion. I think we need to fix the accessibility and let the chips fall where they may; we're going to have to fix the accessibility sooner or later, and random "patches" hardly ever work. **************************************************************** From: Tucker Taft Date: Tuesday, September 26, 2006 1:01 PM > Now I'm confused. What's "the task" here? You can't mean to require full > program analysis, and this problem occurs for any classwide type. Besides, I > have a hard time believing that there is anything special about a parameter > of a classwide type: typically, it is a completely normal parameter -- I > have to think that there are other ways to create this problem. I think what Pascal is suggesting is that we leave the rules roughly as is, but clarify that in a rendezvous, all of the masters dynamically enclosing the caller that do not also dynamically enclose the task type are considered deeper than the accept statement. I think there is still the issue of nested tasks, and whether they should have different rules. > Beyond that, I would be strongly opposed to any requirement that depended on > the sort of parameters in otherwise unrelated subprograms/entries -- we have > no such rules now, and for good reason: changing an unrelated parameter > could suddenly make your program illegal, and far away from the parameter. > So any rule would have to simply be an accessibility rule -- which is where > we started this discussion. I think we need to fix the accessibility and let > the chips fall where they may; we're going to have to fix the accessibility > sooner or later, and random "patches" hardly ever work. I think the "requirement" is simply on implementations. It doesn't change the rules, but it says that this more complicated check (from an implementation point of view) is only necessary under the circumstances Pascal identified. The allocator where the check would be needed must be statically enclosed within the task, so no "full program analysis" is required. **************************************************************** From: Randy Brukardt Date: Tuesday, September 26, 2006 3:40 PM I don't see this. If the nested extension "belongs" to the task (and the class-wide type does not), it would seem that the check is needed (as the dynamic nesting would be incompatible). That could be the case in an instance, for example (with the generic unit being separately compiled). So, it seems to be a general check. (And I don't see how you would write the rules such that it would only apply to a class-wide object passed as a parameter to an entry, but I think that is less important.) I can understand your "draconian" rules (they can be limited to the task body only), but I don't see how Pascal's could be (so it's equivalent to the original problem). **************************************************************** From: Tucker Taft Date: Tuesday, September 26, 2006 4:22 PM I am afraid I have lost track of what you mean by a "general check". Can you illustrate with one or more specific examples? I believe we are focusing on the check associated with an initialized allocator for an access-to-classwide type, where the initial value potentially comes from the caller in a rendezvous, and we might get the wrong answer if we did things the "normal" way (which I presumes involves using a static accessibility level stored in a place accessible from the tag(s) of the initial value). **************************************************************** From: Randy Brukardt Date: Tuesday, September 26, 2006 5:11 PM Sure, but I'm just asking why that check doesn't apply to the same example wrapped in a generic package and instantiated in the right place. The accessibility check in the generic body is a dynamic one (else nothing at all would be allowed), and it would seem to have the same problems as the "in-place" allocator you are worried about -- except that it is separately compiled. (Note that the type extension itself does not need to be in the generic body for this to happen -- it would be illegal for the type extension to be in the generic body, but it is OK in the spec.) I can write out the example if you insist, but I'd rather do real work. Or maybe thinking about accessibility has driven me to insanity... **************************************************************** From: Randy Brukardt Date: Tuesday, September 26, 2006 5:21 PM > I can write out the example if you insist, but I'd rather do real work. Having said this, I figured I better actually write it: type T is tagged ... generic package Gen is procedure Operation (Obj : in out T'Class); private type Acc_T_Class is access T'Class; end Gen; package body Gen is procedure Operation (Obj : in out T'Class) is P : Acc_T_Class := new T'Class'(Obj); begin end Operation; end Gen; Now, if you have an accept statement: accept E (O : in out T'Class) do declare package OK is new Gen; begin OK.Operation (O); end; end E; you seem to have the same problem as before, except now it is separately compiled (discounting generic macro expansion effects, which we don't have of course). So any sort of static check seems to be out, and a dynamic check means distributed overhead... **************************************************************** From: Tucker Taft Date: Tuesday, September 26, 2006 7:13 PM I understand your point, though the example you show I don't believe needs any special handling. I believe the special handling is only needed when dealing with types that are declared inside the task body but outside the accept statement. In any case, it does seem to be a particular challenge for shared generics. I would envisage that each non-limited tagged type might need an implicit dispatching operation that does the accessibility checks associated with allocators and function return. A type that is not a nested extension could do the straightforward thing. A type that is a nested extension would have to worry about the case of a type declared in a task body that has accept statements. These types could presumably be specially marked when declared, and checking against them would force the more complex check. **************************************************************** From: Pascal Leroy Date: Wednesday, September 27, 2006 1:23 AM > Besides, I have a hard time believing > that there is anything special about a parameter of a > classwide type: typically, it is a completely normal > parameter -- I have to think that there are other ways to > create this problem. Please show an example. "I have a hard time" is not a valid argument. Class-wide types are special for allocators and function returns. Access parameters are special for task entries. I don't find it too surprising that class-wide parameters are special for task entries. So far no other issue has been identified. **************************************************************** From: Pascal Leroy Date: Wednesday, September 27, 2006 1:57 AM > accept E (O : in out T'Class) do > declare > package OK is new Gen; > begin > OK.Operation (O); > end; > end E; > > you seem to have the same problem as before, except now it is > separately compiled (discounting generic macro expansion > effects, which we don't have of course). (As Tuck mentioned, you want the generic instantiation to be in the task body but outside of the accept statement; in the code above the access type goes away at the end of the rendezvous so there is no problem.) My view is that this case should be covered by the general rule in 4.8(5.1). It is not at the moment because are missing a rule that would say that the type identified by the tag of O has an accessibility level incomparable with that of the access type. If we add this rule, the allocator will fail the check and everything will be fine. > So any sort of > static check seems to be out, and a dynamic check means > distributed overhead... Well, since the check depends on the parameter, it has to be a runtime check. What I was trying to point out is that in many circumstances you can statically eliminate it: it's only needed if there are both class-wide parameters and local access-to-class-wide types in sight. In particular if you replicate generics, it's only ever needed inside the task. If you are using nested type extensions and you share generics, yes, there is some distributed overhead, but there is one anyway for the allocators/function returns check, independently of tasking (remember that you cannot determine on the generic if a formal type will be class-wide). My view is that the language should not make an universally shared implementation of generics infeasible, but it the rules require passing extra thunks, so be it. At any rate, I don't see the need for "full program analysis". **************************************************************** From: Randy Brukardt Date: wednesday, September 27, 2006 12:42 PM ... > > So any sort of > > static check seems to be out, and a dynamic check means > > distributed overhead... > > Well, since the check depends on the parameter, it has to be a runtime > check. What I was trying to point out is that in many circumstances you > can statically eliminate it: it's only needed if there are both class-wide > parameters and local access-to-class-wide types in sight. In particular > if you replicate generics, it's only ever needed inside the task. OK, but that's not what you wrote. Since you were responding to Tucker's "special check" idea, it appeared that you were proposing a different "special check". I think any "special check" is silly here; we just have to get the accessibility right and be done with it. I don't much care (in terms of defining the correct language rules) whether or not it can be statically eliminated if you have a 20 pass compiler and the moon is full. :-) That's just an implementation detail (and one that is impractical in many implementations, I suspect), and mentioning it confuses the issue more than it helps. > If you are using nested type extensions and you share generics, yes, there > is some distributed overhead, but there is one anyway for the > allocators/function returns check, independently of tasking (remember that > you cannot determine on the generic if a formal type will be class-wide). > My view is that the language should not make an universally shared > implementation of generics infeasible, but it the rules require passing > extra thunks, so be it. > > At any rate, I don't see the need for "full program analysis". The "special check" you seemed to be proposing did require it; but since you say that isn't what you meant, then I agree. You'd need a multi-pass compiler and full program analysis (or macro substitution for all generics and stubs) to take advantage of the static elimination that you suggest (making it impractical, I think), but since the whole thing is irrelevant to the proper rules, it doesn't matter. **************************************************************** From: Randy Brukardt Date: Wednesday, September 27, 2006 12:46 PM > Please show an example. "I have a hard time" is not a valid argument. > Class-wide types are special for allocators and function returns. Access > parameters are special for task entries. I don't find it too surprising > that class-wide parameters are special for task entries. So far no other > issue has been identified. Classwide types are just a normal kind of type, and they can be used in all of the normal ways. That means that there must be other ways to get class-wide objects into an entry. Perhaps they're all illegal for some other reason (I realize components are), but I'm dubious. No, I haven't found an example -- that's Steve's job. ;-) **************************************************************** From: Brad Moore Date: Thursday, September 28, 2006 2:12 PM > Please show an example. "I have a hard time" is not a valid argument. > Class-wide types are special for allocators and function returns. Access > parameters are special for task entries. I don't find it too surprising > that class-wide parameters are special for task entries. So far no other > issue has been identified. I am wondering if there might be a problem with protected objects as well. The following is very similar to the original example, except it involves a protected entry instead of a task. My compiler compiler currently crashes if the allocator isn't commented out, so I do not know if the compiler would have viewed this as legal or not. Protected_Example : declare type T1 is tagged null record; protected type P is procedure S (X : T1'Class); private R2 : access T1'Class; end P; protected body P is procedure S (X : T1'Class) is begin R2 := new T1'Class'(X); -- Compiler crashes if uncommented null; end S; end P; Prot : P; procedure Proc is type T2 is new T1 with null record; X2 : T2; begin Prot.S (X2); end Proc; begin -- Protected_Example Proc; end Protected_Example; **************************************************************** From: Brad Moore Date: Sunday, October 1, 2006 11:48 PM > I am wondering if there might be a problem with protected types as well. This is just an update regarding whether the accessibility check problem discussed in this thread also applies to protected types in addition to task types. The compiler issue I was having was quickly fixed by the vendor. Once this fix was in place, it was confirmed that the behavior is similar to the case for tasking. That is, using protected types, it is possible to squirrel way an access to a type that outlives the lifetime of the type being referenced. I have a rather light-hearted and somewhat graphic program below that also illustrates the problem. The program implements the same interface for a regular package, a tasking implementation, and a protected type implementation. The regular package implementation works as expected and prevents zombie squirrels, while the tasking and protected type implementations do not. This also demonstrate dispatching to a tagged type after the type's lifetime has expired, which is something that shouldn't be allowed to happen. Note: I would normally try to create a minimal example that strictly demonstrates the issue, and I admit I got carried away a bit here. Pascal's usage of the term “squirreling away” in his original example inspired me, and I guess I was having fun with the squirrel concept. My apologies to squirrel lovers everywhere. A further note, this does somewhat hit close to home, as in the mid 1960's a couple of non-native gray squirrels escaped from the local zoo, and now there are literally thousands of them all over our city. with Ada.Text_IO; use Ada.Text_IO; procedure test_accessibility_checks is package Chaos is type Critter is interface; function Species (Object : Critter) return String is abstract; type Cage is limited interface; procedure Put_In_Kennel (X : in out Cage; Y : in Critter'Class) is abstract; procedure Roll_Call (X : in out Cage) is abstract; function Cage_Type (X : in Cage) return String is abstract; end Chaos; ---------------------------------------------------------- package Edible_Straw_Cage is -- Squirrels have chewed a hole task type Straw_Cage is new Chaos.Cage with entry Put_In_Kennel (Y : in Chaos.Critter'Class); entry Roll_Call; end Straw_Cage; function Cage_Type (X : in Straw_Cage) return String; end Edible_Straw_Cage; package body Edible_Straw_Cage is task body Straw_Cage is R2 : access Chaos.Critter'Class; -- Squirrelable begin accept Put_In_Kennel (Y : in Chaos.Critter'Class) do R2 := new Chaos.Critter'Class'(Y); -- Compiler Run time -- accessibility check passes end Put_In_Kennel; accept Roll_Call do Put (R2.Species); end Roll_Call; end Straw_Cage; function Cage_Type (X : in Straw_Cage) return String is begin -- Cage_Type return "Straw (Tasking)"; end Cage_Type; end Edible_Straw_Cage; ---------------------------------------------------------- package Safe_Metal_Cage is -- Squirrel proof cage type Metal_Cage is new Chaos.Cage with private; overriding procedure Put_In_Kennel (X : in out Metal_Cage; Y : Chaos.Critter'Class); overriding procedure Roll_Call (X : in out Metal_Cage); overriding function Cage_Type (X : in Metal_Cage) return String; private type Metal_Cage is new Chaos.Cage with record R1 : access Chaos.Critter'Class; -- Squirrel proof end record; end Safe_Metal_Cage; package body Safe_Metal_Cage is procedure Put_In_Kennel (X : in out Metal_Cage; Y : Chaos.Critter'Class) is begin -- Put_In_Kennel X.R1 := new Chaos.Critter'Class'(Y); -- Compiler Run time accessibility check fails end Put_In_Kennel; procedure Roll_Call (X : in out Metal_Cage) is begin -- Roll_Call if X.R1 /= null then Put (X.R1.Species); -- Won't get called end if; end Roll_Call; function Cage_Type (X : in Metal_Cage) return String is begin -- Cage_Type return "Metal (Regular Package)"; end Cage_Type; end Safe_Metal_Cage; ---------------------------------------------------------- package Chewable_Plastic_Cage is -- Squirrels have chewed a hole protected type Plastic_Cage is new Chaos.Cage with procedure Put_In_Kennel (Y : Chaos.Critter'Class); procedure Roll_Call; function Cage_Type return String; private R3 : access Chaos.Critter'Class; -- Squirrelable end Plastic_Cage; end Chewable_Plastic_Cage; package body Chewable_Plastic_Cage is protected body Plastic_Cage is procedure Put_In_Kennel (Y : Chaos.Critter'Class) is begin R3 := new Chaos.Critter'Class'(Y); -- Run time accessibility check does not fail null; -- Compiler problems uncommenting out preceding line end Put_In_Kennel; procedure Roll_Call is begin -- Roll_Call if R3 /= null then -- Compiler Crash if uncommented Put (R3.Species); end if; null; -- Compiler problems commenting out preceding if statement end Roll_Call; function Cage_Type return String is begin return "Plastic (Protected Type)"; end Cage_Type; end Plastic_Cage; end Chewable_Plastic_Cage; ---------------------------------------------------------- procedure Test_Cage (Cage : in out Chaos.Cage'Class) is package Nasty is type Squirrel is new Chaos.Critter with null record; overriding function Species (Object : Squirrel) return String; end Nasty; package body Nasty is function Species (Object : Squirrel) return String is begin return "Zombie Squirrels"; end Species; end Nasty; Rocky_The_Flying_Squirrel : Nasty.Squirrel; begin -- Test_Cage Cage.Put_In_Kennel (Rocky_The_Flying_Squirrel); -- Dispatch on Cage Type Put_Line ("Squirrel from " & Cage.Cage_Type & " cage has been granted immortality!"); exception when others => Put_Line ("Squirrel from " & Cage.Cage_Type & " cage has been caught!"); end Test_Cage; Metal_Cage : Safe_Metal_Cage.Metal_Cage; Plastic_Cage : Chewable_Plastic_Cage.Plastic_Cage; Straw_Cage : Edible_Straw_Cage.Straw_Cage; begin -- test_accessibility_checks -- Dispatch some calls to see if any squirrels are still alive. -- They shouldn't be, because the Nasty Squirrel package is out of scope. Test_Cage (Straw_Cage); Straw_Cage.Roll_Call; -- By now, the squirrels lifetime should be over. -- Note: I think I should have been able to use the object prefix notation here. -- I suspect this is a compiler bug. Put_Line (" from " & Edible_Straw_Cage.Cage_Type (X => Straw_Cage) & " cage are running amok!"); Test_Cage (Metal_Cage); Metal_Cage.Roll_Call; Put_Line ("No squirrels running loose from " & Metal_Cage.Cage_Type & " cage."); Test_Cage (Plastic_Cage); Plastic_Cage.Roll_Call; -- By now, the squirrels lifetime should be over. Put_Line ("from " & Plastic_Cage.Cage_Type & " cage have returned!"); end test_accessibility_checks; -------------------------------------------------------------------------- The output is: Squirrel from Straw (Tasking) cage has been granted immortality! Zombie Squirrels from Straw (Tasking) cage are running amok! Squirrel from Metal (Regular Package) cage has been caught! No squirrels running loose from Metal (Regular Package) cage. Squirrel from Plastic (Protected Type) cage has been granted immortality! Zombie Squirrels from Plastic (Protected Type) cage have returned! **************************************************************** From: Pascal Leroy Date: Monday, October 2, 2006 5:37 AM > Note: I would normally try to create a minimal example that > strictly demonstrates the issue, and I admit I got carried away > a bit here. Pascal's usage of the term “squirreling away” in his > original example inspired me, and I guess I was having fun with > the squirrel concept. "To squirrel away" used to be a technical term in Ada 95: look it up in the index. Apparently a squirrel-hater has removed it in Ada 2005, although the term is still used in the AARM. **************************************************************** From: Randy Brukardt Date: Tuesday, October 3, 2006 2:21 PM I just looked in my original Ada 95 manual, and I can't find squirrel away" in the index. It wouldn't have surprised me if it had been indexed, but I can't find any evidence that it was. So I don't think that there was a "squirrel-hater". **************************************************************** From: Robert A. Duff Date: Tuesday, October 3, 2006 3:52 PM During Ada 9X, all silly jokes were relegated to the Annotated version of the reference manual. **************************************************************** From: Pascal Leroy Date: Thursday, October 5, 2006 8:22 AM I am looking at the accessibility rules for objects created by allocators in 3.10.2(14-14.2). These rules don't seem to cover derived types: type T (D : access Integer) is null record; type D is new T (new Integer'(3)); The allocator here is not quite used to define the constraint in a subtype_declaration so 3.10.2(14.1/2) doesn't apply. There also appears to be a rule missing for the following case: type A is access T; X : A (new Integer'(4)); It seems clear that the allocator is *not* being used to define the discriminant of an object, so the wording of 3.10.2(14.3/2) does not apply. **************************************************************** From: Gary Dismukes Date: Sunday, November 12, 2006 4:57 PM The discussion we had a while back about accessibility checks (initiated by Steve Baird's posting) led me to start thinking about cases that could pose problems for using static levels to implement the checks for allocators. I think there was general agreement that the intent is certainly that it should be possible to use static levels, and that if dynamic levels must be maintained this could lead to real difficulties (for example because of cases involving multiple task stacks). That segued into a discussion of problems related to accessibility checking within task entries with class-wide parameters, and how we might restrict those (discussion still not fully resolved AFAIK, to be continued I believe at the ARG meeting). Here I'm looking at a different sort of case, with no tasks involved. Consider this example that uses an anonymous access-to-subprogram type. procedure Test is type T1 is tagged null record; procedure Proc_1 (AP : access procedure (T1C : T1'Class)) is type Ref is access all T1'Class; type NT1 is new T1 with null record; XNT1 : NT1; begin AP.all (XNT1); end Proc_1; procedure Proc_2 is type Ref is access all T1'Class; X : Ref; procedure Indirect_Proc (T1C : T1'Class) is begin X := new T1'Class'(T1C); -- Should raise C_E if T1C'Tag = NT1'Tag end Indirect_Proc; begin Proc_1 (Indirect_Proc'access); -- Do something with X.all... end Proc_2; begin Proc_2; end; What's going on here is that a tagged object of a type (NT1) declared within procedure Proc_1 is passed back out of its scope in a call to Indirect_Proc, which then executes a class-wide allocator initialized to the passed NT1 object. The allocator is for an access type declared at a shallower level than type NT1. This is required to raise an exception, since it would create a reference to an object of a "dangling" tagged type. But how can we implement that check if we only use static levels? The static levels of types NT1 and Ref match. (For that matter, the passed procedure and access type could be declared at an even deeper static level.) Putting aside the question of how to cheaply determine a dynamic level (using a frame pointer being one possibility), there needs to be a way to associate the dynamic level with the object passed to Indirect_Proc. It seems that the level either needs to be provided as an extra parameter with class-wide parameters, as is done for access parameters, or else perhaps contained within the object itself (or obtained via the object's tag). This seems rather unpleasant, especially the need at all for dynamic levels, but offhand I don't see how it can be avoided for case like this. Note that I'm not pointing out any semantic problem with the RM rules here. This case is semantically well-defined. I'm just worried about how to go about implementing it. Also, as I mentioned above, there were concerns raised that if dynamic levels are needed then this create cause real problems for cases involving multiple task stacks. The question is, in view of this sort of example, is it still possible to retain an implementation model based on static levels? Maybe the above case could be handled by using a special "infinite" level value to avoid the need for using fully dynamic levels, but it still seems necessary to associate the level with the parameter, and that would appear to entail a possible distributed overhead, which seems unfortunate. Unless anyone can suggest an easy and efficient way to deal with cases of this kind, I'd like to see this issue added to the discussion of accessibility in task entries at the upcoming ARG meeting (assuming that's going to be on the agenda). **************************************************************** From: Randy Brukardt Date: Monday, November 13, 2006 2:15 AM Gary gives a thought-provoking example, and then poses the question: > The question is, in view of this sort of example, is it still possible to > retain an implementation model based on static levels? No. The static level implementation required some bizarre fix-ups in Ada 95 to work, and it doesn't surprise me in the least that we managed to break those when we relaxed various restrictions. But it probably is possible to retain that model in most cases. (Details left to the implementor.) > Maybe the above > case could be handled by using a special "infinite" level value to avoid > the need for using fully dynamic levels, but it still seems necessary to > associate the level with the parameter, and that would appear to entail > a possible distributed overhead, which seems unfortunate. The accessibility level of the type should be associated with the tag. A tag for a nested extension already has a dynamic part, so I don't see why there would be any significant hardship with having a dynamic accessibility level. The nasty thing would be figuring out when they are incomparable, but that probably could be done in a library routine. > Unless anyone can suggest an easy and efficient way to deal with cases of > this kind, I'd like to see this issue added to the discussion of accessibility > in task entries at the upcoming ARG meeting (assuming that's going to be on > the agenda). The idea that there is anything easy about accessibility checking makes me laugh uncontrollably! ;-) I do want to say that I'm not trying to think too deeply about this (it's 2 am, after all), so I might have missed something. But I don't see that we're going to say "gee, the accessibility checks are harder to do. Let's repeal the ability to have nested type extensions!!" And eliminating cases like the above some other way would probably make class-wide parameters (or anonymous access-to-subprogram types) unusable. So, I think implementers are just going to have to suck it up on this one. (Maybe I'll feel differently tomorrow...) **************************************************************** From: Robert A. Duff Date: Monday, November 13, 2006 8:59 AM While sleepwalking at 2:00 am, "Randy Brukardt" writes: > The accessibility level of the type should be associated with the tag. How about storing two things in the type descriptor (what the tag points at): - the frame pointer of the innermost enclosing master - a unique id for the current task Then check that the task id's are equal, and the frame pointers <=, and raise C_E otherwise. Does that make sense? It requires a contiguous stack. :-( **************************************************************** From: Pascal Leroy Date: Monday, November 13, 2006 10:26 AM > Then check that the task id's are equal, and the frame > pointers <=, and raise C_E otherwise. Does that make sense? I don't know much about the implementation of tasking, but I am under the impression that querying the current task id is an expensive operation if you are running on top of a thread library. **************************************************************** From: Randy Brukardt Date: Monday, November 13, 2006 5:26 PM Possibly true, but since the accessibility checks are comparing the levels of two tagged types, you would only need to get the task id when you are creating the tag (and only for nested extensions). It doesn't seem likely that the cost of creating tags is frequent enough to matter significantly. (The most likely case would be in containers instances, but those are relatively expensive anyway.) The actual accessibility checks would be fairly cheap, especially as you could include a marker for extensions that are not nested (they cannot fail these nested extension checks). So I think Bob's strategy would work well for nested extension accessibility checks (and would implement "incomparibility" easily). I haven't thought about the access type checks, but it seems to me that it would always be possible to cache the task id/stack frame at the point of the type declaration, and thus prevent needing to get the task id frequently. So this seems like a viable approach to investigate further. (I won't go so far as to say that it will have to work, because nothing with accessibility is ever as it seems...) **************************************************************** From: Robert A. Duff Date: Monday, November 13, 2006 6:54 PM > I don't know much about the implementation of tasking, but I am under the > impression that querying the current task id is an expensive operation if > you are running on top of a thread library. Good point. It is certainly possible (and desirable!) that current-task-id be efficient. But I have seen threading systems that get in the way of that. The other question is whether this scheme does what we want and/or what the RM currently requires. I'm not sure. **************************************************************** From: Tucker Taft Date: Monday, November 13, 2006 7:21 PM I think what Gary showed is that the challenge is not restricted to inter-task calls. The fundamental problem is when you need to communicate an accessibility level at run-time to a subprogram that is not within the scope of the caller. We see this problem in Ada 95 in 3.10.2(22.dd), where an outer subprogram passes along its access parameter to a local nested subprogram. This nested subprogram was not in the scope of the original caller, and as such the static accessibility level from the original caller is not particularly meaningful. In specific, if the actual is more nested than the outer called subprogram, 3.10.2(22.dd) suggests its accessibility level should be adjusted to match that of the outer called subprogram's outermost scope. This makes it meaningful to the nested routine being called. Unfortunately, unless we want all tagged types to be treated like access parameters, we cannot easily adjust an accessibility level as part of parameter passing. Whatever accessibility level information we have for a tagged type, it is presumably set when the tagged type is elaborated, and readily accessible from every object of the type. The most fundamental question is whether the tagged type is more nested than some ancestor. If not more nested, then of course no accessibility check associated with nested extensions can fail. And in the opposite case, where the type is more nested than some ancestor (we will call this a "nested extension"), if we are checking against the level of an access type or function declared at the library level, then the check is certain to fail. These two cases probably will account for 99% of all checks, and the remaining case where we have a nested extension, and the access type or function against which the check is performed is not declared at library level, can probably incur a fair amount of overhead without slowing down the program significantly. Let us presume that in any scope in which one or more nested extensions are declared, or an access type or function is declared against which a tagged-type-accessibility check might be performed, we create an accessibility "anchor" of some sort with a unique ID (such as its address), and arrange that each nested extension refers to this anchor. We then simplify another portion of the checks, because if the anchor ID matches that of the function or access type against we are checking, we know the check passes. We are now down to the cases where the anchor ID doesn't match that of the access type/function, and we need to check that the anchor ID of the nested extension outlives that of the access-type/function. If we presume that anchors are linked to the next longer-lived anchor, we can simply walk the chain of anchors starting at the anchor of the access-type/function, and if we don't run into the anchor referenced by the nested extension before we reach library level, then we raise an exception. Of course there is no real need for a library-level anchor; it can be represented by a null pointer. Note that these anchors approximate the "master records" that some run-time systems create on the stack to link frames containing task objects and/or finalizable objects. Since nested extensions may have finalization actions, and access-to-class-wide types generally have finalization actions as well (since the designated objects might have controlled parts), it might be natural to use a master record as the accessibility anchor. The only clearly new need for such a master record would be for a scope declaring one or more functions that return class-wide types. Creating the master record could be deferred until it is determined that one or more of the functions contain a return statement that needs to perform a dynamic tagged-type accessibility check. So to summarize the possible approach -- when entering a scope containing an access-to-class-wide, function-returning-classwide, or nested extension, an accessibility "anchor" or master record must be allocated on the stack, and initialized to point to the enclosing anchor/master record. When it is time to do a check, presuming the special cases mentioned above don't apply, we walk the anchor/master chain starting at that of the access-type/function until we find that of the nested extension (check then passes), or reach the library level (check then fails). It just might work... ****************************************************************