Version 1.3 of ai12s/ai12-0352-1.txt

Unformatted version of ai12s/ai12-0352-1.txt version 1.3
Other versions for file 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 -- 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 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