!standard 3.9.2(20.1/2) 11-03-17 AI05-0197-1/02 !standard 3.9.2(20.2/2) !class binding interpretation 10-02-08 !status work item 10-02-08 !status received 09-06-09 !priority Low !difficulty Medium !qualifier Error !subject Dispatching when there are multiple inherited operations !summary Dispatching calls the inherited concrete subprogram when multiple operations are inherited. !question AI05-0126-1 changes the wording of 3.9.2(20.2/2) to talk about "the corresponding operation of the parent type or progenitor type from which the operation was inherited" handles the cases that motivated that question. But what happens here? package Pack1 is type Int1 is interface; procedure Op1 (X : Int1) is null; -- (1) end Pack1; package Pack2 is type T2 is tagged null record; private procedure Op1 (X : T2); -- (2) end Pack2; with Pack1, Pack2; package Pack2.Pack3 is type T3 is new Pack2.T2 and Pack1.Int1 with null record; -- procedure Op1 (X : T3) is null; [inherited (1)] --private -- procedure Op1 (X : T3); [inherited (2)] end Pack2.Pack3; with Pack2.Pack3; procedure Proc4 is X : Pack3.T3; begin Pack2.Pack3.Op1 (X); end Proc4; Now there are two implicitly declared operations that could be considered as "the operation" in the new 3.9.2(20.2). At any point, only one or the other will be visible, but which one depends on whether the private part of Pack2.Pack3 is visible at that point. Applying 3.9.2(20.2) [as modified by AI05-126] means we have to determine which *one* we're talking about, so that we can determine "the parent type or progenitor type from which the operation was inherited" -- we can't call both. If it's the first one, then the call to Op1 is the same as a call on (1), which is null; if it's the second one, then it's the same as a call on (2). Which is intended? (A call on (2).) !recommendation (See Summary.) !wording Insert a new bulleted item after 3.9.2(20.1/3): * If the corresponding operation is a predefined operator then the action comprises an invocation of that operator; [Editor's note: This handles predefined "=", which is not inherited but rather constructed anew for each time.] Modify 3.9.2(20.2/2): * otherwise, the action is the same as the action for the corresponding operation of the parent type or progenitor type from which the operation was inherited. {If there is more than one such corresponding operation, the action is that for the operation that is not a null procedure, if any; otherwise, the action is that of an arbitrary one of the operations.} Add to the AARM note 3.9.2(20.a): For the last bullet, if there are multiple corresponding operations for the parent and progenitors, all but one of them have to be a null procedure. (If the progenitors declared abstract routines, there would have to be an explicit overriding of the operation, and then the first bullet would apply.) We call the non-null routine if one exists. [Editor's note: An alternative formulation for this wording would be to talk about the parent: "If there is more than one such corresponding operation, the action is that for the operation of the parent type, if any; otherwise, the action is that of an arbitrary one of the operations." This works because the parent operation is the only one that can be concrete. Not sure if this is better; rewording of the AARM Note would be required if this option is chosen.] !discussion Wording using "overriding" seems better than the proposed wording: "If there is more than one such corresponding operation, the action is that for the operation that overrides the others." But this does not work properly, as "overriding" depends on the order of implicit declarations. If the declarations don't occur at the same place, the later one overrides the earlier one (by 8.3(12)), even though the later one might be null. For example: package Pack1 is type Int1 is interface; private procedure Op1 (X : Int1) is null; -- (1) end Pack1; package Pack2 is type T2 is tagged null record; procedure Op1 (Y : Int1); -- (2) end Pack2; with Pack1, Pack2; package Pack1.Pack3 is type T3 is new Pack2.T2 and Pack1.Int1 with null record; -- procedure Op1 (Y : T3); [inherited (2)] -- (3) --private -- procedure Op1 (X : T3) is null; -- [inherited (1), overrides (3) by 8.3(12)] -- (4) end Pack1.Pack3; package body Pack1.Pack3 is Obj : T3; begin Op1 (X => Obj); -- (5) end Pack1.Pack3; For the call (5), (4) is the declaration that is called. That can be verified by using the formal parameter name X in named notation in the call; a call using parameter name Y is illegal as declaration (3) is hidden from all visibility. (4) is a null procedure, which we don't want to call in this case. Since (5) is a dispatching call, we call the body determined by the definition of the execution of a dispatching call. The wording using overriding would indeed call (4), which is not what we want. However, so long as we take care that the wording of the definition of the execution of a dispatching call is correct, we are OK (even for statically bound calls like this one). With proper wording, we still call the body of the operation declared at (2). That makes it important that we get this wording right. [Editor's note: An alternative solution would be to ban hidden primitive routines for interfaces. They're already illegal for abstract subprograms, so only null subprograms would be affected, and this case seems to be messier than it is worth. Then we could use the overriding wording.] --!corrigendum 3.9.2(20.2/2) !ACATS Test An ACATS C-Test similar to the example in the question should be constructed. !appendix !topic AI05-126 !reference AI05-126, 3.9.2(20-20.2) !from Adam Beneschan 09-06-09 !discussion I have a feeling AI05-126 still isn't quite right. I notice that the phrase in 3.9.2(20.2) was changed from "otherwise, the action is the same as the action for the corresponding operation of the parent type" to "otherwise, the action is the same as the action for the corresponding operation of the parent type or progenitor type from which the operation was inherited". I think that that change was necessary, or else this simple case couldn't be handled: package Pack1 is type Int1 is interface; procedure Op1 (X : Int1) is null; end Pack1; package Pack2 is type T2 is tagged null record; end Pack2; with Pack1, Pack2; package Pack3 is type T3 is new Pack2.T2 and Pack1.Int1 with null record; end Pack3; with Pack3; procedure Proc4 is X : Pack3.T3; begin Pack3.Op1 (X); end Proc4; Using the old wording, we'd run into problems when determining what happens during the call on the dispatching operation on an object of type T3. 3.9.2(20) wouldn't apply, since the Op1 operation is not "explicitly declared" for the type T3; 3.9.2(20.1) doesn't apply since no tasks or protected types are involved; so we have to apply 3.9.2(20.2), which referred to the "corresponding operation of the parent type". But the parent type is T2 and there is no corresponding operation. The new wording, which talks about "the corresponding operation of the parent type or progenitor type from which the operation was inherited" handles this case. But what happens here? package Pack1 is type Int1 is interface; procedure Op1 (X : Int1) is null; end Pack1; package Pack2 is type T2 is tagged null record; private procedure Op1 (X : Int1); end Pack2; with Pack1, Pack2; package Pack2.Pack3 is type T3 is new Pack2.T2 and Pack1.Int1 with null record; -- procedure Op1 (X : T3) is null; [inherited from Int1] --private -- procedure Op1 (X : T3); [inherited from T2] end Pack2.Pack3; with Pack2.Pack3; procedure Proc4 is X : Pack3.T3; begin Pack2.Pack3.Op1 (X); end Proc4; Now there are two implicitly declared operations that could be considered as "the operation" in the new 3.9.2(20.2). At any point, only one or the other will be visible, but which one depends on whether the private part of Pack2.Pack3 is visible at that point. Applying 3.9.2(20.2) [as modified by AI05-126] means we have to determine *which* *one* we're talking about, so that we can determine "the parent type or progenitor type from which the operation was inherited". If it's the first one, then the call to Op1 is the same as the call to the corresponding operation on Int1, which is null; if it's the second one, then it's the same as the call to the corresponding operation on T2, the one defined in Pack2. In this (statically tagged) case, it might make sense to use the one that's visible at the point of the call. From Proc4, we can "see" the null Op1 inherited from Int1, and we can't see the Op1 inherited from T2, so it *might* make sense that we can't directly call the latter one---although I thought that in cases like this the intent was that the one with the body should be called. (That's what would happen if an overriding Op1 were explicitly declared in the private part of Pack2.Pack3.) So even in the statically tagged case, I don't think the wording in AI05-126 leads to the desired result. But the dynamically tagged case is more confusing: with Pack1; procedure Proc5 (X : Int1'Class) is begin Pack1.Op1 (X); end Proc5; Assume Proc5 is called on an object of type T3. 3.9.2(20-20.2) is still supposed to apply, using the type identified by the controlling tag, and under the new wording we need to determine what "the operation" in the phrase "parent type or progenitor type from which the operation was inherited" means, and I'm not at all clear on what "the operation" is. The whole section deals with "a call on a dispatching operation", and I think "the dispatching operation" is the one declared in Pack1. Clearly it would make no sense to say that this is "the operation" for applying 3.9.2(20.2). So what is "the operation"? If we mean the one that's implicitly declared for the type identified by the controlling tag, i.e. T3---there are two of them, which one? (And note that it may not work to decree that it's the one that overrides the other, because I could mess it up with this:) package Pack1 is type Int1 is interface; private procedure Op1 (X : Int1) is null; end Pack1; package Pack2 is type T2 is tagged null record; procedure Op1 (X : Int1); end Pack2; with Pack1, Pack2; package Pack1.Pack3 is type T3 is new Pack2.T2 and Pack1.Int1 with null record; -- procedure Op1 (X : T3); [inherited from T2] --private -- procedure Op1 (X : T3) is null; -- [inherited from Int1, overrides the one inherited from T2 -- by 8.3(12)] end Pack1.Pack3; So I don't think the proposed wording in AI05-126 is adequate. **************************************************************** From: Steve Baird Sent: Thursday, March 3, 2011 3:28 PM > AI05-0197-1: Improve this wording (with help from Tucker). [Sorry, I > don't have any more detail in my notes than that; they say "Tucker > does not like this wording which" and little else.] Neither Tuck nor I could reconstruct the problem that we thought we had identified at the Tampa meeting, but I did find a couple of new issues while trying. Again, thanks to Randy and Tuck for their review and suggestions. Problem #1: Applying 3.9.2(20-20.3) to a dispatching call to the predefined "=" operator of a tagged type, it says Is it explicitly declared? No. Move to next bullet. Is it implemented via an entry/protected subp? No. Move to next bullet. This one begins with "otherwise", so it must be right! We use the parent's equality op and extension components do not participate in the result. Not good. Suggested fix: !wording (in addition to the existing wording for this AI) Insert a new bulleted item after 3.9.2(20.1/3): * If the corresponding operation is a predefined operator then the action comprises an invocation of that operator; This should not require any change to any compiler; this is just changing the wording to reflect what was obviously intended to begin with. Problem #2: Making an arbitrary choice among null procedures assumes that they are interchangeable, and leads to problems if they are not. Consider the following example: declare package Pkg1 is type Ifc is interface; procedure Op (X : in out Ifc) is null; end Pkg1; package Pkg2 is type T is tagged null record with Type_Invariant => Is_Valid (T); procedure Op (X : in out T) is null; function Is_Valid (X : T) return Boolean; end Pkg2; package body Pkg2 is ... end Pkg2; package Pkg3 is type D is new Pkg1.T and Pkg2.Ifc with null record; end Pkg3; begin ...; end; Does a dispatching call to Pkg3.Op where the tag of the controlling operand is Pkg3.D'Tag result in a call to Is_Valid? It seems like it depends on the "arbitrary" choice mentioned in this AI's new wording for 3.9.2(20.2/2), which defines the dynamic semantics of such a dispatching call: * otherwise, the action is the same as the action for the corresponding operation of the parent type or progenitor type from which the operation was inherited. {If there is more than one such corresponding operation, the action is that for the operation that is not a null procedure, if any; otherwise, the action is that of an arbitrary one of the operations.} If we flip a coin to decide which from among two candidates is the "corresponding operation ... from which the operation was inherited", and if exactly one of these two candidates includes a call to Is_Valid in its dynamic semantics, then it seems like we have a problem. Both here and when 8.3(12.3/2) says "one is chosen arbitrarily", we are relying on the premise that syntactically null procedures with appropriately conformant profiles are interchangeable with respect to dynamic semantics. One approach to this problem (which Randy can explain his views on in a separate message) would involve two steps: 1) In these two arbitrary-choice situations (3.9.2 and 8.3), we add a preference rule preferring operations inherited from a non-interface type over operations inherited from an interface type. 2) We take whatever steps are needed (possibly none?) in order to ensure that null procedures which are primitive ops of interface types really are interchangeable (e.g., we already disallow pre and post conditions for null procedures). This issue needs more work. **************************************************************** From: Randy Brukardt Sent: Friday, March 4, 2011 1:15 AM ... > Both here and when 8.3(12.3/2) says "one is chosen arbitrarily", we > are relying on the premise that syntactically null procedures with > appropriately conformant profiles are interchangeable with respect to > dynamic semantics. > > One approach to this problem (which Randy can explain his views on in > a separate message) would involve two steps: > > 1) In these two arbitrary-choice situations (3.9.2 and 8.3), > we add a preference rule preferring operations inherited > from a non-interface type over operations inherited from > an interface type. > > 2) We take whatever steps are needed (possibly none?) > in order to ensure that null procedures which are primitive ops > of interface types really are interchangeable (e.g., we > already disallow pre and post conditions for null procedures). > > This issue needs more work. I don't really have any views specifically on this problem, but the discussion of it has gotten me very concerned that there is something fundamentally wrong with the way we handle Pre/Post/Type_Invariant aspects on dispatching calls. I'll call these "contract aspects" in the discussion below; most of the points apply to all three of them. Tucker has explained that the contract aspects that apply to a particular subprogram body are always determined when that subprogram is declared. In particular, inheritance never changes the contract aspects of a subprogram body. That clearly works well on the body side of the contract; if the contracts changed with inheritance, it would be much harder to figure out what properties can be depended upon (or have to be preserved in the results). OTOH, it doesn't seem to work as well on the call side of the contract. The current rules say that the contracts depend on the actual body executed; the implementation is likely to be a wrapper around the "regular" body (if the contracts are normally enforced at the call site). This has some unfortunate effects when interfaces are "added" into an existing type hierarchy. For example, consider: package P1 is type T1 is tagged private; function Is_Valid (Obj : T1) return Boolean; procedure P1 (Obj : in out T1); private ... end P1; package P2 is type I2 is interface; function Is_Wobbly (Obj : I2) return Boolean is abstract; procedure P1 (Obj : in out I2) is null with Post'Class => Is_Wobbly (I2); procedure P2 (Obj : in I2) is null when Pre'Class => Is_Wobbly (I2); end P2; with P1, P2; package P3 is type T3 is new P1.T1 and P2.I2 with private; overriding function Is_Wobbly (Obj : T3) return Boolean; overriding procedure P2 (Obj : in T3); private ... end P3; with P1, P3; procedure Main is procedure Do_It (Obj : in P3.T3'Class) is begin Obj.P1; -- (1) Obj.P2; -- (2) end Do_It; O3 : P3.T3; begin Do_It (O3); end Main; The dispatching call to P1 at (1) will call P1.P1, inherited from type T1. This P1 has no postcondition, so Is_Wobbly will not be called on exit from P1. However, the following call to P2 at (2) will call P3.P2; it will inherit the precondition from the interface. Thus Is_Wobbly will be called on entrance to P2. If P1 returned a value for which Is_Wobbly is False (perhaps because the programmer forgot to override P1 for type T3), the *precondition* on the call to P2 would fail. But that is bizarre; given the postcondition matches the precondition for the type of the calls, it will be totally mysterious to the programmer as to why the failure is there. The only way to reason about these calls is to know the details of the contracts for all of the various possible routines involved -- that way seems to lead to madness! (And of course the code will most likely not be this easy to analyze). It is of course possible to create similar examples with the Type_Invariant aspect. These examples bother me so much simply because here we have a contract that we appear to be promising to enforce, yet we aren't enforcing it. Lying to the client (whether or not there is some logical reason for it) does not seem like a good policy. This is just one effect of these rules. More generally, these rules prevent any analysis of the contracts of a dispatching call -- by the programmer, by the compiler, or by any tool that doesn't have access to the complete source code of the program. That's because even if the entire program obeys a programming style rule to avoid the "funny" cases, a subprogram that hasn't even been written yet can add additional contracts (via any of the class-wide or specific contract aspects) -- and those would render any analysis wrong. One side effect of this that generating the contract aspects at the point of the call is not possible in general. That of course means that the compiler cannot eliminate checks of those aspects when they are not needed, nor point out when they are guaranteed to fail. It also means that wrappers are needed for the dispatching versions of routines if the compiler generates such aspects at the call site for statically bound calls. The rules for combining contracts (especially preconditions) also seem confusing at best. Adding a precondition to an overriding routine causes a *weaker* precondition; the original class-wide precondition no longer needs to be true. That does not seem helpful and surely makes analysis harder. All of these rules seem to vary from the usual model of dispatching calls. For instance, constraints can be thought of a weaker form of contract aspects (as preconditions involving only a single parameter, for instance). Ada 95 required constraints to match exactly (3.9.2(10/2) requires subtype conformance) for routines that override inherited routines (that is, possible dispatching targets). (Ada 2012 will of course extend this to subtype predicates.) Ada also has similar rules for other properties (including Convention, also in 3.9.2(10/2), and the No_Return aspect (6.5.1(6/2))). It would have had similar rules for global in/global out annotations had those been defined, and most likely would have similar rules for exception contracts as well. So why should contract aspects (Pre/Post/Type_Invariant) be different? If these are considered part of the profile, with suitable rules all of the bizarre cases go away. The easiest rule would be to require (full) conformance of contract aspects that apply to a subprogram. Effectively, only one (class-wide) precondition could apply to a dispatching call - the same precondition would apply to all of the subprograms that could be dispatched to. This sounds very limiting, but as Tucker has pointed out, such a precondition could be made up of other dispatching calls. (Sorry Tucker for using your argument against you... :-) If we were to make such aspects part of the profile, then it would seem that we should have similar requirements on access-to-subprogram types. That would not be strictly necessary, but note that the same sorts of issues apply to calls through access-to-subprogram types. These bother me far less (especially as wrappers might be required to deal with access-before-elaboration checks), but consistency seems valuable. We could use other somewhat weaker rules. One thing that bothers me about the "absolute" rule is that a lot of potentially dispatching routines are never called that way in practice. Such routines could have conventional specific contract aspects without problems. One could even imagine using Tucker's original rules (although that leaves Steve's problem unsolved) so long as there is the possibility of compile-time enforcement of a stricter rule so that at least "well-structured" dispatching calls could have their properties known at the call site. For me, the most important part of contract aspects is that they are known at the call site. This opens up the possibility of the compiler eliminating the checks (and even more importantly, warning when the contracts are known to fail) without having to know anything about the implementation of the subprogram. Denying this possibility to dispatching calls makes such calls a second-class citizen in the Ada universe, and reduces the contract aspects to little more than fancy Assert pragmas (only Type_Invariant does much automatically). Thus I think we need to reconsider this area. (As a side effect, such reconsideration may very well eliminate the problem that Steve was trying to fix.) **************************************************************** From: Tucker Taft Sent: Friday, March 4, 2011 9:43 AM After thinking more about this, I now agree with Randy that we have a problem. It arises whenever an operation inherited from an interface is overridden with an operation inherited from some other type. My conclusion is that it may be necessary for a new version of the inherited routine to be generated, so that the compiler can insert the additional checks implied by Pre'Class, Post'Class, etc. aspects inherited from the interface. This breaks the principle that you can always just reuse the code when you inherit an operation, but I believe in the presence of multiple inheritance of class-wide aspects, we don't really have a choice. **************************************************************** From: Randy Brukardt Sent: Friday, March 4, 2011 9:46 PM I'm happy to hear that; I'd hate to think that I was making less sense than Charlie Sheen... However, I don't think this quite works, because of the "weakening" rules for preconditions. The new precondition inherited from the interface could "counterfeit" the precondition on the original body, leading to a scenario where the body is called without problem with parameters that violate the precondition that it knows about. That seems pretty nasty. To give a specific example: package P1 is type T1 is tagged private; function Is_Valid (Obj : T1) return Boolean; procedure P (Obj : in out T1) with Pre'Class => Is_Valid (Obj); private ... end P1; package body P1 is procedure P (Obj : in out T1) is begin -- Code here that assume Is_Valid is True for Obj. end P; end P1; package P2 is type I2 is interface; function Is_Wobbly (Obj : I2) return Boolean is abstract; procedure P (Obj : in out I2) is null with Pre'Class => Is_Wobbly (I2); function Something_Wobbly return I2 when Post'Class => Is_Wobbly (Something_Wobbly'Result); end P2; with P1, P2; package P3 is type T3 is new P1.T1 and P2.I2 with private; overriding function Is_Wobbly (Obj : T3) return Boolean; overriding function Something_Wobbly return T3; private ... end P3; with P1, P3; procedure Main is procedure Do_It (Obj : in P3.T3'Class) is begin Obj.P; -- (1) end Do_It; begin Do_It (P3.Something_Wobbly); end Main; Using the new semantics Tucker suggested, the call at (1) has to pass its precondition, as Is_Wobbly(Obj) has to be true (based on the postcondition of Something_Wobbly). However, since preconditions are effectively combined with "or", Is_Valid(Obj) might in fact be False. And if it is, the body of P is going to be mighty surprised to get an object that violates its precondition! (I don't think this problem happens for Post or Type_Invariant, as they are "anded", nor would it happen if the precondition was described as a Dynamic_Predicate.) Also note that this "weakening" means that even Pre'Class that necessarily must apply to all calls cannot be generated at the call-site (because of the possible need to "or" it with some other precondition) -- which eliminates the ability of the compiler to do much in the way of checking elimination. (Again, this is not a problem with the other aspects.) It seems that "weakening" doesn't apply to multiple inheritance as much as it does to the "primary" inheritance. But that doesn't seem to lead to a rule that makes much sense, as it would seem to treat progenitors different than interface parents (something we've avoided in the past). The easy fix would be to combine the preconditions with "and". But I realize there are logical issues with that on the call side of the equation. It strikes me that there are logical issues on one side or the other whenever contract aspects are combined; they only make sense if there is only one. Thus a radical solution would be to require that exactly one precondition apply to each subprogram (either Pre or Pre'Class, possibly inherited). To support combining and "weakening", we'd need a way to refer to the aspects of a parent routine, so that they can be used in a new Pre aspect. An attribute would work, something like: P'Parent_Pre. That would mean that you couldn't change Pre'Class precondition; if you need to do that, you'd have to use a Pre on each subprogram in the inheritance. Not sure if that is too complicated. And you couldn't assume much about the calls if you did that (rather than using a single Pre'Class), but as that is the current state, I can hardly imagine that it is harmful. Anyway, more thought is needed. **************************************************************** From: Jean-Pierre Rosen Sent: Saturday, March 5, 2011 1:26 AM > Using the new semantics Tucker suggested, the call at (1) has to pass > its precondition, as Is_Wobbly(Obj) has to be true (based on the > postcondition of Something_Wobbly). However, since preconditions are > effectively combined with "or", Is_Valid(Obj) might in fact be False. > And if it is, the body of P is going to be mighty surprised to get an > object that violates its precondition! Doesn't Eiffel have the same problem? How is it handled? (Just trying to avoid reinventing the wheel). **************************************************************** From: Randy Brukardt Sent: Saturday, March 5, 2011 2:14 AM I had wondered the same thing, but am not sure if Eiffel has interfaces or some other form of multiple inheritance. (Without that, this particular problem cannot come up.) **************************************************************** From: Bob Duff Sent: Saturday, March 5, 2011 7:32 AM Eiffel has multiple inheritance. Not just interfaces -- you can inherit two non-abstract methods that conflict, and there's some syntax for resolving the conflict (combine them into one, rename one or both, ....). I haven't had time to understand the issue you guys are talking about, so I don't know how it relates to Eiffel. One possibly-related thing is that in Eiffel preconditions are checked by the caller ("as if", of course). **************************************************************** From: Tucker Taft Sent: Saturday, March 5, 2011 10:28 AM Eiffel has full multiple inheritance. **************************************************************** From: Jean-Pierre Rosen Sent: Saturday, March 5, 2011 11:26 AM > I had wondered the same thing, but am not sure if Eiffel has > interfaces or some other form of multiple inheritance. (Without that, > this particular problem cannot come up.) Eiffel has full multiple inheritance (Bertrand Meyer has claimed that no language is usable without full multiple inheritance - but that was before interfaces) ****************************************************************