Version 1.3 of ai12s/ai12-0352-1.txt
!standard 4.5.2(9.8/4) 20-01-16 AI12-0352-1/02
!class binding interpretation 20-01-10
!status Amendment 1-2012 20-01-15
!status ARG Approved 11-0-2 20-01-15
!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
--
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 declaration of 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 for 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.
!corrigendum 4.5.2(9.8/4)
Replace the paragraph:
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 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.
by:
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 declaration of 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.
!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.
****************************************************************
Questions? Ask the ACAA Technical Agent