!standard 3.9(13-15) 07-05-25 AI05-0057-1/00 !class binding interpretation 07-05-25 !status work item 07-05-25 !status received 07-05-19 !priority Medium !difficulty Medium !qualifier Omission !subject The class attribute of a constrained subtype !summary !question 3.9(13-15) says: For every subtype S of a tagged type T ... the following attributes are defined: S'Class S'Class denotes a subtype of the classwide type ... for the class rooted at T. S'Class is unconstrained. However, if S is constrained, then the values of S'Class are only those that when converted to the type T belong to S. However, S'Class is an unconstrained type by 3.7(26). Thus, 4.6(51/2) does not apply, and thus there is no check that a converted value actually belongs to S. Similarly, 4.5.2(30/2) says that a membership returns True if the value satisfies any constraints of the named subtype. But a classwide type doesn't have any constraints (it's unconstrained) nor is there any definition of satifies for them. Something is seriously wrong here. !recommendation (See Summary.) !wording !discussion The idea of a type conversion to an *unconstrained* type raising *Constraint*_Error for a *constraint* violation is enough to make me sick and dizzy. It also seems to add a third kind of "partially constrained" type which invalidates predicates like Is_Constrained in compilers (and ASIS). A brief compiler survey with a simple test program shows that there is no consistency in the handling of this case: GNAT gets bogus error messages; with those fixed it makes no checks and memberships don't check the discriminants; Janus/Ada makes no checks and memberships doesn't check the discriminants; IBM Rational makes checks in some cases and not in others; Steve Baird claims that the checks are just accidents of a case not considered at all; Sofcheck makes checks in most cases, but not in memberships or type conversions (there also was a private report that it gives various compile-time errors in the AppletMagic version, so it may be even be consistent). --!corrigendum A.4.3(26/2) !ACATS Test !appendix [This thread was split from AI05-0032-1, as it is a separate issue.] From: Randy Brukardt Sent: Saturday, May 19, 2007 12:07 AM Tucker writes: > Here is the AI on allowing the type of the return object > in an extended_return_statement to be a specific type when > the result type is class-wide. While writing this, I bumped > into what seems to be a longstanding hole in 4.6 (Type > Conversions). Given two subtypes of T'Class, S1'Class > and S2'Class, we don't ever seem to check that when converting > from S2'Class to S1'Class, that the operand satisfies > the constraints of S1. See the AI discussion section for > more on this. A related hole seems to exist in 6.5 > (Return Statements), where if the result subtype is > S1'Class, we allow the return object to be declared of > subtype S2'Class, and again never check against S1. > I tried to fix this latter hole, but ended up just > presuming that 4.6 did the right thing, which it doesn't. > So another AI is probably needed to address the 4.6 hole. I don't understand your hole. I don't think there is any hole in 6.5, at least before your proposal. 6.5(5.2/2) requires static matching of the return object subtype if the result subtype is constrained; so if the result subtype S1'Class has a constraint, an object subtype of S2'Class is illegal unless it has the same constraints. If the result subtype doesn't have a constraint, there is nothing to check. Humm, 4.6(51/2) says that after the conversion, there is a check that the value satisfies any constraint of the target subtype if it is constrained. What more do you need? (If, for some bizarre reason, S1'Class is not considered constrained, then it has no real constraints, because pretty much all of the rules of the language are written "if constrained, then blah blah". It would effectively act like was unconstrained. While I'd prefer this interpretation, I don't think it was what was intended - I remember *losing* that argument during Ada 95. And having constraints on unconstrained subtypes seems like the first step on the road to madness...) **************************************************************** From: Tucker Taft Sent: Saturday, May 19, 2007 3:22 PM The problem is that all classwide subtypes have unknown discriminants, and are therefore considered to be *un*constrained. It is true that they might have a constraint on some of the discriminants, but there might be others that are unconstrained. Perhaps we could fix the hole by simply stating that such class-wide subtypes are indefinite, yet constrained, subtypes. But that is not currently a possible combination. **************************************************************** From: Randy Brukardt Sent: Tuesday, May 22, 2007 3:10 PM Whatever we do would be a new combination; I worry that the wording of the standard is not up to handling such combinations (not to mention compilers). I'd like to make the argument that the current wording of the language is just fine (even if it isn't quite what the designers intended). I thought that there were some ACATS tests that insisted on checks, but when I looked I wasn't able to find them. (Since I'm pretty sure that Janus/Ada doesn't make any such checks, any such tests should show up easily.) As such, I'd like to do a compiler survey in order to figure what compilers currently in these sorts of cases. Otherwise, we'll not know whether compatibility is a real issue here, and what (if anything) the language ought to be compatible with. Its probably too close to the meeting to get results from such a survey before the meeting, so I'll try to do it afterwards (this issue isn't yet on the agenda anyway). **************************************************************** From: Tucker Taft Sent: Tuesday, May 22, 2007 5:57 PM The Ada_Magic-based compilers (ours, Green Hills, and Aonix) certainly check any discriminant constraint associated with the root subtype of a class-wide subtype, in case you are making a table. Anything else would be highly surprising to the user, I would think. The RM seems clearly broken in this area. For example: type T(D : Integer) is tagged ... subtype S1 is T(1); X : S1'Class := T'(D => 2, ...); will definitely raise Constraint_Error with our compiler. **************************************************************** From: Randy Brukardt Sent: Tuesday, May 22, 2007 6:49 PM A classwide type essentially means to allow everything that's possible, now and in the future, for a type. That's inconsistent with a constraint. Moreover, Ada does not allow partially constrained types (for good reason, I think). For these reasons, I happen to think that using 'Class on a subtype where you can see the unconstrained type should be illegal. (The rule could be very similar to what we did to fix general access subtypes.) Cases where you can't see the subtype also couldn't fail any checks and thus would cause no surprise. [This happens to correspond to the cases where you can avoid writing a constrained subtype.] An attempt to partially constrain a type is a bug, and that's what this is. Sadly, I suspect such a rule would be too incompatible to seriously consider. [If I'm wrong, that's *my* proposal for this issue...] > The RM seems clearly broken in this area. For example: ... And I find it just too weird that an unconstrained type would raise Constraint_Error. And I seriously doubt that the language rules (any of them) allow for the possibility. After all, your original problem in 6.5 is a artifact; there is supposed to be a static matching check on anything with a constraint (thus eliminating any need for a runtime check). That doesn't happen because S'Class is unconstrained. But rather than fixing that or fixing the 6.5 wording so the static matching check will cover it, you want to change to a runtime check. That doesn't make sense to me: why should this one goofy case be different? It seems to me that fixing this somehow will require major surgery to the standard (as is necessary to fix 6.5 properly) and possibly to compilers, yet I don't see any indication that this is an important capability or even that it is widely implemented. Perhaps I'm wrong, but I want to get evidence of that one way or the other before making a decision. **************************************************************** From: Pascal Leroy Sent: Wednesday, May 23, 2007 7:29 AM > type T(D : Integer) is tagged ... > > subtype S1 is T(1); > > X : S1'Class := T'(D => 2, ...); For the record our compiler don't raise C_E here. I must say that I have a hard time following Tuck's reasoning. The RM is fairly clear: 3.7(26) states that "A tagged class-wide type also has unknown discriminants. Any subtype of a type with unknown discriminants is an unconstrained and indefinite subtype". So S1'Class is unconstrained and indefinite and there is no reason to raise C_E. In other words, as far as I can tell there is no difference between S1'Class and T'Class. Surely we are not going to introduce subtypes that are partially constrained. That would be an earthquake for the language and for implementers, and it wouldn't really bring any benefit to users. **************************************************************** From: Tucker Taft Sent: Wednesday, May 23, 2007 7:47 AM I agree this capability is not essential, but it has been a part of the language since 1995. It is discussed quite explicitly in 3.9(13-15) in the Ada 95 RM (and unchanged in the Ada 2005 RM): For every subtype S of a tagged type T ... the following attributes are defined: S'Class S'Class denotes a subtype of the classwide type ... for the class rooted at T. S'Class is unconstrained. However, if S is constrained, then the values of S'Class are only those that when converted to the type T belong to S. I don't see how it could be more explicit. Clearly other parts of the manual, in particular 4.6, failed to implement this requirement properly. I don't see it as a particularly big job to fix, since the wording above is so clearly explicit about what it means. And although both Randy and Pascal claim that the language doesn't have "partially constrained" subtypes, it clearly has classwide subtypes as described above, so perhaps they should be called "partially unconstrained" ... ;-). There is still the issue about whether we want to require static matching or introduce a run-time check. In poking around, I found a precedent in the rules for X'Access, which require static matching for constrained untagged types, but allow looser "coverage" rules for tagged types, with a view conversion providing a run-time check whenever there isn't static matching. See RM 2005 3.10.2(27/2-27.2/2, 30). Of course all of this depends on fixing up 4.6 so it does the right thing on a view conversion to S'Class. I think the following would probably do the trick: Add another sentence to the end of 4.6(51/2): ... If the target subtype is a discriminated class-wide subtype S'Class, and S is constrained, then a check is performed that the discriminants of the converted value satisfy the constraint. **************************************************************** From: Edmond Schonberg Sent: Wednesday, May 23, 2007 2:06 PM >> type T(D : Integer) is tagged ... >> >> subtype S1 is T(1); >> >> X : S1'Class := T'(D => 2, ...); > > For the record our compiler don't raise C_E here. Also for the record, GNAT rejects the code, indicating that the confusion here is widespread! Part of the GNAT code tries to find an actual constrained subtype for this class-wide object, and another part rejects the constraint on the grounds that it has an already constrained subtype.... Given the very explicit text in 3.9 (15) I think that Tuck's reading is inescapable. **************************************************************** From: Randy Brukardt Sent: Wednesday, May 23, 2007 5:48 PM Pascal writes: ... > Surely we are not going to introduce subtypes that are partially > constrained. That would be an earthquake for the language and for > implementers, and it wouldn't really bring any benefit to users. Not to mention unconstrained types that have constraints raise Constraint_Error - a complete twisting of English and logic. I'm glad to see that I'm not completely nuts :-) and others also find this distasteful. Tucker replies: > I agree this capability is not essential, but it has > been a part of the language since 1995. And it was wrong and misguided there, too. > It is discussed > quite explicitly in 3.9(13-15) in the Ada 95 RM (and > unchanged in the Ada 2005 RM): > For every subtype S of a tagged type T ... the following > attributes are defined: > S'Class S'Class denotes a subtype of the classwide type > ... for the class rooted at T. S'Class is > unconstrained. However, if S is constrained, > then the values of S'Class are only those that > when converted to the type T belong to S. > I don't see how it could be more explicit. Clearly other > parts of the manual, in particular 4.6, failed to implement > this requirement properly. I would argue that it is essentially the *entire* manual that fails to implement this. I haven't been able to find any text in the Standard that takes such "partially constrained" types into account. The closest I've found is membership operations, but those talk about constraints on the subtype, while the above text talks about values (the S'Class subtype does not appear to have constraints as defined by the language). There's no definition of the constraints on such a type, there is no definition of compatibility with other constraints and satisfaction of values for these types, nor anything else that I can see. > I don't see it as a particularly big job to fix, since the wording above > is so clearly explicit about what it means. Except that that this definition doesn't match any of the other semantics definitions of the language. At a very least, there has to be a recognition of the fact that this does *not* define a constraint, but still acts like one. Ed says: > Given the very explicit text in 3.9 (15) > I think that Tuck's reading is inescapable. I surely hope that the simple existence of some text doesn't mean we have to adopt a wrong-headed rule. After all, Ada 95 had text which clearly defined an incomplete type as a different type than its completion (and thus making all useful programs containing incomplete types as illegal). We didn't try to fix up this explicit wording, we simply replaced it by "view" terminology. The questions about this rule are: (1) Is this rule of any benefit to users? (Not much, if any. I think coextensions are more likely to be useful in practice, and I think *those* are very unlikely to be useful.) (2) Is anyone depending on this rule? (Seems unlikely, given that GNAT crashed when it came up) (3) How high is the implementation cost of this rule? (Very high, at least for some implementations where it would destroy all of the invariants.) (4) Is the rule going to make the language easier to understand and/or use? (Arguable either way. An unconstrained type that raises Constraint_Error isn't going to help anyone's understanding, but silently ignoring constraints isn't ideal, either.) The Gnat results suggest that we could adopt a relatively radical solution without too much user pain. (User's can't be depending on code that crashes the compiler!) But I don't want to argue anything without having done a more formal compiler survey. I'll try to get that initiated ASAP. **************************************************************** From: Edmond Schonberg Sent: Wednesday, May 23, 2007 7:24 PM Small correction: the compiler does not crash, it rejects the code with a rather contorted error message. If the solution you have in mind is somehow to make this code illegal, then indeed GNAT is already there. The error message could be improved.... **************************************************************** From: Tucker Taft Sent: Wednesday, May 23, 2007 11:02 PM It is of course hard to debate how much of an earthquake something is for anyone's compiler, but I can tell you why this isn't a big deal for ours. Class-wide types are pretty much a special case everywhere the appear in our compiler, so for conversion, the special handling implied by 3.9(15) is no big deal. I wouldn't leap to conclusions about the GNAT result. The example I concocted was artificial. It would be rare to create such an obvious constraint violation. More interesting would be a case where an object of subtype S1'Class is initialized from a parameter of subtype T'class, or a function call returning subtype T'Class. The question is whether GNAT performs a constraint check in that case. procedure Test(X : T'Class) is Y : S1'Class := X; begin ... The reason I don't think this creates much of an "earthquake" in the manual is that in almost all interesting contexts, we use "subtype conversion" to capture the need for constraint checks. So if subtype conversion does the right thing for classwide subtypes, then most other parts of the manual will follow along. I might add that I would have expected the big Ada 2005 "earthquake" in this general vicinity to be that access types now can be "constrained" in two independent ways, one with a null exclusion, and one (for access-to-composite) with a constraint on the designated subtype. Semi-constrained class-wide subtypes seems like small potatoes compared to that... ;-) **************************************************************** From: Randy Brukardt Sent: Thursday, May 23, 2007 12:18 AM > It is of course hard to debate how much of an earthquake > something is for anyone's compiler, but I can tell you > why this isn't a big deal for ours. Class-wide types are > pretty much a special case everywhere the appear in our > compiler, so for conversion, the special handling implied > by 3.9(15) is no big deal. But that's backwards. Even though there are literally hundreds of places to check [that turns out to be an overestimate, there are 63 calls of Is_Classwide in Janus/Ada - although it is likely that there are some cases not protected with such a call - Randy - May 25th], that isn't what worries me. What worries me is that such a subtype would necessarily look identical to a constrained subtype, and that would cause breakage everywhere. For instance, what does Is_Unconstrained return for such a type? Either True or False is likely to be wrong in some contexts. The only way to implement this in our compiler would be something like (the following is pseudo-Ada, of course): subtype S1 is T(1); subtype S1'Class is T'Class(1); A constraint is one kind of node, and a type (like T'Class) is another kind of node, chained together. (Duplicating all of the constraint code in the class node is just too disgusting to contemplate, and then we'd have a forest of similar types which would usually compare equal, which would damage other invariants.) You couldn't tell from the constraint whether it was "normal" or one of these special thingy's. > I wouldn't leap to conclusions about the GNAT result. > The example I concocted was artificial. It would be > rare to create such an obvious constraint violation. > More interesting would be a case where an object of > subtype S1'Class is initialized from a parameter of > subtype T'class, or a function call returning subtype > T'Class. The question is whether GNAT performs a > constraint check in that case. > > procedure Test(X : T'Class) is > Y : S1'Class := X; > begin > ... I think code like this is brain-damaged. Why in heaven's name would you try to force a failure like this? I can vaguely imagine using S1'Class in a parameter specification in order to define some sort of contract, but not inside of a procedure to essentially violate the contract given the user. In any case, I wish you had sent this a half-hour ago when I was still writing the test; now I'll have to retract it and write another one... Finally, my understanding of the Gnat problem is that it failed trying to look for the constraint in S1'Class. I doubt that it will matter where it is used for that case. > The reason I don't think this creates much of an > "earthquake" in the manual is that in almost all > interesting contexts, we use "subtype conversion" > to capture the need for constraint checks. So if > subtype conversion does the right thing for classwide > subtypes, then most other parts of the manual will > follow along. I don't buy this. There is a lot more here than just constraint checks; we need to define memberships, "satisfaction", and probably a number of other things. And we'd still have the insanity of an unconstrained type raising Constraint_Error. > I might add that I would have expected the big Ada 2005 > "earthquake" in this general vicinity to be that access types > now can be "constrained" in two independent ways, one with > a null exclusion, and one (for access-to-composite) > with a constraint on the designated subtype. Semi-constrained > class-wide subtypes seems like small potatoes compared > to that... ;-) Null exclusion doesn't have any runtime baggage (it's just a single bit), so it can be duplicated in every access type and subtype and not worried about beyond that. I do suspect that checking the rules for reconstraint will be a mess and probably buggy forever, but at least they are in a single place, not more than a half-dozen places like discriminant checks/memberships/etc. **************************************************************** From: Randy Brukardt Sent: Thursday, May 23, 2007 12:51 AM [Note: Version 1 of the test is not posted here, as no one other than me used it. - RLB] Over on the ARG list, we're having a debate about the semantics of classwide types. I've created a test program to determine what existing compilers do (from what has been discussed so far, that seems to be all over the map). If you have access to a compiler other than GNAT, we'd appreciate it if you'd run this test program and send us the results. (You can post them here or send them privately to me.) I only exclude GNAT because pretty much every one has a version, and we already have results on several versions of it. I've put the test program as an attachment in hopes that Outlook won't wrap the lines incorrectly. If you have trouble with the attachment, please drop me a line personally and I can send the test another way. ------------------- with Ada.Text_IO; procedure Chk_AI57 is -- Check the behavior of an Ada compiler on classwide constrained -- subtypes. -- Edit History: -- 5/23/07 - RLB - Created test. -- 5/24/07 - RLB - Added function result cases to reduce the -- chance of compile-time detection. type Root(D : Integer) is tagged null record; subtype S1 is Root(1); subtype S2 is Root(2); type Der(C : Integer) is new Root(C) with null record; procedure Check_It_1 (Obj : in S1'Class) is begin if Obj.D = 1 then null; -- Not interesting. else null; -- More info. at call site. --Ada.Text_IO.Put_Line ("-- No check on constrained " & -- "classwide parameter subtype"); end if; end Check_It_1; procedure Check_It_2 (Obj : in Root'Class) is begin null; end Check_It_2; O1 : Root(1) := Root'(D => 1); O2 : Root(2) := Root'(D => 2); OD1 : Der(1) := Der'(C => 1); OD2 : Der(2) := Der'(C => 2); subtype Obj_Kind is Natural range 1 .. 4; function Factory (Kind : Obj_Kind) return Root'Class is begin case Kind is when 1 => return O1; when 2 => return O2; when 3 => return OD1; when 4 => return OD2; end case; end Factory; begin Ada.Text_IO.Put_Line ("--- Check behavior of constrained classwide " & "types (version 2)"); begin Check_It_1 (O1); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (root)"); end; begin Check_It_1 (O2); Ada.Text_IO.Put_Line ("-- No discrim. check on constrained " & "classwide parameter subtype (root)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide parameter subtype (root)"); end; begin Check_It_1 (OD1); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (der)"); end; begin Check_It_1 (OD2); Ada.Text_IO.Put_Line ("-- No discrim. check on constrained " & "classwide parameter subtype (der)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide parameter subtype (der)"); end; begin declare X : S2'Class := Root'Class(O1); begin if X in S1'Class then Ada.Text_IO.Put_Line ("-- Membership ignores discrims (root)"); else Ada.Text_IO.Put_Line ("-- Membership checks discrims (root)"); end if; Ada.Text_IO.Put_Line ("-- No Discrim. check on constrained " & "classwide object subtype (root)"); end; exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide object subtype (root)"); end; begin declare X : S2'Class := Root'Class(O2); begin if X in S1'Class then Ada.Text_IO.Put_Line ("-- Membership ignores discrims (root)"); else Ada.Text_IO.Put_Line ("-- Membership checks discrims (root)"); end if; end; exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (root, obj)"); end; begin declare X : S2'Class := Root'Class(OD1); begin if X in S1'Class then Ada.Text_IO.Put_Line ("-- Membership ignores discrims (der)"); else Ada.Text_IO.Put_Line ("-- Membership checks discrims (der)"); end if; Ada.Text_IO.Put_Line ("-- No Discrim. check on constrained " & "classwide object subtype (der)"); end; exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide object subtype (der)"); end; begin declare X : S2'Class := Root'Class(O2); begin if X in S1'Class then Ada.Text_IO.Put_Line ("-- Membership ignores discrims (der)"); else Ada.Text_IO.Put_Line ("-- Membership checks discrims (der)"); end if; end; exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (der, obj)"); end; begin Check_It_2 (S1'Class(O1)); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (root)"); end; begin Check_It_2 (S1'Class(O2)); Ada.Text_IO.Put_Line ("-- No Discrim. check on constrained " & "classwide type conversion (root)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide type conversion (root)"); end; begin Check_It_2 (S1'Class(OD1)); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (der)"); end; begin Check_It_2 (S1'Class(OD2)); Ada.Text_IO.Put_Line ("-- No Discrim. check on constrained " & "classwide type conversion (der)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide type conversion (der)"); end; begin Check_It_1 (Factory(1)); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (factory root)"); end; begin Check_It_1 (Factory(2)); Ada.Text_IO.Put_Line ("-- No discrim. check on constrained " & "classwide parameter subtype (factory root)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide parameter subtype (factory root)"); end; begin Check_It_1 (Factory(3)); exception when Constraint_Error => Ada.Text_IO.Put_Line ("** Check failed with matching " & "discrims (factory der)"); end; begin Check_It_1 (Factory(4)); Ada.Text_IO.Put_Line ("-- No discrim. check on constrained " & "classwide parameter subtype (factory der)"); exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide parameter subtype (factory der)"); end; begin declare X : S2'Class := Factory(1); begin if X in S1'Class then Ada.Text_IO.Put_Line ("-- Still ignores discrims (factory root)"); end if; Ada.Text_IO.Put_Line ("-- No Discrim. check on constrained " & "classwide object subtype (factory root)"); end; exception when Constraint_Error => Ada.Text_IO.Put_Line ("-- Discrim. check on constrained " & "classwide object subtype (factory root)"); end; Ada.Text_IO.Put_Line ("--- Test complete"); end Chk_AI57; **************************************************************** From: Randy Brukardt Sent: Thursday, May 20, 2007 12:53 AM For Janus/Ada, I get: --- Check behavior of constrained classwide types (version 2) -- No discrim. check on constrained classwide parameter subtype (root) -- No discrim. check on constrained classwide parameter subtype (der) -- Membership ignores discrims (root) -- No Discrim. check on constrained classwide object subtype (root) -- Membership ignores discrims (root) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide object subtype (der) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide type conversion (root) -- No Discrim. check on constrained classwide type conversion (der) -- No discrim. check on constrained classwide parameter subtype (factory root) -- No discrim. check on constrained classwide parameter subtype (factory der) -- Still ignores discrims (factory root) -- No Discrim. check on constrained classwide object subtype (factory root) --- Test complete For the older version of Gnat, I get the same results as before. I tried commenting out parts of the test to (dis)prove Tucker's contention, and as I suspected, the error moved to different uses of S1'Class or S2'Class. I did find one case that I could get to compile, and it reports: -- No discrim. check on constrained classwide parameter subtype (factory root) -- No discrim. check on constrained classwide parameter subtype (factory der) which is consistent with Janus/Ada on those test cases. (So far, Tucker, you're all alone on this... ;-) **************************************************************** From: Sent: Thursday, May 24, 2007 2:50 AM > Here's a revised test program; I added some function result > cases to cover Tucker's concern. My current development compiler (which no customer has ever seen) produces the following output: --- Check behavior of constrained classwide types (version 2) -- Discrim. check on constrained classwide parameter subtype (root) -- Discrim. check on constrained classwide parameter subtype (der) -- Membership ignores discrims (root) -- No Discrim. check on constrained classwide object subtype (root) -- Membership checks discrims (root) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide object subtype (der) -- Membership checks discrims (der) -- Discrim. check on constrained classwide type conversion (root) -- Discrim. check on constrained classwide type conversion (der) -- Discrim. check on constrained classwide parameter subtype (factory root) -- Discrim. check on constrained classwide parameter subtype (factory der) -- Still ignores discrims (factory root) -- No Discrim. check on constrained classwide object subtype (factory root) --- Test complete On the face of it, it looks like Steve B. couldn't make up his mind: I don't think he actually flips a coin when deciding whether to make a check or not, but that's close. ;-) **************************************************************** From: Stephen W. Baird Sent: Thursday, May 24, 2007 12:22 PM Not a coin flip; more of an uninitialized variable. If code is written with no thought given to a particular case (and certainly no testing of that case), then you get this sort of result. **************************************************************** From: Robert Dewar Sent: Thursday, May 24, 2007 6:54 AM My view here is that a) this "feature" is completely useless, so what we do should not be based on an argument of providing more functionality. b) the RM is unclear/inconsistent, so what we do should not be based on an argument of being faithful to the RM. c) compilers do radically different things now, so what we do should not be based on arguments about portability. The goals should be to a) figure out rules that are as simple as possible to implement in existing compilers. b) figure out rules that are as simple as possible to state in the RM. **************************************************************** From: Tucker Taft Sent: Thursday, May 24, 2007 7:36 AM I would hope that pretty high on our list is that users can easily understand what Ada source code means. There are no "surprises." Making such subtypes illegal would clearly address that issue, but as long as S1'Class is legal, we should be somewhat attentive to what user intent it intuitively represents. Of course we as implementors worry about all the different places where the RM talks about "satisfies" and so on, but most users would look at the one place where S'Class is defined and read that (i.e. 3.9(13-15)). We really should try to see if we can find any usage of this feature in the various user code we have access to, and if so, how it is being used. **************************************************************** From: Christoph Grein Sent: Thursday, May 24, 2007 7:30 AM I nevertheless fed it [the test program - ED] to GNAT 6.0.1 (the latest supported version as of 2007-01-17): chk_ai57.adb:98:33: type is already constrained chk_ai57.adb:116:33: type is already constrained chk_ai57.adb:132:33: type is already constrained chk_ai57.adb:150:33: type is already constrained chk_ai57.adb:238:33: type is already constrained compilation error Is GNAT right to reject these? **************************************************************** From: Robert A. Duff Sent: Thursday, May 24, 2007 9:36 AM That's what the ARG is trying to decide. Apparently, the RM is unclear on this point, and different compilers have taken different interpretations. **************************************************************** From: Tucker Taft Sent: Thursday, May 24, 2007 9:25 AM I think Ed Schonberg is looking into these messages. They don't relate to any RM legality rule of which I am aware. **************************************************************** From: Edmond Schonberg Sent: Thursday, May 24, 2007 1:48 PM as i mentioned, the error message indicates a semantic confusion in the front-end: one part of it considers class-wide types to be unconstrained, another one considers that a discriminant constraint indicates that the type cannot possibly receive an additional one. It is a trivial matter to make the error message disappear, after which GNAT reports: --- Check behavior of constrained classwide types (version 2) -- No discrim. check on constrained classwide parameter subtype (root) -- No discrim. check on constrained classwide parameter subtype (der) -- Membership ignores discrims (root) -- No Discrim. check on constrained classwide object subtype (root) -- Membership ignores discrims (root) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide object subtype (der) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide type conversion (root) -- No Discrim. check on constrained classwide type conversion (der) -- No discrim. check on constrained classwide parameter subtype (factory root) -- No discrim. check on constrained classwide parameter subtype (factory der) -- Still ignores discrims (factory root) -- No Discrim. check on constrained classwide object subtype (factory root) --- Test complete that is to say, in all other circumstances GNAT treats the type as unconstrained and applies no checks at all. I suspect that it will be a fairly distributed fix to force checks in all these places, should we decide that this is the desired semantics. **************************************************************** From: Tucker Taft Sent: Thursday, May 24, 2007 9:16 AM Thanks for the test case, Randy. It is almost always interesting to run such test cases against one's compiler, as you can be sure to discover things you didn't know about your compiler. One related case that it would be interesting to check is the following: type Der1 is new Root(1) with null record; ... declare X : Root'Class := Factory(2); Y : Der1; begin Y := Der1'(Root(X) with null record); Ada.Text_IO.Put_Line("-- No discrim.check on ancestor part"); exception when Constraint_Error => Ada.Text_IO.Put_Line("-- Discrim. check on ancestor part"); end; ... This involves a case where the discriminant is constrained as part of the derived type definition. Here I think we would all agree that a check is required on the value of the discriminant as part of the extension aggregate. It would be nice to try to comment out the lines of your test that give GNAT compile-time problems and see what it does with the ones that remain. In any case, without further ado, here are the results of running your test on a relatively recent version of our compiler: --- Check behavior of constrained classwide types (version 2) -- Discrim. check on constrained classwide parameter subtype (root) -- Discrim. check on constrained classwide parameter subtype (der) -- Discrim. check on constrained classwide object subtype (root) -- Membership ignores discrims (root) -- Discrim. check on constrained classwide object subtype (der) -- Membership ignores discrims (der) -- No Discrim. check on constrained classwide type conversion (root) -- No Discrim. check on constrained classwide type conversion (der) -- Discrim. check on constrained classwide parameter subtype (factory root) -- Discrim. check on constrained classwide parameter subtype (factory der) -- Discrim. check on constrained classwide object subtype (factory root) --- Test complete Interestingly, we do the check on subtype conversion, but on type conversion, don't bother to do a final subtype conversion as required by 4.6(51/2) for "constrained" subtypes. Touche'. We also don't seem to do any discriminant checking in classwide membership tests. We only do tag coverage checks. By the way, the wording for membership tests uses the phrase "satisfies any constraints of the named subtype." This nicely sidesteps the issue of "constrained" vs. "unconstrained," and gives us a possible way to describe S1'Class. That is S1'Class "has a constraint" but it is not a (fully) "constrained" subtype (since any given value might have additional, unconstrained discriminants). FYI, for the additional test case I suggested, we produce the following: -- Discrim. check on ancestor part By the way, I don't think there is a problem with the RM wording relating to "satisfies." A value satisfies a discriminant constraint if its discriminants have the values imposed by the constraint. That works fine for classwide values. The discriminants of a classwide value T'Class are defined in 3.4.1(5) to be those of T. Another tidbit: the only wording I could find that seems to disallow applying a constraint to a classwide type as follows: subtype S1C is T'Class(1); -- presumably not legal is that a discriminant constraint can only be applied to an unconstrained *discriminated* subtype, and if you read the definition of "discriminated" you might be able to decide that a classwide subtype is never "discriminated" (even though it "has discriminants") because it doesn't "inherit known discriminants" and also it has "unknown discriminants." All pretty subtle. 3.7.1(7/2) could be more explicit by saying an unconstrained discriminated *specific* subtype, or by adding a sentence at the end saying the subtype_mark shall not denote a class-wide subtype. Overall, I would say 4.6(51) is only one of several places where we didn't fully account for class-wide types and/or subtypes. I would argue it is more disruptive to the language to change the meaning of S1'Class as given clearly in 3.9(13-15), than to correctly account for the special characteristics of class-wide types and subtypes where appropriate. Omissions are a normal sort of after-the-fact correction. To withdraw a whole paragraph defining the semantics of a construct since we goofed in subtle ways on wording elsewhere in the manual seems like a funny sort of after-the-fact correction. As suggested above, one possible wording that would help is "has a constraint" when we want to include S1'Class beasties, as opposed to "constrained" or "unconstrained." That is what membership tests use, and seems like it could help fix 4.6(51/2). And perhaps similarly, "has discriminants" rather than "discriminated" if we want to include values of a class-wide type. **************************************************************** From: Pascal Leroy Sent: Thursday, May 24, 2007 10:52 AM > As suggested above, one possible wording that would help > is "has a constraint" when we want to include S1'Class > beasties, as opposed to "constrained" or "unconstrained." > That is what membership tests use, and seems like it could > help fix 4.6(51/2). And perhaps similarly, "has > discriminants" rather than "discriminated" if we want to > include values of a class-wide type. You are actually making a pretty compelling argument *against* your own position, Tuck. Who wants the added conceptual complexity of distinguishing between "constrained" and "has constraint", and "discriminated" and "has discriminants"? I surely don't want to have to explain to a customer that their deployed application failed because they didn't realize that their non-discriminated type actually had discriminants. I don't care which way the decision goes, but I am with Robert: choose the simplest option; we should not add even the tiniest bit of complexity to deal with this cornercase. **************************************************************** From: Tucker Taft Sent: Thursday, May 20, 2007 11:29 AM > ... > It would be nice to try to comment out the lines of your test that give > GNAT compile-time problems and see what it does with > the ones that remain... After commenting out the lines that seemed to cause compile-time failure in GNAT, and adding in my "extra" test case with an extension aggregate, I got the following from GNAT 5.03b: --- Check behavior of constrained classwide types (version 2) -- No discrim. check on constrained classwide parameter subtype (root) -- No discrim. check on constrained classwide parameter subtype (der) -- No Discrim. check on constrained classwide type conversion (root) -- No Discrim. check on constrained classwide type conversion (der) -- No discrim. check on constrained classwide parameter subtype (factory root) -- No discrim. check on constrained classwide parameter subtype (factory der) -- Discrim. check on ancestor part --- Test complete **************************************************************** From: Tucker Taft Sent: Thursday, May 24, 2007 11:34 AM >> ... And perhaps similarly, "has >> discriminants" rather than "discriminated" if we want to >> include values of a class-wide type. > ... I surely don't want to have to > explain to a customer that their deployed application failed because they > didn't realize that their non-discriminated type actually had > discriminants. Unfortunately, this issue has nothing to do with the partially constrained class-wide subtypes. It has to do with whether or not a class-wide type is considered a "discriminated" type. 3.7 is clear that class-wide types have "unknown discriminants" but is not at all clear whether they should be called "discriminated" types, which is then relevant to whether T'Class(D => 1) is permitted in 3.7.1. So I think we do need to look more carefully at the wording associated with class-wide types where the root type has discriminants. **************************************************************** From: Robert Dewar Sent: Thursday, May 24, 2007 1:15 PM > I would hope that pretty high on our list is that users > can easily understand what Ada source code means. There are no > "surprises." Making such subtypes illegal would clearly > address that issue, but as long as S1'Class is legal, we > should be somewhat attentive to what user intent it intuitively > represents. Don't worry about it too much if, as seems the case, users don't in practice use such subtypes anyway. > > Of course we as implementors worry about all the different > places where the RM talks about "satisfies" and so on, but > most users would look at the one place where S'Class is > defined and read that (i.e. 3.9(13-15)). We really should > try to see if we can find any usage of this feature > in the various user code we have access to, and if so, > how it is being used. SOunds like a good idea. **************************************************************** From: Randy Brukardt Sent: Friday, May 25, 2007 7:37 PM For what it's worth (probably not a lot), I've never seen a visible discriminant on a tagged type nor a constrained tagged subtype in any user code nor any of our code. S'Class therefore can't happen. I don't think there are any discriminanted tagged types in Claw or any of our other programs. (I say "don't think" because I can't think of any practical way to verify that; manually looking through several hundred type declarations is not practical.) **************************************************************** From: Robert Dewar Sent: Friday, May 25, 2007 7:48 PM I can't help but recall the effort we made to get rid of discriminants on tagged types in Ada 95, but unfortunately we failed. **************************************************************** From: Tucker Taft Sent: Friday, May 25, 2007 8:14 PM For what it's worth, a high percentage of the tagged types in our static analyzer have at least one discriminant, sometimes several. Bob Duff can give you some of the rationale behind it (he designed much of this stuff). **************************************************************** From: Robert A. Duff Sent: Saturday, May 26, 2007 7:33 AM Well, Robert will be quick to point out that Bob Duff is not a typical Ada programmer. ;-) As I recall, most of the types in question are limited. In Ada 95, the only way to initialize a limited object on its declaration is to use discriminants. In the glorious post-AI-318 world of Ada 2005, however, we could use functions, and the discriminants could disappear, or become private. I'm not sure they _should_ become private. Discriminants are constant, so I see no harm in exposing them in the public interface, if the alternative is an accessor function -- who cares if clients say X.Blah versus Blah(X). Many of those limited types are descendants of Root_Storage_Pool. Many are controlled. Many of the types have a Name discriminant, of type String. This is used for debugging printouts. It is also used as a key in hash tables and the like. (Yeah, yeah, I know String is annoyingly illegal -- the types are actually various string-like things such as access-to-string or index-into-string-table.) Many of the discrims are access types. For example, a limited controlled type whose init/finalize do something-or-other to some other object. The "other object" is passed as a parameter (i.e. a discrim) to the controlled type. This is the RAII pattern of C++. **************************************************************** From: Tucker Taft Sent: Saturday, May 26, 2007 9:04 AM Thanks, Bob, for giving some explanation. I would never accuse you of being a typical Ada programmer... However, there is a probably small collection of people like you who do what one might traditionally call "systems" programming in Ada. I think of the folks who implemented various parts of the distributed systems annex for GNAT, and I think of some of the people at Object Interface Systems who implemented their CORBA ORB in Ada. These programmers tend to use every feature of the language, often creating nightmares for implementors because of the stress they put on otherwise rarely used features. Although they are probably not good examples of "typical" Ada coding styles, some of the code they create is used by many other Ada programmers as a foundation for "application"-level programming. So if we have access to various examples of "systems" level programming in Ada, we should include them in our considerations, even if they are "atypical" from a numbers point of view. (I would probably disqualify some of the code written by the compiler implementor, because the compiler implementor often makes an effort to *avoid* the features that they know are flaky in their own compiler. ;-) In any case, once you have discriminants on tagged types, and you can constrain them when creating a type extension (e.g. type NT is new T(blah) with ...) then it is not unreasonable to write: subtype S is T(blah); type NT is new S with ... type NT2 is new S with ... and then you have a natural "class" of types that you might refer to as "S'Class". For what that's worth... **************************************************************** From: Robert I. Eachus Sent: Saturday, May 26, 2007 7:11 PM I think I vote to label this whole issue as pathology and move on. The problem is not what it would be nice to be able to do if visibility and dispatching allowed it, but what is really possible in practice. It is easy to create example code where you have a parent class, a subclass, and their relationship all visible, but in production code that is very, very unlikely. Years ago, I was working on a message passing package that used heterogenous lists as mailboxes* Another case of a low-level package where normal Ada programming is being stretched to its limit. The reason for commenting here comes from when debugging the heterogenous list code. A lot of that code had to be more or less duplicated in several places, and I probably spent more time working on the debugging scaffolding than anything else. Using GNAT, when the debug flag was on, you got, not the normal class itself, when creating a message, but a child class with lots of checking and diagnostics implemented. The problem I ran into was that there were places where I would needed the mailbox code to determine whether a message had the extra debug fields in it. (I could and often did compile just one class with debugging on.) The mailman and mailbox code usually didn't have the debug flag on, but I wanted to be able to tell when they saw a message that did. The solution, if I can call it that was a package with an array of tags and doing comparisons against the tags. I would have loved to use "if Message in Debug'Class" but that was impossible. Not due to anything here, but due to visibility rules. I think that is why these cases have not come up before. Anywhere you want to use the 'Class attribute to define a subclass, due to visibility or dispatching, it doesn't work. The case I usually ran into was a class descended from a parent class, but that relation being hidden in packages which were not visible, or in private parts. So while insisting that "if X in Subtype'Class" work is a nice theoretical exercise, in practice you can't write it when you want to. (The real Catch-22 is dispatching. If you have the package that exports a tagged type also export a membership test explicitly, dispatching means that it doesn't work the way you want. You need a separate package, where the specification withs the root package, and the body withs all the children. As I said above been there done that.) * I probably should create a new version that uses discriminants, but that is not the point here. The issue was message filtering. There are times when you want to sort through a mailbox and find a particular class of message. One or more discriminants of enumeration types would have been wonderful. This would still allow 'normal' class extention for say new members of the TEXT class, but adding a new class of messages would call for non-local code revisions. **************************************************************** From: Stephen W. Baird Sent: Friday, October 26, 2007 5:05 PM Before I try to write up AI05-0057, I'd like to informally understand the rules that I'm trying to formulate wording for. Specifically, I'd like the answer to one yes/no question: Given a constrained subtype S of an unconstrained specific tagged subtype T, is S'Class synonymous with T'Class? As the AI notes, 3.9(13-15) clearly states that the answer is "No" while other places in the RM suggest otherwise. So what do we want to do? It seems to me that 3.9(13-15) reflects an intention that was never fully carried out. This passage is so unambiguous and explicit that it is hard not to tend towards honoring it and making corresponding changes elsewhere in the RM. In Paris, however, it was argued that this would be a huge amount of work for implementors for very little benefit. It was also argued that an indefinite-but-sort-of-constrained subtype is a bad idea which doesn't belong in the language even apart from such pragmatic concerns. Some of the feature interactions the 3.9 definition of S'Class would introduce are pretty obscure. For the IBM/Rational Ada compiler, for example, this would require passing additional values to the function result allocation callback routines used in the implementation of some initialize-in-place functions in order to implement the constraint checking associated with a dispatching call which is used as the initial value of an initialized allocator of an access type whose designated subtype is S'Class. This sort of thing is easy to miss. So perhaps we should make the much simpler change of changing 3.9 and defining S'Class to be synonymous with T'Class. Is there a consensus out there? **************************************************************** From: Randy Brukardt Sent: Friday, October 26, 2007 5:36 PM I've been strongly in favor of this change since day one. But, I'm not sure if there is enough support for it. Early in the Ada 9x process, it was this way, and that is what we implemented. I recall complaining about 3.9(13-5) to no avail when they were put in. That suggests that at least someone thinks that they're a good idea, even though those rules aren't implemented consistently by any Ada compiler that I know of. We've never done anything to our implementation to implement them. Having "partially constrained" types (as AARM 3.9(15.b) calls them is folly and means that there is a third state of constraint (constrained, unconstrained, and "partially constrained"). Implementing this in Janus/Ada would require rewritting (or at least checking) essentially all of the code that handles dynamic semantics for classwide types (because currently all of our checking code is of the form "If Is_Unconstrained (...) then ... else end if;. It's not worth it, IMHO. I thought that we had discussed this topic long enough in Paris to come to some sort of decision, but I see that I didn't record anything at all for this AI. One obvious question is how wrong is the Standard, really? If only a few rules need to be changed, there is an argument for "implementers be damned", since these rules have existed since at least Ada 9x 2.0 - March 1993 (I went back and looked). But if the changes are widespread, then the complexity of the fix becomes an issue (for instance, if it warps the Ada constraint model). So perhaps the best plan is to write up both alternatives so that those of us who want to kill this idea have more concrete proof of how disruptive this is. Of course, you really didn't want a suggestion to do more work!! :-) **************************************************************** From: Robert A. Duff Sent: Friday, October 26, 2007 5:37 PM > So perhaps we should make the much simpler change of changing 3.9 > and defining S'Class to be synonymous with T'Class. If that works, it's OK with me. I seem to recall some problem with something like: type T2 is new T1 (Discrim => 10); where we know all objects of type T2 have Discrim = 10. And T2'Base is illegal. Is T2'Class an underhanded way of getting at T2'Base? Does this cause problems? I'm not sure, which of course means this argument is FUD. **************************************************************** From: Randy Brukardt Sent: Friday, October 26, 2007 6:01 PM > If that works, it's OK with me. > > I seem to recall some problem with something like: > > type T2 is new T1 (Discrim => 10); That better be type T2 is new T1 (Discrim => 10) with null record; or it is illegal. > where we know all objects of type T2 have Discrim = 10. > And T2'Base is illegal. Is T2'Class an underhanded way > of getting at T2'Base? Does this cause problems? > > I'm not sure, which of course means this argument is FUD. I don't think there is a problem with T'Base for tagged types, as the locations of components can't move between a derived type and their parent. And you can't leave the discriminants out of the object, because you have to support a view conversion to T1 or T1'Class (which surely has a discriminant), and the type must be by-reference. I thought the issue were with untagged types where neither of these things is true (a view conversion can always be accomplished with copy-in, copy-out for any type that is not required to be by-reference, and the only view conversions on for in out parameters anyway). But I may have forgotten something, so my response to your FUD may have been more FUD. FUD-a-licious ;-) **************************************************************** From: Stephen W. Baird Sent: Friday, October 26, 2007 6:35 PM > I seem to recall some problem with something like: > > type T2 is new T1 (Discrim => 10); > > where we know all objects of type T2 have Discrim = 10. > And T2'Base is illegal. Is T2'Class an underhanded way > of getting at T2'Base? Does this cause problems? I don't think there is a problem. We are talking about tagged types here, so your example would turn into type T2 is new T1 (Discrim => 10) with null record; and values in T2'Class will always have a Discrim value of 10. **************************************************************** From: Tucker Taft Sent: Friday, October 26, 2007 6:50 PM I would rather make S'Class illegal than make it misleading. If S is a constrained subtype, then if someone writes a subprogram that has a formal parameter of subtype S'Class, they should be safe to assume that the actual parameter satisfies whatever constraints that S imposes. Similarly, if someone writes "Obj in S'Class" then they presumably want to know whether Obj satisfies the constraints imposed by S. So I am not in favor of making S'Class synonymous with T'Class. But it may be that this feature is almost never used, so it might be reasonable to disallow it. I suppose there might be generic contract model issues in making it illegal, so we might need to have it raise Program_Error if a generic applies 'Class to anything other than a first subtype. Or require implementations to implement what 3.9(13-15) implies, and fix other places in the RM that neglected to cover it. **************************************************************** From: Randy Brukardt Sent: Friday, October 26, 2007 7:29 PM > I would rather make S'Class illegal than make > it misleading. If S is a constrained subtype, > then if someone writes a subprogram that has > a formal parameter of subtype S'Class, they > should be safe to assume that the actual parameter > satisfies whatever constraints that S imposes. It can't be illegal, because of examples like the one Bob showed (incorrectly): type T2 is new T1 (Discrim => 10) with null record; There is no unconstrained name for type T2. If we had T2'Base (which of course is illegal), then perhaps you could have such a rule. But it can't be illegal to say T2'Class if there is no alternative with the correct meaning. But why is this misleading? T'Base isn't misleading for the types that it is allowed on: there is no constraint. I think of 'Class the same way: there is no constraint and it in fact allows any child type as well. It's exactly like 'Base except even more powerful. I suppose that is the crux of the issue: I see 'Class as a super version of 'Base, and you see it as some sort of modifier that leaves constraints intact (making it a completely independent dimension and thus adding that complexity to everything). The rules need to take a single view (obviously), but I think either view is consistent. My own guess is more people see it like me than you (because of the lack of use of this feature - if there was use, this would have been fixed long ago), but that can only be a guess. **************************************************************** From: Tucker Taft Sent: Saturday, October 27, 2007 7:18 AM >> I would rather make S'Class illegal than make >> it misleading. If S is a constrained subtype, >> then if someone writes a subprogram that has >> a formal parameter of subtype S'Class, they >> should be safe to assume that the actual parameter >> satisfies whatever constraints that S imposes. > > It can't be illegal, because of examples like the one Bob showed > (incorrectly): > > type T2 is new T1 (Discrim => 10) with null record; I should have been more precise. I am only talking about the case where S imposes more constraints than the first subtype. In this case T2 imposes a constraint, but it *is* the first subtype. **************************************************************** From: Stephen W. Baird Sent: Saturday, October 27, 2007 7:41 PM > I would rather make S'Class illegal than make > it misleading. If S is a constrained subtype, > then if someone writes a subprogram that has > a formal parameter of subtype S'Class, they > should be safe to assume that the actual parameter > satisfies whatever constraints that S imposes. Playing devil's advocate, I can't resist asking how you feel about getting a negative value back from a call to either Natural'Input or to a generic formal function whose result type is declared to be Natural. Still, your point is well taken. Just because the language has other problems in this area does not mean that we should make things worse. > So I am not in favor of making S'Class synonymous > with T'Class. But it may be that this feature > is almost never used, so it might be reasonable > to disallow it. > > ... > > Or require implementations to implement what > 3.9(13-15) implies, and fix other places in > the RM that neglected to cover it. Which brings us back to my original question. We now have 3 alternatives: 1) Require implementations to implement what 3.9(13-15) implies, and fix other places in the RM that neglected to cover it. 2) Make S'Class synonymous with T'Class. 3) Prohibit S'Class and somehow deal with the generic-related issues. You've stated that you prefer #3 to #2. How do you rank #1 relative to the other two? **************************************************************** From: Tucker Taft Sent: Sunday, October 28, 2007 4:37 AM > Playing devil's advocate, I can't resist asking how > you feel about getting a negative value back from a > call to either Natural'Input or to a generic formal > function whose result type is declared to be Natural. I have always been uncomfortable with Natural'Value and Natural'Input. But at least I have an underlying rationale there, namely that we are using the subtype to select a type-related attribute function. There is only one such function for the whole type, so you get the same function no matter which subtype you use to select it. On the other hand, if I use Natural as a formal parameter subtype, or in a membership test, there is no confusion what I get, and it matches my intuition. It makes perfect sense to me that a class-wide type might have subtypes, which represent subsets of the values of the type. Given that tagged types have discriminants, it makes good sense that you could define a subtype of a class-wide type that specified values for the discriminants visible on the class-wide type. And it makes good sense that I might want to test whether a given class-wide value satisfies the discriminants that are visible on the class-wide type, and using a membership test to do that seems perfectly reasonable. ... > Which brings us back to my original question. > > We now have 3 alternatives: > 1) Require implementations to implement what > 3.9(13-15) implies, and fix other places in > the RM that neglected to cover it. > 2) Make S'Class synonymous with T'Class. > 3) Prohibit S'Class and somehow deal with > the generic-related issues. > > You've stated that you prefer #3 to #2. > How do you rank #1 relative to the other two? I believe it is useful to test the discriminants of a class-wide value with a membership test, so I prefer #1. The only justification for #3 is that it is better than #2, which I believe is actively misleading, and upward *inconsistent* (not just upward incompatible), for implementations that happen to have done what I consider the "right thing" in the past. **************************************************************** From: Randy Brukardt Sent: Sunday, October 28, 2007 6:40 PM ... > On the other hand, if I use Natural as a formal parameter > subtype, or in a membership test, there is no confusion > what I get, and it matches my intuition. And what about Natural'Base? This names a more unconstrained type than Natural alone, and S'Class does the same thing. > It makes perfect sense to me that a class-wide type > might have subtypes, which represent subsets of > the values of the type. Not to me: 'Class is making a more unconstrained type, and trying to mix that with less unconstrained subtypes leads to madness. > Given that tagged types > have discriminants, it makes good sense that you > could define a subtype of a class-wide type that > specified values for the discriminants visible on > the class-wide type. Discriminants on tagged types exists because of orthogonality, not because they're useful. The primary uses in practice are for hidden implementation things that no one should be depending on -- and thus no names should be necessary for those subtypes. > And it makes good sense that > I might want to test whether a given class-wide > value satisfies the discriminants that are visible > on the class-wide type, and using a membership > test to do that seems perfectly reasonable. I don't think I've every written a membership to test discriminants (outside of ACATS-type tests) in my 27 years of Ada programming, and I doubt more than 1 out of a hundred Ada programmers would think to do it even if they needed it. So you have two "features" that are very rarely going to be used; "reasonable" is not the word I would use to describe this coding. ... > I believe it is useful to test the discriminants > of a class-wide value with a membership test, so > I prefer #1. The only justification for #3 is > that it is better than #2, which I believe is > actively misleading, and upward *inconsistent* > (not just upward incompatible), for implementations > that happen to have done what I consider the > "right thing" in the past. Are there any such implementations? If not, there is no practical incompatibility. **************************************************************** From: Tucker Taft Sent: Sunday, October 28, 2007 9:46 PM > ... > Some of the feature interactions the 3.9 definition of S'Class would > introduce are pretty obscure. For the IBM/Rational Ada compiler, > for example, this would require passing additional values to the function > result allocation callback routines used in the implementation of some > initialize-in-place functions in order to implement the constraint checking > associated with a dispatching call which is used as the initial value of an > initialized allocator of an access type whose designated subtype is > S'Class. This sort of thing is easy to miss. That is a mouthful. Could you create an example that illustrates this? It might help understand the implementation implications a bit better. **************************************************************** From: Tucker Taft Sent: Sunday, October 28, 2007 11:19 PM One reason I ask is that I would have thought this sort of constraint could be checked after return from the function, even though it is allocate in place, since presumably the storage pool has to support arbitrarily large allocations if it handles S'Class. **************************************************************** From: Stephen W. Baird Sent: Tuesday, October 30, 2007 2:28 PM > That is a mouthful. Could you create an example that > illustrates this? It might help understand the > implementation implications a bit better. I was just trying to make the point that there are lots of little interactions to be aware of. The details of this one example are not particularly important, nor is it a very difficult problem to solve. It's just a typical example. However, since you ask ... For purposes of this discussion, define an "initialize-in-place function" to be a function which is prohibited from creating a separate return object by 7.5(8.1/2). In the case where the result subtype of an initialize-in-place function is indefinite (or, in the case of a discriminated type with default discriminant values, unconstrained), the IBM/Rational Ada compiler defines an additional implicit parameter for the function: a reference to a storage-allocation routine. Within an execution of a call to the function, at the point where the the constraints of the function result are known, this callback routine is invoked. The constraint values (i.e., array bounds or discriminant values) are passed in and the callback routine does one of three things: 1) It propagates an exception. 2) It returns null. 3) It returns a references to the location where the function return object is to reside. The size of the function return object is also passed in. Constraint_Error is propagated if the function call is being used to initialize an object whose constraints are incompatible with the constraint values that were passed to the callback routine. Null is returned if the function call is not being used to initialize an object (e.g., if the function call occurs as an actual parameter to another call). A non-null result is returned otherwise. If the function call is used as the initial_value of an initialized allocator, the callback routine returns a reference to storage that it allocated out of the appropriate collection. As things stand today, in the case of a tagged type only the values of the noninherited discriminants are passed in. Other discriminant values (and the tag value) do not seem to be needed. If a function returns as its result a call to another function, this callback parameter is simply passed along. Now, the example: declare package Pkg1 is type Root (Flag : Boolean) is tagged limited null record; function Make (X : Root) return Root; end Pkg1; package body Pkg1 is function Make (X : Root) return Root is begin return X : Root (False); end Make; end Pkg1; package Pkg2 is type Extension (Int : Integer) is new Pkg1.Root (True) with null record; overriding function Make (X : Extension) return Extension; end Pkg2; package body Pkg2 is function Make (X : Extension) return Extension is begin return X : Extension (Int => 37); end Make; end Pkg2; type Root_Class_Ref is access all Pkg1.Root'Class; Ext : aliased Pkg2.Extension (Int => 38); Ext_Ref : Root_Class_Ref := Ext'access; function Designated return Pkg1.Root'Class is begin return Pkg1.Make (X => Ext_Ref.all); end Designated; subtype S is Pkg1.Root (False); type S_Ref is access S'Class; Ptr : S_Ref := new Pkg1.Root'Class'(Designated); begin null; end; Consider the call to the function Designated. The caller passes in a routine which is expects to be passed a size. Depending on the resolution of this AI, it might also take the Boolean needed to perform the constraint check. The callback routine allocates the requested amount of storage from the collection associated with the access type S_Ref. Designated passes this callback routine reference along in its dispatching call to Pkg1.Make, which ends up executing the body of Pkg2.Make. Pkg2.Make currently invokes the callback routine with a size and the value of the Int discriminant (i.e., 37); no Boolean value is passed in. The parameter profile mismatch (recall that the callee knows nothing about this Integer discriminant) is only apparent - I'm glossing over some details - but no Boolean discriminant value is currently passed in. This may or may not be ok depending on the resolution of this AI. **************************************************************** From: Tucker Taft Sent: Tuesday, October 30, 3007 3:35 PM For this case, since the allocation properties of "access S'Class" are the same as those for "access Root'Class" there seems no need for the allocation call-back routine to even be aware of the additional constraint check. It can be performed after the initialize-in-place function returns. That is certainly what would happen for a function that is not initialize-in-place. That also seems consistent with the order suggested in 4.8(7) for the semantics for allocators: ... the evaluation of the qualified_expression is performed first. An object of the designated type is created and the value of the qualified_expression is converted to the designated subtype and assigned to the object. With initialize-in-place it is tough to say exactly when the "assignment" occurs, but here it is clear that the conversion to the designated subtype can occur after creation of the object, so any Constraint_Error could be raised after already allocating the storage. I presume the reason for the allocation call-back routine normally to perform the constraint check is that it might determine the maximum size that would be needed from the allocator, but for any class-wide subtype, the 'Max_Size_In_Storage_Elements is presumably going to be something approximating Storage_Count'Last. S'Class can't be used as a component, so there won't be any pre-allocated space already set aside for it in an enclosing object. Because of these characteristics of S'Class, I think any check required can generally be "tacked on" at the end, rather than having to be inserted in the middle of things, which I think should simplify implementations. In particular, I don't think you need the extra "Boolean" parameter you describe (of course, you might still choose to use that approach, but it doesn't seem required by the language). **************************************************************** From: Stephen W. Baird Sent: Tuesday, October 30, 3007 4:10 PM > With initialize-in-place it is tough to say exactly > when the "assignment" occurs, but here it is > clear that the conversion to the designated > subtype can occur after creation of the > object, so any Constraint_Error could be > raised after already allocating the storage. I just sent my writeup of AI05-0050 to Randy. The topic of exactly where initialize-in-place constraint checks may be performed is discussed there. You are certainly right that Constraint_Error could be raised later. In our implementation, there are good reasons to try to prevent a "bad" object from being allocated. We have a runtime library routine which takes a collection and a finalization procedure and calls the procedure once for each object allocated out of the collection. That's how access type finalization is implemented. The routine assumes that each object satisfies the constraints of the designated subtype. With this approach, allocating "bad" objects can lead to problems. I didn't describe our strategy for dealing with "backing-out" of a return statement (e.g., exiting an extended return statement via a goto statement), but there would be similar problems in that area if we allowed a "bad" object to be allocated. > ... > Because of these characteristics of S'Class, > I think any check required can generally be > "tacked on" at the end, rather than having > to be inserted in the middle of things, > which I think should simplify implementations. I agree that the language would allow this, but I think it would not be a simplification for our implementation. In any case, my main point was only that this is the kind of fairly subtle point that implementors would have to review in order to implement constrained class-wide subtypes. **************************************************************** From: Stephen W. Baird Sent: Tuesday, October 30, 2007 2:42 PM Tuck feels that 3.9(13-15) is fine as it is written. Randy disagrees. Bob says that Randy's position "is ok with me". Not exactly the consensus I had hoped for, but so it goes. Ed - the minutes of the Paris meeting are inexplicably silent regarding this AI, but I think I remember you arguing against requiring full implementation of 3.9(13-15) on the grounds that it would be a lot of work for a very small benefit. Is my memory correct? Is this your position? **************************************************************** From: Edmond Schonberg Sent: Tuesday, October 30, 3007 2:57 PM > Ed - the minutes of the Paris meeting are inexplicably silent > regarding this AI, but I think I remember you arguing > against requiring full implementation of 3.9(13-15) on > the grounds that it would be a lot of work for a very small > benefit. Is my memory correct? Is this your position? Yes, this was my position at the Albuquerque meeting, which is where I remember discussing this AI. Our implementation has no mechanism currently to support partial constraints on otherwise unconstrained types, and I'm not convinced that this is a glaring gap. **************************************************************** From: Tucker Taft Sent: Tuesday, October 30, 3007 3:08 PM I might mention that the notion of S'Class as being a "corresponding class-wide subtype for S" is mentioned in 3.4.1(4) where class-wide types are first defined. The intent seems pretty clear. I see no support in the manual for the notion that S'Class is somehow like S'Base, even if some implementors chose to implement it that way. It is certainly unfortunate there were no ACATS tests for S'Class when S was not the first subtype, but that isn't usually considered an excuse for dropping the feature. I admit the feature is not the most important, but if a programmer actually uses S'Class, where S is not the first subtype, anyone having read either 3.4.1(4) or 3.9(13-15) would know what they intended. And at least some people's intuition ;-) would allow them to guess the meaning without having memorized those paragraphs. **************************************************************** From: Stephen W. Baird Sent: Tuesday, October 30, 3007 4:39 PM > I might mention that the notion of S'Class as being > a "corresponding class-wide subtype for S" is mentioned in > 3.4.1(4) where class-wide types are first defined. The > intent seems pretty clear. I agree. > I see no support in the manual > for the notion that S'Class is somehow like S'Base, > even if some implementors chose to implement it that way. I agree. The intent seems clear, but the consequences were never worked out. > It is certainly unfortunate there were no ACATS tests for > S'Class when S was not the first subtype, but that isn't > usually considered an excuse for dropping the feature. It's not the ACATS coverage that is the issue. It is the fact that, at least as far as I know, noone has ever come close to implementing this feature correctly since it was introduced in Ada95, no customer has ever complained about it, and it would be a lot of work to get it right. I disagree with Randy's view that ignoring the constraints of S when defining S'Class is cleaner and more intuitive. I have a lot more sympathy for Ed's position. On the other hand, backing out of an Ada95 feature just because it was never fully defined or implemented (by anyone) is, as far as I know, unprecedented. Ultimately, the group will have to decide. I could write up an AI which takes one position or the other, but the group would have to vote on it. I think that the writeup for the preserve-the-constraints position would be a fair amount of work which I would like to avoid if we are going to vote the thing down based solely on the information that we already have. **************************************************************** From: Robert A. Duff Sent: Tuesday, October 30, 3007 5:01 PM > > I might mention that the notion of S'Class as being > > a "corresponding class-wide subtype for S" is mentioned in > > 3.4.1(4) where class-wide types are first defined. The > > intent seems pretty clear. > > I agree. Me, too (agreeing with Tuck on the intent). The Ada 9X team discussed this, and we knew it was a little bit weird to have these partially-constrained things, and we decided to do it anyway, and I think the RM and AARM makes that clear. However, from a practical point of view, why should implementers do a lot of work that nobody cares about? At this point, I guess I'm in favor of Tuck's compromise, which is to make the Constrained'Class construct illegal. Better make it illegal than misleading. (Except, to preserve the generic contract model, we have to make it a run-time check -- no big deal.) > It's not the ACATS coverage that is the issue. ... Well, if ACATS had had a test early on, all implementers would have noticed the issue, and implemented the intent. But that's water under the burned bridge. ;-) > On the other hand, backing out of an Ada95 feature just > because it was never fully defined or implemented (by anyone) > is, as far as I know, unprecedented. Really? Surely we've backed off on some things. Anyway, I wouldn't call S'Class (where S is constrained) a "feature" -- it's an obscure combination of features. > Ultimately, the group will have to decide. I could write up > an AI which takes one position or the other, but the group > would have to vote on it. I think that the writeup for the > preserve-the-constraints position would be a fair amount of > work which I would like to avoid if we are going to vote the > thing down based solely on the information that we already have. I'd say you should avoid that work. **************************************************************** From: Randy Brukardt Sent: Tuesday, October 30, 3007 5:24 PM ... > On the other hand, backing out of an Ada95 feature just > because it was never fully defined or implemented (by anyone) > is, as far as I know, unprecedented. We surely have made things illegal that used to be legal, so that seems to suggest that choice (3) may be a good compromise. Whether you call the thing being made illegal a "feature" or a "mistake" is probably only a matter of perspective. After all, if you believe that it is reasonable for others to take my view - and it seems that virtually all implementers did - then we have a case where something can reasonably be thought to have two different semantics depending on the user's viewpoint - and with no particular reason to favor one over the other, we usually make such things illegal. I continually find it annoying that it is illegal to mix classwide and static operations in a call without sticking in extra type conversions - and surely there are a number of possible meanings that are consistent - the reason it is illegal is that a number of meanings could have been meant without an indication of *which* was meant. That seems to apply here, too. **************************************************************** From: Randy Brukardt Sent: Tuesday, October 30, 3007 6:39 PM > On the other hand, backing out of an Ada95 feature just > because it was never fully defined or implemented (by anyone) > is, as far as I know, unprecedented. It strikes me that while it may or may not be unprecedented in the past, another one of your homework AIs (AI05-0053) that we'll vote on in Washington has exactly that effect: dropping "aliased" from extended return statements. It surely was never fully defined or implemented. So whether there is a precedent or not will depend on the order that we consider the AIs at the meeting. ;-) **************************************************************** From: Tucker Taft Sent: Tuesday, October 30, 3007 8:35 PM I admit I haven't investigated in detail the return-in-place implications (though it seems based on our recent exchange that the checks can be done after return), but in Ada 95, we *intended* to implement it all. But admittedly we never tested it thoroughly. On testing it more recently, it appears we correctly implemented the constraint checks, but made a (small) mistake (since fixed) in the implementation of the membership test. Neither was much of an effort. I'm surprised you describe it as a lot of work, but I suppose any rule in Ada can be a lot of work to get *exactly* right. I still don't think that is reason to withdraw the rule, since every rule in the language, some of which are *very* obscure and admittedly painful to implement, can be work to get right. I just don't see why this one should be singled out. I agree that making it illegal is a possible compromise, but it seems strange to give up on this one. There certainly seems no urgency for implementors to implement it, but there also seems no urgency in making it illegal. I would rather make whatever fixes are needed to make sure the entire manual is consistent with what 3.4.1 and 3.9 already say, and then leave it at that. Implementors will get around to implementing it correctly when they individually choose to do so. Apparently it is not a feature that many customers use, so leaving it to the market to decide when to force implementors to get it right is not that terrible. On the other hand, having implementors do work to make it illegal seems like a *real* waste of energy. **************************************************************** From: Randy Brukardt Sent: Tuesday, October 30, 3007 8:50 PM At least several compilers implement this completely wrong ('Class is always unconstrained). That's clearly a portability hazard - moving code from our compiler to yours could lead to real problems if the users assumed S'Class is unconstrained. That seems like a sandtrap to me, one that we need to address. If we decide to fix the standard, ACATS tests for that fix would be a high priority (because we *know* that implementers get it wrong, and there is a portability hazard). If it is worth fixing the standard, then it is worth testing it. We don't have a "fix but don't test" category, because such a thing is illogical. If we don't want implementers to fix it, then we should make it illegal. (Or declare it a pathology and refuse to fix it at all.) ****************************************************************