AI22-0083-1

!standard 4.6(23/2)                                       24-02-23  AI22-0083-1/05

!standard 4.6(23.1/2)

!standard 8.5.1(3/5)

!standard 8.5.1(4/5)

!class Amendment 23-09-27

!status Corrigendum 1-2022  24-02-22

!status ARG Approved  12-0-3  24-02-22

!status work item 23-09-27

!status received 22-06-10

!priority Medium

!difficulty Medium

!subject Treat dynamically-tagged expressions as class-wide in various contexts

!summary

We allow dynamically-tagged expressions in conversions and object renaming where before class-wide expressions were required.

!issue

Currently, it is not legal to convert an expression of a specific tagged type to another specific tagged type, unless the target type is an ancestor. Also, you cannot convert a specific type to a class-wide type, unless the target type covers the operand type. One must first convert to a "covering" class-wide type, and then convert to the desired target type. This reflects the general rule that a value of a specific type doesn't conceptually "reveal" its run-time tag, unless you convert it to a class-wide type first.

However, if you call a primitive function and pass in a class-wide controlling operand, where the primitive function has a controlling result, you end up with a "dynamically-tagged" expression which for all intents and purposes should be treated as class-wide, since the tag of the controlling operand effectively flows through as the tag of the controlling result, thanks to the dispatching.

For example:

type Set is interface;
function Union (Left, Right : Set) return Set is abstract;

...

type Bit_Set is new Set with private;

...
function Combine (A, B : Set'Class) return Bit_Set'Class is
   ... Union (A, B) ...
      -- this expression is nominally of a specific type but should
      -- probably be treated like an expression of a class-wide type

 

In a tagged abstraction like a set or bignum, there are a lot of function operations that are used in combination (think operators for a bignum), and we want those combinations to work the same as a single object of the type. For example, we do not want different rules to apply to ONE and -ONE (where ONE is a parameterless bignum function giving the obvious value).This implies that we should examine all rules that are restricted to class-wide operands, and consider shifting the wording to be "dynamically-tagged" instead. In particular, the rules in RM 4.6(23/2) and 4.6(23.1/2) should be candidates for this shift to "dynamically-tagged". Another possibility would be as a prefix to 'Tag though it would be a bit odd to call a primitive function only to examine the tag of the result.

This brings up an interesting question -- what do you get when you rename the result of a dynamically-tagged call on a primitive function? If you specify the type in the renaming (which as of Ada 2022 is no longer required), should you specify specific or class-wide? And does it retain its dynamic-tagged-ness? RM 3.9.2(9/1) effectively disallows renaming as a specific type, since the renaming would imply a specific expected type:

If the expected type for an expression or name is some specific tagged type, then the expression or name shall not be dynamically tagged unless it is a controlling operand in a call on a dispatching operation. Similarly, if the expected type for an expression is an anonymous access-to-specific tagged type, then the object designated by the expression shall not be dynamically tagged unless it is a controlling operand in a call on a dispatching operation.

Renaming as an object of a class-wide type would also be illegal, according to RM 8.5.1(3/5):

The type of the object_name shall resolve to the type determined by the subtype_mark, if present. If no subtype_mark or access_definition is present, the expected type of the object_name is any type.

Here we could generalize this issue to consider allowing dynamically-tagged expressions in all places where class-wide expressions are allowed, and allow the object_name in a class-wide renaming to resolve to the associated specific tagged type, but only if it is dynamically-tagged. If no subtype_mark is specified, then if we allow it, we would clearly want it to remain dynamically-tagged -- whether or not its type is considered specific or class-wide then becomes less important.

Should we look for places where we should replace class-wide with dynamically-tagged? (Yes.)

!recommendation

The legality rules in RM 4.6(23/2) and 4.6(23.1/2) dealing with tagged-type conversions should be candidates for this shift to dynamically-tagged:

The operand type shall be a class-wide type that covers the target type; or

The operand and target types shall both be class-wide types and the specific type associated with at least one of them shall be an interface type.

We also propose to augment the rules in RM 8.5.1 Object Renaming so as to allow renaming the result of a dynamically-tagged function call as a class-wide object.

!wording

Modify 4.6(21/3) as follows:

If there is a type (other than a root numeric type) that is an ancestor of both the target type and the operand type, or {the target type is class-wide and the operand is dynamically tagged (see 3.9.2)}[both types are class-wide types], then at least one of the following rules shall apply:

Modify 4.6(23/2) and 4.6(23.1/2) as follows:

Modify the first sentence of 8.5.1(3/5):

The type of the object_name shall resolve to the type determined by the subtype_mark, if present{, or if the determined type is class-wide, either to the class-wide type or to the root type of its class}.

Add after 8.5.1(4/5):

In the case where the type determined by the subtype_mark is class-wide, the object_name shall be dynamically tagged (see 3.9.2).

!discussion

Dynamically-tagged expressions are the result of dispatching calls, and the run-time tag of the result is not known at compile time (even though the expression is nominally of a specific type) which makes them as dynamic as expressions of a class-wide type, so it seems reasonable to treat them as class-wide for the purposes of certain legality rules.

It is possible for a previously legal call to become ambiguous due to this change. For example, if a package P declared a Union that returned a class-wide type, and in a package Q declared a Union as defined in these examples with a controlling result, a context that calls Union and has use clauses for both P and Q would now be ambiguous whereas it would have worked previously.  This incompatibility seems unlikely, and is easily worked around.

!example

Here are some examples that would become legal with these changes:

type Set is interface;
function Union (Left, Right : Set) return Set is abstract;
...
type Bit_Set is new Set with private;
function Union (Left, Right : Bit_Set) return Bit_Set;
...
function Combine (A, B : Set'Class) return Bit_Set'Class is
     (if Is_Empty (B) then Bit_Set'Class (A)  --  Already legal
      else Bit_Set'Class(Union(A, B)));  

        -- Downward conversion now legal due to 4.6 changes
...
L : Bit_Set'Class := ...
M : Bit_Set'Class := ...
U : Bit_Set'Class renames Union(L, M);  -- Now legal due to 8.5.1 changes

!ACATS test

ACATS tests with code similar to that of the above examples should be constructed.

!appendix

See issue #8 on ARG GitHub issue list.