AI22-0138-1

!standard 6.3.1(21/4)                                    25-08-15  AI22-0138-1/03

!class Binding Interpretation 25-07-15

!status Amendment 1-2022  25-07-30

!status WG9 Approved 25-10-08

!status ARG Approved  14-0-2  25-07-30

!status work item 25-07-15

!status received 25-03-04

!assigned author Randall Brukardt

!submitter Stephen Baird

!priority Low

!difficulty Easy

!qualifier Omission

!subject Inheritance and conformance

!summary

For the purposes of the requirement of 6.3.1(21/4), we allow different inherited declarations to conform so long as the original non-inherited ancestor operation is the same.

!issue

6.3.1(21/4) requires all direct_names in an expression that is required to conform to refer to the same declaration. When subprograms are inherited from an ancestor, each such inheritance creates a different declaration.

In particular, a private type that inherits a subprogram and a full type that inherits a homograph of that subprogram have two separate declarations for the inherited subprograms (even in the case when it is the same subprogram that is being inherited).

This means that uses of such inherited declarations in a default expression can never conform. Is this intended? (No.)

!recommendation

Ada allows overriding subprograms to have different details, such as parameter names and default expressions. Clearly, conformance of such differences cannot be allowed (the runtime behavior of the different default expressions could be different, and that should never be considered the same). Thus, any change to the 6.3.1(21/4) rule needs to be narrowly focused.

On the other hand, a strict reading of the 6.3.1(21/4) rule means that for a private extension, default expressions that contain an operation of the extension can never conform unless the named operations are explicitly overridden, since the full type necessarily provides a second (different) set of inherited operations. That would be unexpected in the case when the same routine is inherited by the private extension and its completing full type.

Therefore, we are suggesting a narrow improvement to the 6.3.1(21/4) rule which allows routines which are different inherited declarations to conform if they are inherited from the same (non-inherited) ancestor routine and they are type-conformant.

!wording

Modify 6.3.1(21/4):

[Editor’s note: I cannot think of a case where the “type-conformant” part would be needed, but since inheritance changes some of the types of the parameters and result, I wasn’t comfortable not having something to ensure that all of the types are the same for both inherited declarations.]

AARM Discussion: For inherited declarations which are not the same, we only want to require that they both are inherited from the same non-inherited declaration; how many times the subprogram is inherited should not matter. We need this rule so that subprograms inherited by private extensions can be used in default expressions in the same specification (otherwise, the subprograms inherited by the full [completing] type are necessarily different than those of the partial view and would violate this rule).

Requiring the two inherited declarations to be type-conformant is sufficient to ensure a complete match, since descending from the same original declaration ensures that the modes, default expressions, parameter names, and other properties match. End AARM Discussion.

[Editor’s note: This AARM note follows the existing notes.]

!discussion

This issue arose when it was noted that different Ada compilers have different behavior on this question.

An ACATS-style test was constructed to determine the exact behavior of the different compilers.

GNAT rejects cases 3, 9, 10, and 11, allowing all of the rest. ObjectAda and Janus/Ada reject all eleven cases (as would be expected from a strict reading of 6.3.1(21/4)).

We do not believe that cases 2 and 4 should be accepted. For case 2, the visible F does not have a parameter Obj1, so the expression should not even resolve. For case 4, the visible G has a different default parameter value than the one called in the original specification. As such, a different result would occur depending on which default expression was used in an actual call of P3D. The purpose of conformance is to prevent such confusion by making such cases illegal.

As such, the GNAT implementation is wrong. But we need to consider if we would be introducing problems if we strictly enforced the rule as written. Consider cases 7 and 8 in the example below. This is essentially the structure of any private extension, and it would be clearly annoying for these cases to be illegal.

Let’s look at case 7 in more detail. Here is the appropriate part of the specification for that case (see the Example section for the complete example).

with B6310RB1; private with B6310RB2;
package B6310RB3 is
   ...
   type T5 is new B6310RB1.T1 with private;
       -- Here, F is inherited and specifies B6310RB1.F.
       -- Similarly, G is inherited and specifies B6310RB1.G.
   T5_Null_Obj : constant T5;
   procedure P5A (X : in Integer := F (T5_Null_Obj));
   procedure P5B (X : in Integer := F (Obj1 => T5_Null_Obj));

private
   ...

   type T5 is new B6310RB1.T1 with null record;
       -- Here, F and G are (re)inherited but still specify
       -- B6310RB1.F and B6310RB1.G.
   T5_Null_Obj : constant T5 := (null record);
end;

Here, both the partial view and the full view inherit (separately), the routine B6310RB1.F. 8.3(12) makes it crystal clear that in such a case, the second inherited subprogram overrides the first (and makes the first hidden from all visibility). As such, the declaration referred to by F in the specification of procedure P5A (that of the routine inherited by the partial view) and by the body of procedure P5A (that of the routine inherited by the full view) are clearly different.

Cases  5 and 6 are similar, but there is another level of inheritance involved. Still, the differently inherited routines are ultimately inherited from the same (non-inherited) subprogram declarations. It certainly is more friendly to users to allow these cases.

Charitably, one can assume that the GNAT implementation was trying to avoid this problem, and simply went too far. Simply confirming the rule would introduce a practical incompatibility for users of GNAT. While there is an easy workaround (define an overriding of F in the visible part), it requires otherwise unnecessary changes and clutter. Thus, we craft a rule to allow cases 5 through 8.

We considered that it might make sense to allow case 1, as the same body is ultimately called. But that can only make sense when the parameter names and default expressions are unchanged (as demonstrated by cases 2, 3, and 4). A rule to allow this case would be quite complex and doesn’t seem worth the effort. After all, the entire problem can be avoided by putting an overriding subprogram into the specification (the inherited routines are then overridden and hidden from all visibility, so they don’t participate in the resolution of the default expressions).

However, it also was suggested that these cases are very unlikely and thus represent a pathology that should not be decided. Derivations like these are not unlikely; it has been pointed out that Claw has such derivations (but does not have any default expressions like these). Default expressions are used often enough that it seems that this structure would be encountered in real programs. Moreover, the straightforward example of cases 7 and 8 show that this issue applies to any private extension (regardless as to what the ancestor of the full type is). Thus, we rejected the idea that these are pathologies, and therefore a decision and solution is needed, in order to avoid unnecessary portability differences between Ada implementations.

!example

Following is the complete ACATS-style test discussed in the !discussion.

-- A ACATS-style test program for a question posed by Steve Baird
-- on June 17th, 2022.
-- Updated to add his secondary case on December 31st, 2024.
-- Updated with cases with parameter names noted by Tucker Taft on
-- March 3rd, 2025.
-- Updated with “normal” private extension cases on July 15th, 2025.

package B6310RB1 is
   type T1 is tagged null record;

   function F (Obj1 : in T1) return Integer;

   function G (Obj1 : in T1; Count : in Natural := 0) return Integer;

end B6310RB1;

package body B6310RB1 is
   function F (Obj1 : in T1) return Integer is
   begin
      return 0;
   end F;

   function G (Obj1 : in T1; Count : in Natural := 0) return Integer is
   begin
      return Count+1;
   end G;
end B6310RB1;

with B6310RB1;
package B6310RB2 is
   type T2A is new B6310RB1.T1 with null record;

   function F (Obj2A : in T2A) return Integer;

   function G (Obj2A : in T2A; Count : in Natural := 1) return Integer;

   type T2B is new B6310RB1.T1 with null record;
   

   type U1 is private;
   N1 : constant U1;
private
   type U1 is array (1..2) of Integer;
   function "=" (A, B : U1) return Boolean;
   N1 : constant U1 := (55, 77);
end B6310RB2;

package body B6310RB2 is
   function F (Obj2A : in T2A) return Integer is
   begin
      return 1;
   end F;

   function G (Obj2A : in T2A; Count : in Natural := 1) return Integer is
   begin
      return Count;
   end G;
   

   function "=" (A, B : U1) return Boolean is
   begin
       return A(1) = B(1);
   end "=";
end B6310RB2;

with B6310RB1; private with B6310RB2;
package B6310RB3 is
   type T3 is new B6310RB1.T1 with private;
       -- Here, F is inherited and specifies B6310RB1.F.
       -- Similarly, G is inherited and specifies B6310RB1.G.
   T3_Null_Obj : constant T3;
   procedure P3A (X : in Integer := F (T3_Null_Obj));
   procedure P3B (X : in Integer := F (Obj1 => T3_Null_Obj));
   procedure P3C (X : in Integer := F (Obj1 => T3_Null_Obj));
   procedure P3D (X : in Integer := G (T3_Null_Obj));

   type T4 is new B6310RB1.T1 with private;
       -- Here, F is inherited and specifies B6310RB1.F.
       -- Similarly, G is inherited and specifies B6310RB1.G.
   T4_Null_Obj : constant T4;
   procedure P4A (X : in Integer := F (T4_Null_Obj));
   procedure P4B (X : in Integer := F (Obj1 => T4_Null_Obj));

   type T5 is new B6310RB1.T1 with private;
       -- Here, F is inherited and specifies B6310RB1.F.
       -- Similarly, G is inherited and specifies B6310RB1.G.
   T5_Null_Obj : constant T5;
   procedure P5A (X : in Integer := F (T5_Null_Obj));
   procedure P5B (X : in Integer := F (Obj1 => T5_Null_Obj));

private
   type T3 is new B6310RB2.T2A with null record;
       -- Here, F is (re)inherited and specifies B6310RB2.F.
       -- Similarly, G is (re)inherited and specifies B6310RB2.G.
   T3_Null_Obj : constant T3 := (null record);
   

   type T4 is new B6310RB2.T2B with null record;
       -- Here, F and G are (re)inherited but still ultimately
       -- specify B6310RB1.F and B6310RB1.G.
   T4_Null_Obj : constant T4 := (null record);

   type T5 is new B6310RB1.T1 with null record;
       -- Here, F and G are (re)inherited but still specify
       -- B6310RB1.F and B6310RB1.G.
   T5_Null_Obj : constant T5 := (null record);
end;

package body B6310RB3 is
   procedure P3A (X : in Integer := F (T3_Null_Obj)) is -- ERROR:
        -- (1) Two different bodies are referenced by F in the
        --     specification and F here. The same body would actually be
        --     executed, but it would be odd for legality to depend on
        --     the actual dynamic semantics.
   begin
      null;
   end P3A;

   procedure P3B (X : in Integer := F (Obj1 => T3_Null_Obj)) is -- ERROR:
        -- (2) Obj1 is not the name of a parameter for F (B6310RB2.F).
   begin
      null;
   end P3B;

   procedure P3C (X : in Integer
                        := F (Obj2A => T3_Null_Obj)) is -- ERROR:
        -- (3) Obj2A does not denote the same (parameter) declaration as
        --     Obj1 in the declaration of P3C, thus 6.3.1(21/4) is
        --     violated.
   begin
      null;
   end P3C;

   procedure P3D (X : in Integer := G (T3_Null_Obj)) is -- ERROR:
        -- (4) G should not conform, since it has a different default
        --     parameter value here as compared to the one in the
        --     original P3D specification.
   begin
      null;
   end P3D;

   procedure P4A (X : in Integer := F (T4_Null_Obj)) is -- Now OK.
        -- (5) Here we have two different (inherited) declarations of F,
        --     but both designate the same body with the same details.
   begin
      null;
   end P4A;

   procedure P4B (X : in Integer
                         := F (Obj1 => T4_Null_Obj)) is -- Now OK.
        -- (6) As previous, we have two different declarations of F, but
        --     both designate the same body with the same details.
   begin
      null;
   end P4B;

   procedure P5A (X : in Integer := F (T5_Null_Obj)) is -- Now OK.
        -- (7) Here we have two different (inherited) declarations of F,
        --     but both were inherited from the same declaration. It
        --     would be annoying if these did not conform.
   begin
      null;
   end P5A;

   procedure P5B (X : in Integer
                         := F (Obj1 => T5_Null_Obj)) is -- Now OK.
        -- (8) As previous, we have two different declarations of F, but
        --     but both were inherited from the same declaration.
   begin
      null;
   end P5B;

end B6310RB3;

package B6310RB2.Child is
   M1 : constant U1;
   procedure P6A (A : Boolean := (N1 = M1));
       -- Here, we're calling the predefined "=" with parameters
       -- Left and Right.

   procedure P6B (A : Boolean := ("="(Left => N1, Right => M1)));
       -- Here, we're calling the predefined "=" with parameters
       -- Left and Right.

   procedure P6C (A : Boolean := ("="(Left => N1, Right => M1)));
       -- Here, we're calling the predefined "=" with parameters
       -- Left and Right.
private
   M1 : constant U1 := (55, 88);
end B6310RB2.Child;

   

package body B6310RB2.Child is
   procedure P6A (A : Boolean := (N1 = M1)) is -- ERROR:
       -- (9) This should not conform. Here, we're calling the overriding
       --     "=" with parameters A and B. Moreover, the two "=" will
       --     call separate bodies that may get different answers. This
       --     is the purpose of conformance checks.
   begin
       if A then
           raise Program_Error;
       end if;
   end P6A;

   procedure P6B (A : Boolean
                  := ("="(Left => N1, Right => M1))) is -- ERROR:
       -- (10) The "=" visible here does not have parameters Left and
       --      Right.
   begin
       if A then
           raise Program_Error;
       end if;
   end P6B;

   procedure P6C (A : Boolean := ("="(A => N1, B => M1))) is -- ERROR:
       -- (11) As with (3), A does not denote the same (parameter)
       --      declaration as Left in the declaration of P5C, thus
       --      6.3.1(21/4) is violated.
   begin
       if A then
           raise Program_Error;
       end if;
   end P6C;
end B6310RB2.Child;

with B6310RB3, B6310RB2.Child;
procedure B6310RB is
begin
    B6310RB3.P3A;
    B6310RB3.P3B;
    B6310RB3.P3C;
    B6310RB3.P3D;
    B6310RB3.P4A;
    B6310RB3.P4B;
    B6310RB3.P5A;
    B6310RB3.P5B;
    B6310RB2.Child.P6A;
    B6310RB2.Child.P6B;
    B6310RB2.Child.P6C;
end B6310RB;

!corrigendum 6.3.1(21/4)

@drepl

@xbullet{each @fa{direct_name}, @fa{character_literal}, and @fa{selector_name} that is not part of the @fa{prefix} of an expanded name in one denotes the same declaration as the corresponding @fa{direct_name}, @fa{character_literal}, or @fa{selector_name} in the other, or they denote corresponding declarations occurring within the two expressions; and}

@dby

@xbullet{each @fa{direct_name}, @fa{character_literal}, and @fa{selector_name} that is not part of the @fa{prefix} of an expanded name in one denotes the same declaration as the corresponding @fa{direct_name}, @fa{character_literal}, or @fa{selector_name} in the other, they both denote type-conformant inherited subprograms which are ultimately inherited from the same noninherited declaration, or they denote corresponding declarations occurring within the two expressions; and}

!ACATS test

The needed ACATS test is found in the Example section above.

!appendix

This AI was created from the ARG Github Issue #129 (https://github.com/Ada-Rapporteur-Group/User-Community-Input/issues/129).