AI22-0053-1

!standard 4.5.2(13)                                    23-03-23  AI22-0053-1/03

!standard 4.5.2(15/5)

!class binding interpretation 22-10-27

!status work item 22-10-27

!status received 22-07-01

!priority Low

!difficulty Easy

!qualifier Omission

!subject An unintended consequence of AI12-0101-1

!summary

If an untagged record type overrides "=" in a private part, that overriding equality is

used for all calls to "=", unless they are within the immediate scope of the type and precede the overriding declaration

!issue

Suppose you have an untagged record type declared (completely; no partial view) in the visible part of a package and an overriding equality operator for it declared in the private part. At one point, that was illegal. AI12-0101-1 made it legal, but failed to adjust the dynamic semantics, illustrated by the example below.

procedure Test53 is
   pragma Assertion_Policy (Check);

   package Pkg is
          type Untagged_Rec is record
             X, Y : Integer;
          end record;

          type Tagged_Rec is tagged record
             X, Y : Integer;
          end record;
   private
          function "=" (L, R : Untagged_Rec) return Boolean is (L.X = R.X);
          function "=" (L, R : Tagged_Rec) return Boolean is (L.X = R.X);
   end Pkg;
   use Pkg;
   
   U1 : constant Untagged_Rec := (0, 111);
   U2 : constant Untagged_Rec := (0, 222);
   T1 : constant Tagged_Rec := (0, 111);
   T2 : constant Tagged_Rec := (0, 222);

   U_Eq : constant Boolean := U1 = U2;
   T_Eq : constant Boolean := T1 = T2;

   type U_Wrapper is record F : Untagged_Rec; end record;
   type T_Wrapper is record F : Tagged_Rec;   end record;

   Uw_Eq : constant Boolean := U_Wrapper'(F => U1) = (F => U2);
   Tw_Eq : constant Boolean := T_Wrapper'(F => T1) = (F => T2);
begin
   -- 3 out of 4 dentists recommend user-defined equality
   pragma Assert (not U_Eq);
   pragma Assert (T_Eq);
   pragma Assert (Uw_Eq);
   pragma Assert (Tw_Eq);
end Test53;

 

The !discussion section for AI05-0123-1 very explicitly addresses this point:

It would be pretty weird if the directly called equality didn't agree with the composed equality. For example, a call to the "=" operator for an untagged record type R ought to yield the same answer as a corresponding call to the predefined "=" operator for a one-field record type whose one component is of type R.

An argument was made in AI12-0101-1 that no changes in dynamic semantics were needed, but that argument ignored the case where the view declared in the visible part is not a partial view. The following is from the !discussion section of AI12-0101-1:

If we simply delete the second sentence of 4.5.2(9.8/3) (as proposed here), then 4.5.2(15/3) comes into effect and any overriding of "=" [before freezing] is used as the definition of "=" everywhere (even where the overriding "=" is not visible).

Note that there are no partial views of anything in the preceding example, so 4.5.2(15/5) does not come into play at all.

Note that we also need to worry about a case where the untagged record type that defines its own “=” operator is derived from an ancestor that also overrode the predefined “=” operator.  At what point does the newly overridden operator take over the semantics of the inherited “=” operator?  Clearly any new parameter names used in the newly overridden operator can only be used when the caller has visibility on the overriding.  But the intent of this AI is that even when these new parameter names are not visible, a call on the still visible inherited operator (perhaps using named notation “=”(El => A, Are => B)) would nevertheless invoke the function body associated with the new overriding, while using the parameter names of the inherited “=” operator.

!recommendation

(See Summary.)

!wording

Add after 4.5.2(13):

For an untagged record type, when outside the immediate scope of the type, any call on a primitive equals operator conformant with the profile of the predefined equals operator of the type, will invoke the body associated with the corresponding primitive equals operator visible at the end of the immediate scope of the type.

[Author's Note: Type extensions already have a separate rule in 4.5.2(14/3), and we have the private type rule in 4.5.2(15/3). It seems to make the most sense to have this new rule be separate and precede the existing rules.]

AARM Reason: A hidden overriding of "=" is used for all equality operators conformant to predefined equality  of the type, so that composition works sensibly. We say "outside the immediate scope of the type" so that a squirreling rename can be used to make the actual predefined equality visible if that is needed.

!discussion

There are two ways to fix this problem.

We could ban such overrides, as they are unusual and likely a mistake. That was the solution given in AI05-0123-1. But the solution proved to be too incompatible, and thus it was repealed by AI12-0101-1. Putting back part of it would seem to simply be repeating the previous mistake. Thus we rejected that alternative.

The other option is to add a rule like 4.5.2(15/5) that applies in this case. This fixes the problem without creating a new (compile-time) incompatibility, but it does create a new inconsistency (a runtime incompatibility). In particular, existing client code would now get the (hidden) overriding "=" rather than the predefined "=". AI12-0101-1 argues fairly persuasively that this inconsistency is most likely fixing a bug.

The proposed rule only hides predefined equality after the freezing point of a type. Any attempt to call equals will freeze the associated type, so this does not cause any anomalies. But it does allow using a squirreling rename to name the actual predefined equality for an untagged record type. This provides a work-around in the unusual case that one needs both the actual predefined equality and the overriding equality.

Note that the proposed rule only changes the effect of "=" for an untagged type declared in the visible part of a package specification, and which has an overriding "=" declared in the private part of that specification. Tagged types already call the overriding body regardless of where it appears, and if "=" is visibly overridden, the overriding body is always used for the call of "=" as well.

!ACATS test

An ACATS C-Test should check that an example like that in the !Issue gives the

expected results.

!appendix

This issue was originally raised by Steve Baird privately.


 

Comment from Tucker Taft, March 23, 2023, 12:06 AM

The third paragraph of the !discussion started:

The other option is to add a rule like 4.5.2(15/5) that applies in this case. This is OK as we do not allow overriding (of anything) after the type is frozen.

The highlighted (second) sentence is only true for tagged types.  We allow overriding of primitives of non-tagged types even after they are frozen (see 13.14(16)).

The offending sentence was deleted.

[Editor’s note: When the associated text was deleted, this useful comment disappeared, too, so I moved it down here.]