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
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.
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.
(See Summary.)
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}.
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 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).
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.
****************************************************************