!standard 13.3(75.1/3) 22-01-24 AI22-0029-1/01 !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. !question 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 Modify 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}. 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. However, the check cannot be performed before both types exist, so that constrains when the exception can be raised. Iin the case where one of the types is declared in a subprogram while the other is declared at library level, the exception cannot be raised until the subprogram is called and its declarations elaborated. 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 unique 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, they cannot raise Program_Error; the check that requires different External_Tags specifically excludes types that come from the same declaration. !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 is left (7.6.1(11/3)). This check can only be made when both types exist. Thus, for types not declared at library-level, the check will have to be made during their elaboration. This is important, as a type that never exists should not cause a program failure. --- When studying this topic, it was noted that the first sentences of 13.3(76) is 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. ****************************************************************