!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 ARG Approved 8-0-0 11-03-17 !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. [Editor's note: Discussion of Problem #2 continues in AI05-0247-1.] ****************************************************************