AI22-0150-1

!standard 4.1.6(5.2/7)                                   26-04-30  AI22-0150-1/06

!class Binding Interpretation 26-01-27

!status Revision-202Y  26-02-20

!status ARG Approved  6-0-0  26-02-19

!status work item 26-01-27

!status received 26-01-27

!assigned author Randy Brukardt

!submitter Randy Brukardt

!priority Medium

!difficulty Medium

!qualifier Omission

!subject User-defined indexing and access types

!summary

Indexing aspects are unspecifiable for access types with certain kinds of designated types; they are allowed for other access types so long as they complete a private type.

!issue

AI22-0141-1 defines some aspects as “unspecifiable” for some classes of types. In particular, user-defined indexing is not allowed on array types, nor can a private type that has user-defined indexing be completed by an array type.

However, user-defined indexing takes a different tack with access types. Even though user-defined indexing is not allowed to be defined directly on access types, it is allowed on private types completed by access types. This causes a number of anomalies, some considered in the original discussion of user-defined indexing, and some not considered. Additionally, the changes to the prefixed view rules of AI22-0149-1 also requires reconsideration of these rules (especially as some anomalies are removed). See the discussion below for details.

It appears that at least some of the problematic cases need rules.

!recommendation

See Summary.

!wording

Add after 4.1.6(5.2/7):

The Constant_Indexing and Variable_Indexing aspects are unspecifiable for an access type whose designated type is any of the following:

AARM Ramification: Indexing aspects are nonoverridable, so they cannot be hidden (see 13.1.1), so this rule does not (nor needs to) break privacy.

AARM Ramification: This rule does not apply when the full type is visible, so in particular it does not apply to private types whose full type is declared in the same scope before the type we are checking. Thus, this primarily applies to private types declared in nested packages.

AARM Reason: We do not allow indexing aspects to be specified for an access type if there is any place where the designated type defines (or might define, in the case of private or incomplete types) any indexing operations on the access type. We do not want conflicts between indexing operations declared in various places (leading to unresolvable ambiguities), nor do we want the potential confusion from needing to look in many places to figure out what an indexing actually does. This rule prevents these sorts of access types as the completion of a private type that has an indexing aspect; direct specification of indexing aspects on access types is prevented by the previous rule.

!discussion

Why indexing aspects are unspecifiable for some types

Indexing aspects are unspecifiable for array types. Why is that? Array types have predefined indexing operations. We could have allowed the indexing operations to be additive, but we felt that would be confusing to users and more difficult for implementers to implement. In particular, if the operations were additive, a reader would have to look at the union of the array declaration and the specifications of indexing aspects. Additionally, any conflicting indexing would be ambiguous. It’s likely that the most important indexing would be defined on both types, leading to a situation where the indexing is critically needed, but unavailable.

Moreover, implementations would have to look in both places (which could be difficult, as predefined array indexing and prefixed calls are likely to be handled by different, independent code in a compiler), as well as being able to diagnose ambiguities (if both the indexing aspects and the array declaration defined a possible indexing for a particular type). Finally, it was noted that it would be possible to relax this restriction in the future, but impossible to add it later if not originally adopted.

“Unspecifiable” is specified so that there is no place where more than one indexing definition is visible for a type. (In the rare case of a type declared in the private part of a generic unit, the type might have different indexing definitions for the formal and actual types, but there would be no place where both are visible.) We need to preserve this in all cases.

Access types and indexing aspects

We have made specifying indexing of a normal access type illegal (see 4.1.6(5.2/7)), because the underlying prefixed view would be illegal (as confirmed by AI22-0149-1). However, AI22-0149-1 carved out an exception for access types that complete a private type. The 4.1.6(5.2/7) rule allows this case.

The most basic example of this is the case of a private type completed by an access type with a simple designated type. For instance:

package P1 is
    type Priv1 is private with
         Constant_Indexing => Foo;
    function Foo (Obj : in Priv1; N : in Natural) return Natural;
    C : constant Priv1;
    procedure Sink (N : in Natural := C(1)); -- (A)
private
    type Priv1 is access Natural;
    C : constant Priv1 := new Natural’(2);
    Obj : Natural := C(1); -- (B)
end P1;

[Aside: In these examples, we are only showing Constant_Indexing as it allows simpler examples, but similar examples can be constructed for Variable_Indexing. We are using default expressions to show interesting expressions in the public part of the package specification, as these avoid call-before-body-elaboration errors. But these same expressions could occur in client code with the same meaning; and such uses are more interesting in practical cases. Similarly, we are showing expressions in the private part of a package; the same expressions would have the same meaning in the body of the package, which is more likely in practice.]

In this case, the indexing at (A) is legal and would be “equivalent to” the prefix call C.Foo(1). The original expectation from AI22-0091-1 was that the call at (B) was illegal. However, that caused Beaujolais-like issues and thus was changed by AI22-0149-1. So now the indexing at (B) is legal.

So far, we don’t have any issues. But recall that we do have the possibility of an implicit dereference for operations (like indexing) for an access type. Thus the operations of the designated type come into play.

The simplest case of this becoming a problem is when the designated type is an array type:

package P2 is
    type Priv2 is private with
         Constant_Indexing => Foo;
    function Foo (Obj : in Priv1; N : in Natural) return Natural;
    C2 : constant Priv2;
    procedure Sink (N : in Natural := C2(1)); -- (C)
private
    type Arr is array (1..10) of Natural;
    type Priv2 is access Arr; -- (E) - Illegal by this proposal
    C2 : constant Priv2 := new Arr’(others => 10);
    Obj2 : Natural := C2(1); -- (D)
end P2;

The indexing at (C) is well-defined, and is a call to P2.Foo. But the indexing at (D) could be the array indexing C2.all(1) or the call C2.Foo(1). Thus the usage would have to be ambiguous. Clearly, we need to make this case (the completion at (E)) illegal presuming that we want to stick to the original principles (this is the first bullet of the proposed wording). [In the rest of this section, we’ll assume that we are sticking to the original principle; we’ll look later at some options for relaxing them.]

Similarly, if the designated type is an indexable container type (which just is a fancy way of saying that the type has one or both indexing aspects defined), we could have a similar conflict. For example:

with P1;
package
P3 is
    type Priv3 is private with
         Constant_Indexing => Foo;
    function Foo (Obj : in Priv3; N : in Natural) return Natural;
    C3 : constant Priv3;
    procedure Sink (N : in Natural := C3(1)); -- (F)
private
    type Priv3 is access P1.Priv1; -- (G) - Illegal by this proposal
    C3 : constant Priv3 := new P1.Priv1’(P1.C);
    Obj3 : Natural := C3(1); -- (H)
end P3;

Here again, the indexing at (H) would be ambiguous, as it could mean P1.Foo(C3.all, 1) or P3.Foo(C3, 1). Thus we make (G) illegal as well (this is the second bullet of the proposed wording).

Unfortunately, there are other cases we have to worry about. If the designated type is a private type, we would have to worry about the possibility that the full type has an array type and that there is some place where that is visible. (Since indexing aspects are nonoverridable, they cannot be hidden, and thus we don’t need to worry about the case where the full type has indexing aspects that are not on the private type.)

We need to consider the various cases. For unrelated units, there is no chance of getting visibility on the full type. We cannot with child units of ourselves (this would create a circular semantic dependency). Private types directly declared in ancestor units other than the one we’re compiling will always have the full type visible in our private part (where the full declaration is occurring), so we don’t need a special rule for that case. Private types declared in nested units of ancestor units other than the one we’re compiling will never have the full type visible outside of the ancestor unit, and the full type of our private type cannot be visible in the body of the ancestor (since it will be not visible from a with clause).

But we can construct problem cases for private types declared in nested packages of our unit. For example:

package P4 is
   package Nested is
      type Priv4N is private;
      C4N : constant Priv4N;
   private

      type Priv4N is array (1..10) of Natural;
      C4N : constant Priv4n := (others => 66);
   end Nested;
   type Priv4 is private with
       Constant_Indexing => Foo;
   function Foo (Obj : in Priv4; N : in Natural) return Natural;
   C4 : constant Priv4;
private
   type Priv4 is access P4.Nested.Priv4N; -- (J) - Illegal as proposed
   C4 : constant Priv4 := new P4.Nested.Priv4N’(P4.Nested.C4N);
   Obj4 : Natural := C4(1)); -- (K)
end P4;

package body P4 is
   package body Nested is
      procedure Fooey is

         Obj4N : Natural := C4(1)); -- (L)
      begin
         null;
      end;
   end Nested;
end P4;

The indexing at (K) has no problem, the full type for Priv4N is not visible. But the indexing at (L) has both the array indexing for the designated type and the user-defined indexing visible. So we have the problem again.

This example looks pretty unlikely. But the problem here is that if we don’t disallow this case (J) somehow, then implementers will have to implement all of the painful implementation cases for this corner case. If we’re going to make them do that work for a corner case like this, we probably ought to have them do it for all cases (and forget most of the unspecifiable restrictions for indexing aspects). That seems to be a bridge too far in this case, thus we need a rule to make case (J) illegal (that is the third bullet of the wording).

One would hope that this corner case is the worst we could see, but unfortunately there is one more way to declare a type needing a (hidden) completion. This one is highly restricted, but the one place it always is allowed to be used is the designated type of an access type. We, of course, are talking about the incomplete type and its sibling, the incomplete view.

While the places where one can see the completion of a private type is quite limited, you can get access to the completion of a limited view with just a with clause. And of course there is no limit to where this completion can be accessed – thus anywhere the full type is visible is at risk, including in the private parts and bodies of any descendant units, and anywhere in private descendant units.

That means that if the completion has a conflicting indexing, we will have the problem again. For example:

package P5 is
   type Arr5 is array (1..10) of Natural;
   C5 : constant Arr5 := (others => 80);
end P5;

limited with P5;
package P6 is
   type Priv6 is private with
       Constant_Indexing => Foo;
   function Foo (Obj : in Priv6; N : in Natural) return Natural;
private
   type Priv6 is access P5.Arr5; -- (M) - illegal as proposed
end P6;

with P5;
package body P6 is
   C6 : Priv6 := new P5.Arr5’(C5);
   Obj6 : Natural := C6(1); -- (N)
end P6;

The indexing at N could be interpreted as C6.all(1) [an array indexing] or C6.Foo(1) [a call on P6.Foo]. This is the conflict problem all over again, but note that here there is no general way to prevent this. The whole idea of incomplete views is to not need to figure out any of the detailed semantics (depending on syntax only), so determining if an incomplete type has indexing is not possible in general – even when “breaking privacy”. (Consider the simple case where the limited with is making an incomplete view of a type derived from P5.Arr5; in that case, the syntax of the derived type declaration could not be of any help in determining that the actual type is an array type.)

As previously noted, the same thing could occur in any child of P6, including those that will be written far in the future. Without a rule preventing this problem at the declaration, one could have a ticking time bomb waiting to go off for any future maintainer.

Note that this problem can occur for any type of an incomplete type or view where the completion isn’t known when the full type is declared. In particular, a Taft-amendment type (an incomplete type whose completion is in the body) can also run into the issue. Thus we cover all incomplete views with this rule, making (M) illegal (the fourth bullet in the wording).


 

Alternatives to the proposed wording

The proposed wording is rather complex. What other choices do we have?

(1) The nuclear option:

    Modify 4.1.6(5.2/7):

The Constant_Indexing and Variable_Indexing aspects are unspecifiable for array types[. These aspects shall not be specified]{and} for [an] access [type]{types}.

This makes indexing attributes unspecifiable for all access types. This is certainly simple, but some people rejected this approach initially and the updates of AI22-0149-1 make it unnecessary in general. There doesn’t seem to be any important reason to prevent the common case of an access to a record type just because an access to array is a problem.

(2) The other nuclear option:

Delete all of the proposed wording and replace it by nothing. (Optionally, also get rid of the rule against array types.)

This would mean that all of these conflicts are legal and should only be rejected if there is an actual ambiguity. This would make implementors do a lot of new work, but it would allow the maximum utility to these declarations. Users would have to avoid conflicts on their own. In particular, it would allow defining indexing for types that already have it (like array types) so long as that indexing is for a profile different than that of the predefined array type.

This approach has a number of maintenance hazards built in, and would cause a lot of implementation work. We rejected it when generalized indexing was originally defined, it's unclear if enough has changed since to require the extra work.

(3) The write it out option:

Avoid the term unspecifiable, rather start the wording with:

The completion of a private type for which the Constant_Indexing or Variable_Indexing aspect is specified shall be be an access type whose designated type is any of the following:

We would also need an AARM note to verify that this rule is *not* rechecked in the private part of a generic unit (as in that case, there is no place where the properties of the  formal and actual types are both visible).

We didn’t use this as it isn’t any simpler than the original wording, and if in the future any further problems are found with the “unspecifiable” idea, any fix would get automatically applied to these rules (with the wording suggested under this item, the problem would likely persist with this wording).

(4) The pile-of-functions isn’t a problem option:

Delete the second bullet:

The implementation of two conflicting sets of user-defined indexing operations is likely to be relatively simple. Implementations are used to the idea of resolving sets of functions, and most likely, resolving the union of two sets just means merging the sets and proceeding normally.

However, in this case, the most important (to the user) indexing is likely to be declared in both sets, making the indexing ambiguous in that case. Working around that might require avoiding indexing and even prefixed views for this particular operation, damaging the abstraction.

As noted elsewhere, these sorts of restrictions are easily dropped in the future if it turns out this is a problem. As such, we start with the reasonable restrictive version and let experience show if we need to change it.

(5) The break-privacy option:

Replace the third bullet with:

Since the type has to be in the same unit, we could defer the check until the end of the compilation and know the actual full type. So we could check directly if there are any problems. (As noted previously, indexing aspects cannot be hidden, so we don’t need to worry about them for this rule.) However, this is severely privacy breaking; we’ve avoided (almost all) Legality Rules that break privacy and this doesn’t seem like an important enough case to change this principle (indeed, this case seems pretty unlikely). Moreover, we can’t break “incompleteness”, so we still have that case needing to be fairly general and restrictive. So this change would only make a minor difference in usability, at the cost of abandoning a bedrock Ada principle.

(6) The blame the user alternative:

Delete the third and fourth bullets and instead add a rule that an indexing is illegal if (1) the full type is visible, an access type, and is an indexable container type, and the designated type is also an indexable container type or an array type. The idea is that any use that could have conflicting indexing is illegal, rather than the declaration of potentially problematic cases.

The problem with this idea is that it detects the error very late. That’s probably not a serious problem for the unlikely private type case, but the much more likely incomplete type case would possibly not be detected for a long time until an unfortunate user stumbled over the problem. That makes this into a landmine for the unsuspecting maintainer. Step in the wrong place, and boom! the unit suddenly is illegal.


 

Change needed because of AI22-0149-1?

We need to look at the original wording (provided by AI22-0141-1) of 4.1.6(5.2/7) to see if the adoption of AI22-0149-1 requires or suggests any change. In particular, we have “These (indexing) aspects shall not be specified for an access type.” AI22-0149-1 allows prefixed views to be specified for access types with a partial view. Does this wording require any change?

It should be obvious for the normal case of an access type declaration that no change is needed, since such a declaration has no partial view and thus can have no change in legality of the nonexistent partial view. However, we need to consider the case of a hidden indexing associated with the full type of a partial view. For example:

package P9 is
  type
Priv9 is private;
   function Foo (Obj : in Priv9; N : in Natural) return Natural;
private
  type
Priv9 is access all Natural with
 
   Constant_Indexing => Foo; -- (Y)
   C9 : Priv9 := new Natural’(2);
   Obj9 : Natural := C9(1); -- (Z)
end P9;

The declaration at (Y) is clearly illegal by the original, AI22-0141-1 version of this rule. That made sense with the original AI22-0091-1 version of prefixed views, since the prefixed view associated with (Z) [C9.Foo(1)] was thought to be illegal. However, with the revised rules for AI22-0149-1, this prefixed view is now legal. So, should the aspect at (Y) also be legal?

To answer this question, we need to remember that indexing aspects are nonoverridable. 13.1.1(18.6/6) says that a nonoverridable aspect that applies to a type with a partial view (and that can be applied to a partial view, as indexing aspects can) has to be given on the partial view. Essentially, hidden indexing aspects are not allowed (as that would make it impossible to enforce the nonoverridable rules on derivation). Therefore, the aspect specification at (Y) is illegal regardless of any restrictions on the type it applies to. As such, there is no need to modify 4.16(5.2/7) because of AI22-0149-1.

!example

See the !discussion for many examples.

!comment The following insertion is needed to appropriately cause a conflict. (The paragraph in question is new in the revision.)

!corrigendum 4.1.6(5.1/4)

@dinsa

The Constant_Indexing and Variable_Indexing aspects are nonoverridable (see @ref{13.1.1}).

@dinsa

The Constant_Indexing and Variable_Indexing aspects are unspecifiable for array types (see @ref{13.1.1}). These aspects shall not be specified for an access type.

The Constant_Indexing and Variable_Indexing aspects are unspecifiable for an access type whose designated type is any of the following:

@xbullet{An array type;}

@xbullet{An indexable container type;}

@xbullet{A private type declared within the declarative region immediately enclosing the access type declaration;}

@xbullet{An incomplete view.}

!ACATS test

An ACATS B-Test needs to check that the various Legality Rules added here are enforced.

!appendix