AI22-0053-1

!standard 4.5.2(9.8/5)                                    23-09-07  AI22-0053-1/05

!standard 4.5.2(13)

!class binding interpretation 22-10-27

!status Corrigendum 1-2022  23-06-26

!status ARG Approved  6-0-0  23-06-11

!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

Modify 4.5.2(9.8/5):

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. {If the untagged record type is declared immediately within the visible part of a package, and the overriding primitive equality operator is explicitly declared within the private part of the package, the operator shall be subtype conformant with the predefined or inherited operator that it overrides.} 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.

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.

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.

We require subtype conformance when the overriding occurs in the private part, to ensure that the same checks, if any, are performed at the call site.

!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 permanently deleted, this useful comment disappeared, too, so I moved it down here.]