!standard 4.5.2(9.8/4) 20-01-10 AI12-0352-1/01 !class binding interpretation 20-01-10 !status work item 20-01-10 !status received 15-04-28 !priority Low !difficulty Easy !qualifier Error !subject Early derivation and equality of untagged types !summary Declaration of a user-defined primitive equality operation for a record type T is illegal if it occurs after a type has been derived from T. !question Consider: package Pkg1 is type Priv is private; procedure Test; private type Priv is record Aa, Bb : Integer; end record; type Derived_Early is new Priv; function "=" (L, R : Priv) return Boolean; end Pkg1; package body Pkg1 is -- if both Aa components are 0, then ignore Bb components function "=" (L, R : Priv) return Boolean is (if L.Aa = 0 and R.Aa = 0 then True else (L.Aa = R.Aa) and (L.Bb = R.Bb)); procedure Test is begin if Priv'(0, 1) /= (0, 2) then raise Program_Error; end if; if Derived_Early'(0, 1) = (0, 2) then raise Program_Error; end if; end; end Pkg1; The user-defined equality is clearly used for objects of type Priv. But what about Derived_Early? The Standard seems clear - the user-defined equality op for Priv is not inherited by Derived_Early and therefore plays no role in determining what happens when the "=" operator for Derived_Early is called. This behavior seems undesirable (to the extent that anyone cares about it one way or the other). Should it be illegal to explicitly declare a primitive equality operator for an untagged record type after the type has been used as the parent type of a derived type? (Yes.) !recommendation (See Summary.) !wording Modify 4.5.2(9.8/4): If the profile of an explicitly declared primitive equality operator of an untagged record type is type conformant with that of the corresponding predefined equality operator, the declaration shall occur before the type is frozen. {In addition, no type shall have been derived from the untagged record type before the primitive equality operator.} In addition to the places where Legality Rules normally apply (see 12.3), this rule applies also in the private part of an instance of a generic unit. !discussion Note that the declaration of any primitives after derivation is not allowed for tagged types, so this only applies to untagged types. For array types, composition of user-defined "=" does not occur, so this oddity doesn't matter. However, for record types which equality otherwise composes, this sort of derivation makes the user-defined equality disappear, something we want to avoid. Note that if the user is intentionally doing this to have a type that has the original equality, it is better to declare the type with the user defined equality second: type Basic_Record is record Aa, Bb : Integer; end record; type Priv is new Basic_Record; function "=" (L, R : Priv) return Boolean; This organization is much clearer to the user that the equality only applies to Priv and not to Basic_Record. Thus, we make the early derivation "trick" illegal, as it is at least as likely to be a surprise as intended behavior. !ASIS No ASIS effect. !ACATS test An ACATS B-test is needed to check that the new rule is enforced. !appendix From: Steve Baird Sent: Thursday, November 7, 2019 7:46 PM AI12-0101 is about treating untagged record types more like tagged types with respect to predefined equality. It just removes a legality rule, which means that AI05-0123 applies in the cases that were previously illegal. Clear enough so far. This example is now legal package Pkg1 is type Priv is private; function Make (Aa, Bb : Integer) return Priv; private type Priv is record Aa, Bb : Integer; end record; function "=" (L, R : Priv) return Boolean; function Make (Aa, Bb : Integer) return Priv is (Aa, Bb); end Pkg1; package body Pkg1 is -- if both Aa components are 0, then ignore Bb components function "=" (L, R : Priv) return Boolean is (if L.Aa = 0 and R.Aa = 0 then True else (L.Aa = R.Aa) and (L.Bb = R.Bb)); end Pkg1; and a call to Pkg1."=" executes the user-defined body: How does this work with derived types? Let's add this case to our example: with Pkg1; package Pkg2 is type Der is new Pkg1.Priv; end Pkg2; What happens when Pkg2."=" is called? It seems clear that we want the user-defined body to be executed. And it is, because 3.4(17/2) says "already exists", as opposed to something like "is visible". No problems so far. But with untagged types we have a possible scenario that is illegal for tagged types: deriving from a type before all of the primitive operations of the parent type have been declared. So consider this variant of our previous example: package Pkg1 is type Priv is private; procedure Test; private type Priv is record Aa, Bb : Integer; end record; type Derived_Early is new Priv; function "=" (L, R : Priv) return Boolean; end Pkg1; package body Pkg1 is -- if both Aa components are 0, then ignore Bb components function "=" (L, R : Priv) return Boolean is (if L.Aa = 0 and R.Aa = 0 then True else (L.Aa = R.Aa) and (L.Bb = R.Bb)); procedure Test is begin if Priv'(0, 1) /= (0, 2) then raise Program_Error; end if; if Derived_Early'(0, 1) = (0, 2) then raise Program_Error; end if; end; end Pkg1; What happens when procedure Test is called? The RM seems clear - the user-defined equality op for Priv is not inherited by Derived_Early and therefore plays no role in determining what happens when the "=" operator for Derived_Early is called. This behavior seems undesirable (to the extent that anyone cares about it one way or the other), but it is well defined. Should it be illegal to explicitly declare a primitive equality operator for an untagged record type after the type has been used as the parent type of a derived type? This would be somewhat similar to the rule added by AI12-0109 Similarly, it is illegal to specify a nonconfirming type-related representation aspect for an untagged by-reference type after one or more types have been derived from it. except that we are talking about the declaration of a primitive equality operator instead of an aspect specification. **************************************************************** From: Jean-Pierre Rosen Sent: Friday, November 8, 2019 12:15 AM > This behavior seems undesirable (to the extent that anyone cares about > it one way or the other), but it is well defined. I think that the one who writes this is knowledgeable enough to do it on purpose, presumably precisely because he doesn't want to inherit the "=" operation. > Should it be illegal to explicitly declare a primitive equality > operator for an untagged record type after the type has been used as > the parent type of a derived type? No, because it removes the possibility to make a type that revives the predefined equality, which can be useful in some cases. **************************************************************** From: Christoph Grein Sent: Friday, November 8, 2019 5:56 AM > I think that the one who writes this is knowledgeable enough to do it > on purpose, presumably precisely because he doesn't want to inherit > the "=" operation. May be. True is that the behaviour is under full control of the developer. >> Should it be illegal to explicitly declare a primitive equality >> operator for an untagged record type after the type has been used as >> the parent type of a derived type? This proposal seems worth a consideration. I do not know how often this remote language feature is used, but I guess Steve's new rule would not produce too many incompatibility problems. > No, because it removes the possibility to make a type that revives the > predefined equality, which can be useful in some cases. Since the behaviour is under full control of the developer, he can, if this seems to him to be useful, define a different sequence of derivation. **************************************************************** From: Tucker Taft Sent: Friday, November 8, 2019 7:32 PM I agree with JP. The current rule is OK, as it provides the flexibility you sometimes need to create a type that has access to the predefined operation. **************************************************************** From: Steve Baird Sent: Friday, November 8, 2019 1:08 PM > I think that the one who writes this is knowledgeable enough to do it > on purpose, Yes, but what about the unfortunate soul who comes along later and has to read/understand this code? The argument that it is ok to use cryptic, easily misunderstood coding idioms deliberately as long as appropriate comments are provided does not carry as much weight in a language like Ada which is intended to stress readability (and not in the way that a convoy of heavy trucks might stress a bridge). I think a better argument for the status quo is that this is a corner case and nobody cares. Yes, the construct is broken but no, it is not worth fixing. > The current rule is OK, as it provides the flexibility you sometimes need to create a type that has access to the predefined operation. I think there are more readable workarounds available in the rare case where someone needs to do this. IMO, early derivation (where we derive from a type before some property or operation of the parent type is specified) is not an idiom that we want to encourage. It interacts particularly badly with formal derived types. **************************************************************** From: Tucker Taft Sent: Friday, November 8, 2019 1:31 PM Anyone who defines their own "=" operator is already operating at a pretty high level. I would say it is actually quite common to want to be able to use the predefined "=" operator when defining a user-defined equality, as a quick check" for trivial equivalence before looking for a more subtle equivalence. YMMV! **************************************************************** From: Jean-Pierre Rosen Sent: Friday, November 8, 2019 2:03 PM > Yes, but what about the unfortunate soul who comes along later and has > to read/understand this code? I find nothing surprising or counterintuitive in a derived type having the properties from the parent type that are declared at the point of derivation... **************************************************************** From: Randy Brukardt Sent: Friday, November 8, 2019 2:54 PM > Anyone who defines their own "=" operator is already operating at a > pretty high level. I would say it is actually quite common to want to > be able to use the predefined "=" > operator when defining a user-defined equality, as a quick check" for > trivial equivalence before looking for a more subtle equivalence. Surely, but that can easily be done without using "early derivation". Specifically: package Pkg3 is type Priv is private; procedure Test; private type Original is record Aa, Bb : Integer; end record; -- Has the default "=". type Priv is new Original; function "=" (L, R : Priv) return Boolean; end Pkg3; [This was Christoph's point, but he didn't show the example.] This is far less confusing way to get access to the default "=" than using early derivation. I view early derivation as a pathology; it has no use that can't be accomplished in a less tricky way. There's a reason that it is banned for tagged types (which "work right", according to the Ada 9x team) equality composition is about making that true for untagged records, too. Note that the "early derivation" trick can only be used in a single declaration list, so ordering the declarations differently is always possible. As Steve noted, one "solution" here is to do exactly that, declare Steve's original example a pathology and ignore it (thus no ACATS tests). But the original reason for 4.5.2(9.8/4) was to prevent cases like this where its unclear what version of "=" should be used (especially important for composition, which is why the rule was added in AI05-0123-1). Steve has merely pointed out that it isn't doing it's intended job in this one case, and it makes sense to fix it to work as intended rather than torturing Ada readers and Ada implementers with a tricky, obscure usage. **************************************************************** From: Randy Brukardt Sent: Friday, November 8, 2019 3:05 PM > > Yes, but what about the unfortunate soul who comes along > later and has > > to read/understand this code? > I find nothing surprising or counterintuitive in a derived type having > the properties from the parent type that are declared at the point of > derivation... Exactly. That's not how record equality works anywhere *except* in this one case (in all other cases, the redefined body is used regardless of where it is declared). That's what makes this confusing. In any case, untagged derivation itself is nearly a pathology. Virtually all of the useful cases are banned by 13.1(10). And almost all of the rest of them are attempting to get the effect of type renaming (a concept that Ada should have instead, IMHO). Generic formal untagged derived types are nearly useless (and virtually unimplementable in Janus/Ada) as a consequence, which doesn't make much sense, either. I'd be happy to agree that Steve's example is a pathology that isn't worth having a rule for, but I'm strongly opposed to making any implementer make it work as Steve (and apparently you) claims it should. Cases like that were explicitly banned by AI05-0123-1, and it's clear that we just plain missed one. No one should have to waste time on pathologies. **************************************************************** From: Joey Fish Sent: Saturday, November 9, 2019 9:02 PM > In any case, untagged derivation itself is nearly a pathology. Virtually all > of the useful cases are banned by 13.1(10). And almost all of the rest of > them are attempting to get the effect of type renaming (a concept that Ada > should have instead, IMHO). It seems like the argument to keep the behavior is pretty much rooted in the need for accessing the default operators; as one possible solution we could introduce an aspect or attribute for such: Type Example is record A, B : Integer; end record; Type Derived is new Example; Function "="(Left, Right : Example) return Boolean; Function "="(Left, Right : Derived) renames "="'Default; -- or maybe: -- Function "="(Left, Right : Derived) return Boolean -- with Default => True; This has the advantage of being explicit, if ugly, and could make things a little easier on the implementer if we eliminate the ability of the derived type to escape the definition of an operator of its parent. And, in the case of using an aspect, it could be used for easier stubbing (and could return the default for the return type when applied to functions)... though that would have better proposed as an alternative to the "is null" of procedures and might be a little more dangerous than we want to accept. **************************************************************** From: Tucker Taft Sent: Sunday, November 10, 2019 7:36 AM You have convinced me. I agree we could make early derivation illegal in this oddball case, since you can choose instead to define the "=" operator on the derived type, rather than on the parent type, if you want them to differ. **************************************************************** From: Jean-Pierre Rosen Sent: Sunday, November 10, 2019 12:49 PM +1 **************************************************************** From: Richard Wai Sent: Sunday, November 10, 2019 10:21 AM I think this might be a case where we don’t want enable weird behaviour, since it doesn’t come with any real value. I think Randy is on point that this kind of corner case shouldn’t have been legal in the first place, and it was simply overlooked. It’s just not useful enough to be able to derive a untagged type and then declare a predefined operation for the ancestor type after the fact. By simply not allowing the user to do that kind of thing in the first place, they are encouraged to rethink their approach, which would usually amount to a simple reorganization of their declarations. I this particular case, it also helps with readability. ****************************************************************