AI22-0029-1

!standard 13.3(75.1/3)                                   22-06-14  AI22-0029-1/03

!standard 13.3(76)

!class binding interpretation 22-01-24

!status work item 22-01-24

!status received 21-12-31

!priority Low

!difficulty Medium

!qualifier Omission

!subject External_Tag collisions

!summary

Program_Error is raised at an appropriate point if two independently-declared tagged types have the same external tag.

External_Tags are only required to be unique if they come from different declarations.

!issue

13.3(75.1/3) says:

   If a user-specified external tag S'External_Tag is the same as

   T'External_Tag for some other tagged type declared by a different

   declaration in the partition, Program_Error is raised by the elaboration

   of the attribute_definition_clause.

But where is the exception raised if the user specified the external tag with an aspect specification?? The usual equivalence doesn't really help us here, as there is no attribute_definition_clause in that case. (The equivalence rule is simply "All specifiable operational and representation attributes may be specified with an aspect_specification instead of an attribute_definition_clause (see 13.3).", which doesn't give us any clue about the details of the 'virtual' attribute_definition_clause. One can assume that the same Legality Rules and most Static Semantics apply to it, but timing details seem like a bridge too far, especially as resolution and freezing rules are definitely different.)

Ergo, this rule needs to say where the exception is raised for an aspect  specification.

!recommendation

(See Summary.)

!wording

Replace 13.3(75.1/3):

If a user-specified external tag S'External_Tag is the same as T'External_Tag for some other tagged type declared by a different declaration in the partition, Program_Error is raised by the elaboration of the attribute_definition_clause.

with

If both S and T exist in a given partition and S’External_Tag is the same as T’External_Tag, Program_Error is raised at one or more of the following places:

AARM Reason: The intent is to allow two different implementations here: a fully static implementation with bind-time construction of an external tag table, and a runtime scheme where the elaboration of a type registers the external tag with a manager in the runtime.

Replace AARM 13.3(75.e/3) with: [still as part of the Ramification]

This check is on a *pair* of types, and the exception can be raised at any of the prescribed points. Furthermore, there is a permission (below) that permits the exception to be raised upon elaboration of either type before the other one exists.

Modify 13.3(76):

In an implementation, the default external tag for each specific tagged type declared in a partition shall be distinct, so long as the type is declared {by a distinct declaration and is }outside an instance of a generic body. If the compilation unit in which a given tagged type is declared, and all compilation units on which it semantically depends, are the same in two different partitions, then the external tag for the type shall be the same in the two partitions. What it means for a compilation unit to be the same in two different partitions is implementation defined. At a minimum, if the compilation unit is not recompiled between building the two different partitions that include it, the compilation unit is considered the same in the two partitions.

Add after AARM 13.3(76.e): [still as part of the Ramification]

If a single type declaration is elaborated multiple times, the default external tags need not be different. Note that if such identical tags are used for two different elaborations of the same declaration, they cannot raise Program_Error; the check that requires different External_Tags specifically excludes types that come from the same declaration.

Modify 13.3(76.1/3):

If a user-specified external tag S'External_Tag is the same as T'External_Tag for some other tagged type declared by a different declaration in the partition, [the partition may be rejected]{an implementation may disallow the partition, or raise Program_Error upon the elaboration of either type}.

!discussion

The only requirement for this check is that one of the types has a user-specified External_Tag. The check is defined symmetrically so that it doesn't matter which type is elaborated first.

Types are created when they are elaborated (3.2.1(11)), and cease to exist when their master is left (7.6.1(11/3)). The required check can only be made when both types exist, but there is a permission to raise Program_Error (or disallow the partition) if the collision is detected at compile time or bind time.

---

When studying this topic, it was noted that the first sentences of 13.3(76) are inconsistent with the model that two types created from the same declaration can have the same external tag. Indeed, it would take fairly heroic efforts to ensure that two such types would have different default external tags while still meeting the requirements of the second part of 13.3(76).

[One compiler was observed to "mangle" the default External_Tag for a nested type declaration by adding a stack address to it. But it would be rather unlikely for that stack address to be the same in some other partition, even when the type declaration and dependencies are unchanged. A more complex implementation of "mangling" would be needed to usefully meet all of 13.3(76) for a nested type declaration that could be elaborated again by a call from another task.]

As such, we have eliminated the requirement that the default external tags are different for multiple elaborations of the same declaration.

!ACATS test

ACATS tests CD30013 and CD30014 test this rule. One could imagine more complex

tests that try various corner cases of this rule (such as allowing multiple

elaborations of a single declaration in a set of recursive calls), but those

cases seem unlikely to come up where the result would be significant (one

would need to involve streaming to make the use of External_Tag meaningful,

and it's hard to imagine how to do that in recursive subprograms).

!appendix

From: Randy Brukardt [privately]

Sent: Friday, December 31, 2021  2:15 AM

Looking for a test to write that represents an incompatibility/inconsistency,

I landed on 13.3(75.1/3):

   If a user-specified external tag S'External_Tag is the same as

   T'External_Tag for some other tagged type declared by a different

   declaration in the partition, Program_Error is raised by the elaboration

   of the attribute_definition_clause.

But where is the exception raised if the user specified the external tag with

an aspect specification?? The usual equivalence doesn't really help us here,

as there is no attribute_definition_clause in that case. (The equivalence rule

is simply "All specifiable operational and representation attributes may be

specified with an aspect_specification instead of an

attribute_definition_clause (see 13.3).", which doesn't give us any clue about

the details of the 'virtual' attribute_definition_clause. One can assume that

the same Legality Rules and most Static Semantics apply to it, but timing

details seem like a bridge too far, especially as resolution and freezing

rules are definitely different.)

Ergo, this rule needs to say where the exception is raised for an aspect

specification. Perhaps something like:

   If a user-specified external tag S'External_Tag is the same as

   T'External_Tag for some other tagged type declared by a different

   declaration in the partition, Program_Error is raised {at the freezing

   point of type S if specified with an aspect specification, or}

   by the elaboration of the attribute_definition_clause {otherwise}.

Another (simple) AI?? Any better wording suggestions?

****************************************************************

From: Tucker Taft [privately]

Sent: Sunday, January 2, 2022  11:18 AM

Good catch!

Could we say simply no later than the freezing point of S?  I know GNAT does

tend to transform aspect clauses into pragmas or rep clauses when feasible,

and I worry this is overspecifying the location of detection of a fatal error.

There is no way to handle the Program_Error in a different place based on the

exact spot where the exception is raised, and it seems irrelevant exactly how

many other declarations are elaborated, so long as they are properly finalized

as part of raising the Program_Error.  On the other hand, specifying the

wording is perhaps more difficult with this level of flexibility.  Perhaps:

  If a user-specified external tag S'External_Tag is the same as T'External_Tag

  for some other tagged type declared by a different declaration in the

  partition, Program_Error is raised {either at the freezing point of type S,

  or, if present} by the elaboration of [the]{an} attribute_definition_clause{

  for S'External_Tag}.

This would allow it to always be performed at the freezing point, or

optionally at the attribute_definition_clause.

****************************************************************

From: Stephen Baird [privately]

Sent: Monday, January 10, 2022  12:34 PM

Tuck suggested wording:

  If a user-specified external tag S'External_Tag is the same as

  T'External_Tag for some other tagged type declared by a different

  declaration in the partition, Program_Error is raised {either at the

  freezing point of type S, or, if present} by the elaboration of

  [the]{an} attribute_definition_clause{ for S'External_Tag}.

I see several problems with this proposed wording.

If we are talking about two types that have the same External_Tag value, then

(obviously) we are talking about two types, not one.

So does this rule refer to the first of the two types or the second? Let's

suppose, for example, that the two types that have the same external tag are

declared in two different bodiless packages, P1 and P2. Suppose further that

neither package is nested within the other, and that P1 happens to be

elaborated before P2. Is the exception raised during the elaboration of P1 or

during that of P2? Presumably P2, but the proposed wording seems unclear on

this point.

[Actions associated with a type declaration typically occur somewhere between

the introduction of the type (perhaps via a private type declaration or an

incomplete type declaration) and the freezing point of the type. What does it

mean to talk about the "first" and "second" of the two types involved in an

external tag collision if these regions overlap for the two types? Whatever

wording we choose needs to handle this case too.]

Also, we want the wording to handle the case of external tag collision where

only one of the two types involved has a user-specified external tag. If the

one with the user-specified external tag is elaborated first, then that means

(I think) that we want to allow the exception to be raised at the freezing

point of the other type (which does not have a user-specified external tag).

I think we always want to allow the exception to be deferred until the

freezing point of the type, even if the user provides an

attribute_definition_clause. In a tradeoff between portability and making

things easier for implementations, the argument for portability is weaker

when we are talking about an error case (such as an external tag

collision).

****************************************************************

From: Tucker Taft [privately]

Sent: Sunday, January 10, 2022  12:54 PM

...

>So does this rule refer to the first of the two types or the second?

Note that most of this is existing wording, so I wonder whether we really

want to open this hornet's nest right now.

In any case, it talks about S and S'External_Tag, so it is talking about "S"

rather than "T", but admittedly it says nothing about which comes "first" or

"second."

>Let's suppose, for example, that the two types that have the same external

>tag are declared in two different bodiless packages, P1 and P2. Suppose

>further that neither package is nested within the other, and that P1

>happens to be elaborated before P2. Is the exception raised during the

>elaboration of P1 or during that of P2? Presumably P2, but the proposed

>wording seems unclear on this point.

And it is mostly existing wording ...

>[Actions associated with a type declaration typically occur somewhere between

>the introduction of the type (perhaps via a private type declaration or an

>incomplete type declaration) and the freezing point of the type. What does it

>mean to talk about the "first" and "second" of the two types involved in an

>external tag collision if these regions overlap for the two types? Whatever

>wording we choose needs to handle this case too.]

If you insist... ;-)

Also, we want the wording to handle the case of external tag collision where

only one of the two types involved has a user-specified external tag. If the

one with the user-specified external tag is elaborated first, then that means

(I think) that we want to allow the exception to be raised at the freezing

point of the other type (which does not have a user-specified external tag).

I suppose.  In the old wording, the exception was raised at the point of the

attribute definition, so there is no corresponding point for the one with the

language-defined external tag.

>I think we always want to allow the exception to be deferred until the

>freezing point of the type, even if the user provides an

>attribute_definition_clause. In a tradeoff between portability and making

>things easier for implementations, the argument for portability is weaker

>when we are talking about an error case (such as an external tag

>collision).

My intent was to allow it to be raised at either point, if there was an

attribute definition clause.  I could try to make that clearer.  Perhaps:

   ...  {either, optionally, at the point of} [by] the elaboration of the

   attribute_definition_clause{ for S'External_Tag, if any, or in any case,

   at the freezing point of S}.

 

Here is a possible rewording that I believe would satisfy your concerns:

   If a user-specified external tag S'External_Tag is the same as

   T'External_Tag for some other tagged type declared by a different

   declaration in the partition, Program_Error is raised by the elaboration

   of [the]{an} attribute_definition_clause{ for S'External_Tag or

   T'External_Tag, if any, or if not then, at the freezing point of S or T}.

****************************************************************

From: Randy Brukardt [privately]

Sent: Monday, January 10, 2022  4:53 PM

Remember that the "original" mistake is that there was an assumption that the

aspect was specified by an attribute_definition_clause, so it's not surprising

that the AARM assumes that there is one. The original AARM is clear that it

doesn't try to specify which declaration if there is more than one, so that is

an old problem that we intended to not worry about.

 

Similarly, the lack of wording about a collision with a language-defined name

is intentional, I think. The choice of a language-defined name is up to the

compiler, and I think the intent was that it avoid collisions. The problem

with that, of course, is that doing so isn't possible, if, in addition, one

has to have  a name that can be depended upon in another partition. That is,

it is impossible in Ada as defined to meet both the first and second sentences

of 13.3(76), If the compiler changes the default name to avoid a collision,

then it probably won't meet the second sentence of 13.3(76) (if the collision

is not present in closure of the tagged type, it might not be present in the

second partition and a different name would result). Moreover, this

interpretation seems rather expensive to implement. So that says that

13.3(75.1/3) is just plain wrong, it needs to go further.

 

I note that the "same declaration" exclusion for recursive declarations also

brings up this problem (being unable to meet both sentence 1 and 2 of

13.3(76)). Any way that the default name could be chosen that would not have

distributed overhead would necessarily get different names in a different

partition. Indeed, one of the compilers I tested manages the name to meeting

the first sentence of 13.3(76), but doing so prevents meeting the second

sentence of 13.3(76) [the mangling includes a machine address which is highly

likely to be different in a different partition]. My guess is that the second

sentence of 13.3(76) was not intended to apply to non-library-level tagged

type declarations, but it doesn't say that. Since we have an unmeetable

requirement, that is a problem that we need to look at as well.

 

It sounds like we need a complete re-visit of this area, since it seems the

problems run much deeper than just the missing aspect specification wording.

 

****************************************************************

[Editor's note: This discussion is heading for the weeds, following is just

the most important parts of following mail.]

****************************************************************

From: Randy Brukardt [privately]

Sent: Monday, January 10, 2022  5:22 PM

Why we need to fix:

 

(1) The wording as written does not allow a conflict that happens after the

freezing/ADC of a tagged declaration, if a later elaborated tagged type has a

name chosen by default that causes a conflict. I don't think we meant to

require that the name chosen by default change to avoid conflicts, but that is

what is currently required by the first sentence of 13.3(76) [since there no

permission to reject in that case].

 

(2) The second sentence of 13.3(76) seems impossible to meet for a non-library

level tagged type without some sort of distributed overhead (like a nesting

counter). It seems unlikely that there would be any use to such a type across

partitions anyway (how does one ensure that you are in the same subprogram in

the same way with the same recursion in two partitions?), so I think it was

intended to exclude that from the requirements. Certainly, *something* needs

to be excluded from those requirements.

 

I don't feel as strongly as Steve that we need to fix the race condition, but

he definitely is right that the race condition doesn't happen unless both

types have specified 'External_Tags. Which doesn't allow the second type to

raise the exception in some circumstances, and that is wrong. I suspect that

fixing that (by adding the freezing point option and eliminating the

"user-defined" part from the wording) simplifies the permission enough to

ensure that any remaining race condition is benign.

****************************************************************

From: Tucker Taft [privately]

Sent: Monday, January 10, 2022  8:26 PM

> Why we need to fix:

 

> (1) The wording as written does not allow a conflict that happens after the

>  freezing/ADC of a tagged declaration, if a later elaborated tagged type has

> a name chosen by default that causes a conflict. I don't think we meant to

> require that the name chosen by default change to avoid conflicts, but that

> is what is currently required by the first sentence of 13.3(76) [since there

> no permission to reject in that case].

I don't think it says that, since it imposes no ordering between S and T.  It

does imply that if neither has a user-specified External_Tag, there can be no

conflict.   Here again is the "order agnostic" version of 13.3(75.1/3)

   If a user-specified external tag S'External_Tag is the same as

   T'External_Tag for some other tagged type declared by a different

   declaration in the partition, Program_Error is raised by the elaboration

   of [the]{an} attribute_definition_clause{ for S'External_Tag or

   T'External_Tag, if any, or if not then, at the freezing point of S or T}.

With the intent that it is up to the implementation to choose which of the

three or four possible points to raise the exception (if that intent is not

clear, we could further emphasize it).

> (2) The second sentence of 13.3(76) seems impossible to meet for a

> non-library level tagged type without some sort of distributed overhead

> (like a nesting counter).

The second sentence of 13.3(76), for reference says:

   If the compilation unit in which a given tagged type is declared, and all

   compilation units on which it semantically depends, are the same in two

   different partitions, then the external tag for the type shall be the same

   in the two partitions.

Did you mean the first sentence?  It says:

   In an implementation, the default external tag for each specific tagged

   type declared in a partition shall be distinct, so long as the type is

   declared outside an instance of a generic body.

I would agree that we should clarify that we mean for a given tagged type

declaration, not for a given elaboration of the declaration.  The check in

13.3(75.1/3) allows for matching tags if they arise from the same declaration,

but this sentence seems to omit that permission.

> It seems unlikely that there would be any use to such a type across

> partitions anyway (how does one ensure that you are in the same subprogram

> in the same way with the same recursion in two partitions?), so I think it

> was intended to exclude that from the requirements. Certainly, *something*

> needs to be excluded from those requirements.

I would agree that we should clarify the issue with non-library-level types,

by saying that they only need to be distinct if they arise from different

declarations.  It is funny that we had a special case for instances, which

seem much simpler to handle than types declared inside a subprogram, where

each invocation of the subprogram conceptually produces a new type.  In any

case, my guess is that in most implementations, all elaborations of the same

tagged type declaration will have the same External_Tag, which was the

original intent (I believe) when we created Ada 95.

Exceptions inside instances are already required to be unique, so making

tags unique in instances doesn't seem much harder, but I guess there is

no need to tighten up the rules at this point.

> I don't feel as strongly as Steve that we need to fix the race condition,

> but he definitely is right that the race condition doesn't happen unless

> both types have specified 'External_Tags. Which doesn't allow the second

> type to raise the exception in some circumstances, and that is wrong. I

> suspect that fixing that (by adding the freezing point option and

> eliminating the "user-defined" part from the wording) simplifies the

> permission enough to ensure that any remaining race condition is benign.

The intent of my last version of the proposed wording was to allow either type

to raise the exception.  That would seem to resolve the race condition.  If

that intent is not clear in the proposed wording, we could emphasize it, as

mentioned above.

****************************************************************

From: Stephen Baird [privately]

Sent: Tuesday, January 11, 2022  5:44 PM

I think we agree that in my example

   package Pkg is

      type T1 is tagged null record;

      for T1'External_Tag use "ABC";

      ...

      type T2 is tagged null record;

      for T2'External_Tag use "ABC";

      ...

   end;

we want to *allow* the first check to

pass and the second check to fail. That's what you'll

get with the global map implementation I described earlier, which I think

we agreed ought to be a valid implementation.

The question is whether the proposed wording captures that intent.

In that wording, the condition of the check is

    "If a user-specified external tag S'External_Tag is the same as

     T'External_Tag for some other tagged type ..."

So in our example, the two checks test the same condition (because "is the

same as" is a symmetric relation).

Given that, I don't see how this wording allows the first check to pass and

the second check to fail. If two checks test the same condition and there is

no dependency on any variable state, then either both checks pass or both

checks fail.

****************************************************************

From: Tucker Taft [privately]

Sent: Tuesday, January 11, 2022  6:13 PM

...

> That's what you'll

> get with the global map implementation I described earlier, which I think

> we agreed ought to be a valid implementation.

OK, I guess I finally understand your concern, but the intent of the wording

is that the Program_Error need not be raised on encountering the first type,

even if it fails the "check".  The intent is that it must be raised at the

elaboration of *one* of the two types involved.  Clearly the pair of tests

fail the check.  The only question is when the problem is detected.  So I

think you are muddying the waters by saying the first type "passes" the

check.  Clearly there is a problem, and the only question is *when* it is

detected and where the exception is raised.

>The question is whether the proposed wording captures that intent.

...

>So in our example, the two checks test the same condition (because "is the

>same as" is a symmetric relation).

True, and that is what makes sense to me.  I think you are overly focused

on exactly when the problem is detected.  There is a problem, and we want to

be sure the program doesn't proceed past the point where both types are fully

elaborated.

>Given that, I don't see how this wording allows the first check to pass and

>the second check to fail. If two checks test the same condition and there is

>no dependency on any variable state, then either both checks pass or both

>checks fail.

I think you are defining the "check" too narrowly, and tying it too closely to

when it gets reported.

>Perhaps if we add something like

>    Program_Error need not be raised if this check has not yet been performed

>    for that other tagged type.

So long as we allow the problem to be reported during elaboration of either

type, why do we need this extra wording?

>If we wanted to be really pedantic, we could talk about whether the first

>check "signals" the second check (as defined in 9.10(2-10)).

Yuck.  This all seems like overspecification.

****************************************************************

From: Stephen Baird [privately]

Sent: Tuesday, January 11, 2022  6:28 PM

OK, it took me a while but I think I get it now. You are saying that for any

given *pair* of tagged types, there is a single External_Tag collision check

associated with that pair and this check may be performed at any one of

between 2 and 4 locations: the attribute_definition clause for the one type

or the other (if such clauses exist) or the freezing points of one type or the

other.

This is sufficiently different from other Ada checking that I found it

confusing, but I now see what you have in mind. I'd say the wording you

propose is fine.

Thanks for all the clarification.

****************************************************************

From: Randy Brukardt [privately]

Sent: Wednesday, January 12, 2022  12:20 AM

Perhaps we need an AARM note to clarify this? The way you put it here seems

like a nice explanation, and I'd hate to have this discussion again in a few

years.

****************************************************************

From: Stephen Baird [privately]

Sent: Wednesday, January 12, 2022  11:07 AM

An AARM note sounds good to me.

****************************************************************