!standard 6.4.1(13/3) 20-08-28 AI12-0378-1/06 !standard 6.4.1(18/3) !class Binding Interpretation 20-06-18 !status Amendment 1-2012 20-07-07 !status work item 20-04-29 !status received 20-03-26 !priority Low !difficulty Medium !subject View conversions and out parameters of access types revisited !summary In the case of an actual out parameter that is a view conversion between two access types with no common ancestor, an implementation permission is granted allowing implementations to pass in the null value of the formal parameter type instead of the value of the actual parameter. !question The legality rule that prevents view conversions of unrelated access types for out parameters is a compatibility problem in practice. When we implemented this in GNAT, a significant number of tests in our customer regression test suite failed to compile. Should we handle such view conversions in a more compatible way? (Yes.) !recommendation (See Summary.) !wording Replace 6.4.1(13/3) with: * For an access type, the formal parameter is initialized from the value of the actual, without checking whether the value satisfies any constraints, predicates, or null exclusions, but including any [Redundant: dynamic] accessibility checks associated with a conversion to the type of the formal parameter. AARM Ramification: The permission to pass *null* can be used in any case where an accessibility check could fail, rather than making a check. Add after RM 6.4.1(18/3): Implementation Permissions If the actual parameter in a parameter_association with mode *out* is a view conversion between two access types that do not share a common ancestor type, the implementation may pass in the null value of the type of the formal parameter instead of the value of the actual parameter. It is implementation-defined under what circumstances the implementation passes in the null value. Add after AARM 6.4.1(18.i/3): Inconsistencies with Ada 2012 Correction: Added a permission to pass null so that value passed into an out parameter for access types is well-defined in the case of a view conversion. Null may be passed for any view conversion between unrelated access types; this is important for conversions that may have problematic accessibility or tags. If the permission is used and the out parameter is read before it is written (perhaps to read a bound or discriminant), Constraint_Error may be raised by Ada 202x when it would not have been in Ada 2012. Additionally, if the called subprogram does not write the out parameter at all, the actual object will be overwritten with null (and possibly raise Constraint_Error if the object is null excluding), while the object would be unchanged in Ada 2012. Such cases are thought to be rare, as most out parameters of access types are overwritten before being read. In addition, at least one widely-used Ada compiler already passes null in these cases. Modify AARM 6.4.1(18.j/4): [to remove any mention of access types - there is no longer any compile-time incompatibility; this also updates the text for AI12-0377-1] Corrigendum: Added rules to ensure that the value passed into a{n} out parameter for {scalar}[elementary] types is well-defined in the case of a view conversion. The new rules can be incompatible. For a view conversion to an unrelated type with the Default_Value aspect specified, the aspect is new in Ada 2012 so it should be unlikely to occur in existing code.[ For a view conversion to an unrelated access type, the incompatibility is possible as this could be written in Ada 95, but such a view conversion is thought to be rare. In both cases, ]{D}eclaring and passing a temporary rather than a view conversion will eliminate the problem. !discussion It's fairly clear that we cannot have the original Legality Rule, as it seems too incompatible. However, this problem occurs not only for access types with different sizes (as outlined in the original AI12-0074-1), but also for any case where the generated code may need to know details about an out parameter. Three such cases were identified in the e-mail attached to AI12-0074-1: [A] Cases involving discriminant checks; [B] Cases involving tag checks; [C] Cases involving accessibility checks. These cases can be seen in the test program given at the start of the !appendix for this AI (more on this test program below). An implementation could assume that all out parameters are unconstrained (and thus making the discriminant checks on every use), thus defanging problem [A]. This would not be problematic, and is in fact the rule since Ada 83, so we don't make any change here. However, the tag and accessibility checks are not normally done at usage sites, so assuming implementations will do the right thing could impose a substantial implementation burden for a very unusual case (but one that is not quite as pathological as we expected in AI12-0074-1). We created a test program to see what existing compilers do in these cases. The test program is reproduced at the start of the !appendix. The test program includes a few preliminary cases to check what an implementation does in cases not involved with the cases in question, then it tries each of the three problematic examples in turn. Running the test program on a recent version of GNAT (from February 2020) shows that it side-steps this problem by passing null to any out parameter when the actual is an explicit view conversion. The implicit view conversion of derivation works as expected, however, so we have a case where one cannot write the same thing as the compiler does automatically. Running the test program on a recent version of ObjectAda (thanks to PTC for providing these results): The discriminant and accessibility cases do not compile (ObjectAda rejects the conversions suggesting an Ada 95 implementation of those). ObjectAda fully supports the derived type cases, including using explicit conversions. The tagged type case causes no errors nor obvious overwriting of memory (but it is not clear what successfully writing the nonexistent component actually did). Running the test program on a recent version of ApexAda (thanks again to PTC for providing these results): The accessibility cases do not compile (ApexAda rejects the conversion with a static accessibility check). ApexAda fully supports the derived type cases, including using explicit conversions. The discriminant case shows that ApexAda does not assume the constraints of an out parameter, so a check was made and Constraint_Error raised. The tagged type case causes no errors nor obvious overwriting of memory (but it is not clear what successfully writing the nonexistent component actually did). Running the test program on a recent version of Janus/Ada shows that it fully supports the derived type cases; it assumes that out parameters do not enforce their constraints (so that they are effectively unconstrained and raises Constraint_Error when incorrect); other memory is overwritten by the tagged case (execution is erroneous); and the accessibility case does not compile (Janus/Ada does not yet support dynamic accessibility for stand-alone objects of anonymous access types). --- These results suggest that having certain problematic cases use the GNAT approach of passing null would not be significantly incompatible in practice, as these cases (tag-check or accessibility-level check issues) can already lead to problems in other Ada compilers. Note that GNAT is the only compiler that passes null currently; other compilers implement the language as defined more closely. Note that an out parameter can always be null, even if the parameter is declared with a null exclusion. Null exclusions are not checked when the parameter is passed in, and null is passed when the actual is not convertible. An implementation should not assume that null exclusions of an out parameter are enforced before the out parameter is written within the subprogram. We could have required null to be passed for all out parameters of access types. That might have been a more sensible rule for a new language, but the runtime incompatibility creating by generally making such a change would be intolerable, so it is much too late to contemplate that. Note that there is a potential runtime incompatibility with passing null, in that in the absence of an assignment to the OUT parameter, the parameter will nevertheless be set to null, even if the tag and accessibility checks would have passed. We considered always performing the tag and accessibility checks, even on OUT parameters, but this was felt to introduce a "tripping hazard" where the supposedly irrelevant value of an OUT parameter prior to a call nevertheless could produce an exception at runtime under unusual circumstances. We considered passing in null only if a tag or accessibility check would have failed, but this made the runtime model harder to describe and to implement, for what are considered corner cases. We also considered a legality rule disallowing problematic cases, but we felt the potential incompatibilities would be worse. A compile-time rule that would pass null in only the problematic cases was very complicated. So we settled on an Implementation Permission to allow implementations to pass null in cases that might be problematic. If the implementation doesn't pass null, then it needs to make the accessibility and tag checks. We allow the "tripping hazard" for accessibility checks in that case, but the tag checks have to be made at each use if null is not passed in. The Implementation Permission includes the magic words "implementation-defined" in order to trigger a documentation requirement as to when null will be passed. --- As far as converting between access types with different representations, the implementation permission also allows passing null if it involves an out parameter with unrelated types. We don't provide any permission or suggestion related to normal conversions or IN OUT parameters in such a case. We presume implementations will do the (implementation-defined) "right thing" in such cases. Similarly, we expect the implementation will do the "right thing" for related types (most likely, not allowing different representations that could be problematic). !corrigendum 6.4.1(13/3) @drepl @xinbull @dby @xinbull !corrigendum 6.4.1(18/3) @dinsa If the nominal subtype of a formal parameter with discriminants is constrained or indefinite, and the parameter is passed by reference, then the execution of the call is erroneous if the value of any discriminant of the actual is changed while the formal parameter exists (that is, before leaving the corresponding callable construct). @dinst @s8<@i> If the actual parameter in a @fa with mode @b is a view conversion between two access types that do not share a common ancestor type, the implementation may pass in the null value of the type of the formal parameter instead of the value of the actual parameter. It is implementation-defined under what circumstances the implementation passes in the null value. !ASIS No ASIS effect. !ACATS test An ACATS C-Test is needed to check that the new rules are enforced, rather than the previous rules. The test program given below can be the basis of an ACATS C-Test. !appendix From: Randy Brukardt Sent: Never [Done April 20, 2020] [Following is the test program for AI12-0378-1.] with Ada.Text_IO, Ada.Exceptions; procedure TestAI74 is begin -- Note: The three "problems" were identified during the work on -- AI12-0074-1. We're testing what they do on existing implementations -- during a revisit of these rules. Note: All of these "problems" -- were made illegal by AI12-0074-1, so a correct Ada 2012 implementation -- would reject this program. If so, please try each case individually -- to provide us the maximum information about runtime behavior. -- Note that many of these cases are allowed by Ada 95 and Ada 2005. Ada.Text_IO.Put_Line ("Check some nasty cases related to AI12-0378-1"); -- Behavior of view conversions and derivation -- Note: This example is fully legal, no Ada 2012 compiler (or Ada 95 -- compiler, for that matter) should reject this. declare package Nest is type T (D : Boolean := False) is -- no partial view record case D is when False => Bar : Character; when True => Foo : Integer; end case; end record; type Unconstrained_Ref is access all T; procedure Q (X : out Unconstrained_Ref); -- A primitive of Unconstrained_Ref type Derived_Ref is new Unconstrained_Ref; -- Inherits Q. end Nest; package body Nest is procedure Q (X : out Unconstrained_Ref) is begin if X.D then X.Foo := 456; else X.Bar := 'B'; end if; end Q; end Nest; X : aliased Nest.T := (D => False, Bar => 'R'); X_Ref : Nest.Unconstrained_Ref := X'Access; Der_X_Ref : Nest.Derived_Ref := X'Access; begin -- Direct call to Q: begin Nest.Q (X_Ref); if X.Bar /= 'B' then Ada.Text_IO.Put_Line ("** Component not changed"); else Ada.Text_IO.Put_Line ("-- Expected result, no conversion"); end if; exception when Err:others => Ada.Text_IO.Put_Line ("** Failed: Exception raised (no conversion) - " & Ada.Exceptions.Exception_Information (Err)); end; -- View conversion to parent type: begin X.Bar := 'R'; Nest.Q (Nest.Unconstrained_Ref (Der_X_Ref)); if X.Bar /= 'B' then Ada.Text_IO.Put_Line ("** Component not changed"); else Ada.Text_IO.Put_Line ("-- Expected result, explicit parent conversion"); end if; exception when Err:others => Ada.Text_IO.Put_Line ("** Failed: Exception raised (explicit parent conversion) - " & Ada.Exceptions.Exception_Information (Err)); end; -- Call to inherited routine (implicit view conversion to parent type): begin X.Bar := 'R'; Nest.Q (Der_X_Ref); if X.Bar /= 'B' then Ada.Text_IO.Put_Line ("** Component not changed"); else Ada.Text_IO.Put_Line ("-- Expected result, inherited routine"); end if; exception when Err:others => Ada.Text_IO.Put_Line ("** Failed: Exception raised (inherited routine) - " & Ada.Exceptions.Exception_Information (Err)); end; end; -- A discriminant problem declare type T (D : Boolean := False) is -- no partial view record case D is when False => Bar : Character; when True => Foo : Integer; end case; end record; type Unconstrained_Ref is access all T; type Constrained_Ref is access all T (True); X : aliased T := (D => False, Bar => 'R'); X_Ref : Unconstrained_Ref := X'Access; procedure P (X : out Constrained_Ref) is begin X.Foo := 123; -- We don't want to require a discriminant -- check here. end P; begin P (Constrained_Ref (X_Ref)); if X.D /= False then Ada.Text_IO.Put_Line ("** Discriminant changed"); else Ada.Text_IO.Put_Line ("?? Possible erroneous execution"); Ada.Text_IO.Put_Line ("?? X.Bar'Pos = " & Natural'Image (Character'Pos (X.Bar))); end if; exception when Err2:Constraint_Error => Ada.Text_IO.Put_Line ("-- Discriminant check made anyway - " & Ada.Exceptions.Exception_Information (Err2)); when Err:others => Ada.Text_IO.Put_Line ("** Exception raised (disc) - " & Ada.Exceptions.Exception_Information (Err)); end; -- A tag problem declare type Root is tagged null record; type Ext is new Root with record F : Integer; end record; type Root_Ref is access all Root'Class; type Ext_Ref is access all Ext; procedure P (X : out Ext_Ref) is begin X.F := 123; -- No tag check is performed here and -- we don't want to add one. X := null; end P; R : aliased Root; O : Integer := 12; Ptr : Root_Ref := R'Access; begin P (Ext_Ref (Ptr)); Ada.Text_IO.Put_Line ("?? Possible erroneous execution"); if R not in Root then Ada.Text_IO.Put_Line ("** Object tag changed"); end if; if O /= 12 then Ada.Text_IO.Put_Line ("** Following object clobbered to " & Integer'Image(O)); end if; exception when Err:others => Ada.Text_IO.Put_Line ("-- Exception raised (tag) - " & Ada.Exceptions.Exception_Information (Err)); end; -- An accessibility problem declare type Int_Ref is access all Integer; Dangler : Int_Ref; procedure P (X : out Int_Ref) is begin Dangler := X; -- No accessibility checks are performed here. -- We rely here on the invariant that -- a value of type Int_Ref cannot designate -- an object with a shorter lifetime than Int_Ref. X := null; end P; procedure Q is Local_Int : aliased Integer; Standalone : access Integer := Local_Int'Access; begin P (Int_Ref (Standalone)); null; end Q; begin Q; Dangler.all := 123; -- Assigns a non-existent object, can't check this. Ada.Text_IO.Put_Line ("?? Likely erroneous execution"); exception when Err:others => Ada.Text_IO.Put_Line ("-- Exception raised (acc) - " & Ada.Exceptions.Exception_Information (Err)); end; Ada.Text_IO.Put_Line ("Test finished"); end TestAI74; **************************************************************** From: Randy Brukardt Sent: Never [Done April 20, 2020] Here's the results of the above test program when run on GNAT 21.0w-200219: Check some nasty cases related to AI12-0378-1 -- Expected result, no conversion ** Failed: Exception raised (explicit parent conversion) - raised CONSTRAINT_ERROR : testai74.adb:42 access check failed -- Expected result, inherited routine -- Discriminant check made anyway - raised CONSTRAINT_ERROR : testai74.adb:117 access check failed -- Exception raised (tag) - raised CONSTRAINT_ERROR : testai74.adb:148 access check failed -- Exception raised (acc) - raised CONSTRAINT_ERROR : testai74.adb:194 access check failed Test finished **************************************************************** From: Randy Brukardt Sent: Never [Done April 20, 2020] Here's the results of the above test program when run on Janus/Ada 3.2.2dev (4/20/20): [Note: Janus/Ada needed the call in the third case commented out, since it does not support dynamic accessibility for stand-alone objects of an anonymous access type. Thus the last case is uninteresting as it didn't set Dangler.] Check some nasty cases related to AI12-0378-1 -- Expected result, no conversion -- Expected result, explicit parent conversion -- Expected result, inherited routine -- Discriminant check made anyway - CONSTRAINT_ERROR Variant record field not available On Line Number 117 In TESTAI74.LOOP.P Called from line number 123 In TESTAI74 ?? Possible erroneous execution ** Following object clobbered to 123 -- Exception raised (acc) - CONSTRAINT_ERROR Attempt to reference thru NULL/Uninitialized pointer On Line Number 194 In TESTAI74 Test finished **************************************************************** From: Randy Brukardt Sent: Never [Done April 21, 2020] Here's the results of the above test program when run on ObjectAda 10.1 (4/21/20) [thanks to PTC for these results] [Note: ObjectAda needed the first and third error cases commented out, as it rejected both of the conversions in those two cases.] Check some nasty cases related to AI12-0378-1 -- Expected result, no conversion -- Expected result, explicit parent conversion -- Expected result, inherited routine ?? Possible erroneous execution Test finished **************************************************************** From: Randy Brukardt Sent: Never [Done April 21, 2020] Here's the results of the above test program when run on ApexAda 5.2 (4/21/20) [thanks to PTC for these results] [Note: ApexAda needed the third error cases commented out, as it rejected the conversion with a static accessibility failure.] Check some nasty cases related to AI12-0378-1 -- Expected result, no conversion -- Expected result, explicit parent conversion -- Expected result, inherited routine -- Discriminant check made anyway - CONSTRAINT_ERROR raised at 16#000000000040439B#, Exception Message: ?? Possible erroneous execution Test finished **************************************************************** From: Tucker Taft Sent: Thursday, March 26, 2020 9:29 AM AdaCore recently implemented this Corrigendum AI, and bumped into a significant number of incompatibilities in their customer regression test suite in the access-type-related part, which disallows view conversions between unrelated access types on an actual parameter if the formal is mode "out." The AI argued that such situations should be rare, but apparently it is more common than we anticipated. I would suggest we remove the access-type-related part from this AI, put it in a separate AI, and consider voting it "no action," or come up with a more compatible approach (e.g. disallow only if the Sizes for the access types differ). **************************************************************** From: Randy Brukardt Sent: Tuesday, April 21, 2020 6:51 PM It turns out that GNAT "solves" this problem by always passing null for an explicit view conversion of an out parameter. It completely ignores what the language says to do in such a case. Probably some solution based on that would be best, although we need to take care not to break existing code that takes advantage of the language as written (since that goes back to Ada 95). Unless, of course, none exists; I've asked implementers about this question in order to see if we get any feedback. AI12-0377-1 will cover this issue and the other one that Gary posted (they're both related to AI12-0074-1, which is a Corrigendum AI, and which will need to be revisited in any case because of the intent to repeal 13.1(10). I'll post this AI later today. **************************************************************** From: Randy Brukardt Sent: Wednesday, April 22, 2020 1:44 PM FYI, I asked PTC (and received almost immediately) about their compilers behavior on the test program. Both of their compilers behaved similarly to the results I reported for Janus/Ada (including not supporting dynamic accessibility on SAOAATs), with the exception of ObjectAda rejecting the conversion in the first test case. Details are found in the posted AI (not the one attached previously, I got an answer from PTC after I sent the previous e-mail). I conclude that taking GNAT's behavior exactly would have a serious risk of being run-time incompatible with existing code (a risk we shouldn't take). A more limited form, however, seems to be the right solution (surely we don't want the erroneous execution that all of the non-GNAT compilers have). **************************************************************** [Editor's note: Some of the e-mails in the following thread discussed topics in both this AI and AI12-0377-1. The e-mails were split and filed in the appropriate AI to avoid confusion.] From: Tucker Taft Sent: Wednesday, April 29, 2020 11:34 AM Wording suggestion: {* if the value is nonnull and belongs to the subtype of the parameter, then} the formal parameter is initialized from the value of the actual, without checking whether the value satisfies any predicate {* otherwise, the the formal parameter is initialized to the null value of the type of the formal, without checking that the value satisfies any predicate or any exclusion of the null value.} **************************************************************** From: Randy Brukardt Sent: Wednesday, April 29, 2020 3:21 PM "belongs" only describes constraints, and really only makes sense for related types. When you are converting *un*related types, there is a bunch of dynamic checks that are in addition to the constraint checks. For access types, these are 4.6(24.11-24.17) - seven paragraphs. These are repeated (ugh!) for memberships in 4.5.2(30.3/4), slightly differently since the result is True/False rather than raising an exception. I though it was best to piggyback on that paragraph rather than writing something new that would be forever wrong. I had hoped there was a term like "convertible" (but that term is static only) to cover this, but it doesn't exist. We could try to add one but I thought that was overkill for this particular fix. In any case, in your hands now. **************************************************************** From: Tucker Taft Sent: Wednesday, April 29, 2020 4:02 PM >"belongs" only describes constraints, and really only makes sense for related >types. When you are converting *un*related types, there is a bunch of dynamic >checks that are in addition to the constraint checks. For access types, these >are 4.6(24.11-24.17) - seven paragraphs. In my RM, those are all legality rules, so would not need to be repeated. The dynamic semantics are in (48-50). And I think some of them could be simplified in this situation. >These are repeated (ugh!) for memberships in 4.5.2(30.3/4), slightly >differently since the result is True/False rather than raising an exception. >I though it was best to piggyback on that paragraph rather than writing >something new that would be forever wrong. I'd like to at least try to come up with some wording that is simpler. Good point about "belongs" but it still does some of what we want. It also seems more natural to piggyback on wording having to do with conversion, rather than membership test. And there is already wording there for view conversion, so perhaps we should put it all in 4.6, and keep 6.4.1 very simple. >I had hoped there was a term like "convertible" (but that term is static >only) to cover this, but it doesn't exist. We could try to add one but I >thought that was overkill for this particular fix. Moving most of it to 4.6 from 6.4.1 might help. >In any case, in your hands now. OK. **************************************************************** From: Erhard Ploedereder Sent: Wednesday, April 29, 2020 4:57 PM ... > I had hoped there was a term like "convertible" (but that term is > static only) So what? Surely I can use a term of static semantics in describing a dynamic constraint. (Seems to me that the term has a perfect fit in context: "convertible" is reuqired as far as type is concerned plus extra aspect constraints). **************************************************************** From: Tucker Taft Sent: Wednesday, April 29, 2020 5:18 PM ... >> I had hoped there was a term like "convertible" (but that term is >> static only) > > So what? Surely I can use a term of static semantics in describing a > dynamic constraint. Again, this is about dynamic semantics, so we should be able to presume that legality rules have already been enforced. I think some of Randy's comments were about legality rules or static semantics, so don't really apply either. **************************************************************** From: Randy Brukardt Sent: Wednesday, April 29, 2020 6:45 PM >>"belongs" only describes constraints, and really only makes sense for >>related types. When you are converting *un*related types, there is a >>bunch of dynamic checks that are in addition to the constraint checks. >>For access types, these are 4.6(24.11-24.17) - seven paragraphs. >In my RM, those are all legality rules, so would not need to be repeated. >The dynamic semantics are in (48-50). And I think some of them could be >simplified in this situation. Sorry, I'm only talking about the Dynamic Semantics rules, whereever they are. :-) There are tag and accessibility checks on top of the constraint, null exclusion, and predicate checks, Here, there's no execution issue with predicates and it's better to skip them. The null exclusion doesn't change anything -- those fail if the value is null, but replacing null by null doesn't change anything so forget that. Constraints can go either way -- we already have to treat out parameters are unconstrained for other types (in particular, integer types) so doing so as well for access types (for which constraints are already strange) isn't a huge issue. (No compiler crashed that allowed that, both Janus/Ada and ApexAda raised Constraint_Error. ObjectAda incorrectly banned the conversion, and GNAT of course passed null for any such conversion, even one that has no semantic effect.) But once we start making some sort of check, it's probably easier to include constraints in it -- those are rare anyway. ****************************************************************