Version 1.5 of ai12s/ai12-0378-1.txt

Unformatted version of ai12s/ai12-0378-1.txt version 1.5
Other versions for file ai12s/ai12-0378-1.txt

!standard 6.4.1(13/3)          20-06-08 AI12-0378-1/03
!class Amendment 20-04-29
!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
An actual of an out parameter that is a view conversion of an access type is not illegal if the types are not related, rather null is passed if the access value would violate any constraint, tag check, or accessibility check that applies to the formal. Predicates and null exclusions are always ignored for out parameters.
!problem
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.
!proposal
(See Summary.)
!wording
Modify 6.4.1(13/3):
[Author's Note: Broken into two bullets.]
* For an access type, {
* if the value is nonnull, satisfies any constraints of the formal, has an accessibility level no deeper than that of the type of the formal, and, if the designated type is tagged, the tag of the designated object identifies a type that is covered by the type of the formal, then the formal parameter is initialized from the value of the actual, without checking whether the value satisfies any predicate;
* otherwise,} the formal parameter is initialized to the null value of the formal type, without checking that the value satisfies [any constraint,] any predicate[,] or any exclusion of the null value;
{AARM Implementation Note: This rule means that any constraint
checks, tag checks, and accessibility checks can be assumed to pass for an out parameter, but null exclusions and predicates cannot be assumed unless the compiler can prove that the object has been previously written within the subprogram.}
!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. However, the tag and accessibility checks are not normally done as usage sites, so assuming implementations will do the right thing would 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 problematic cases use the GNAT rule of passing null would not be significantly incompatible in practice. We would only want to use such a rule in cases that are actually problems, in order that derivation works as expected (as noted in the preliminary parts of the test program), and so that explicit calls that are exactly like calls on inherited subprograms work the same way as the inherited call. 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 convertable. 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 may have been a more sensible rule for a new language, but it would mean that changing an out parameter of a record type to an access-to-record type would also require changing the mode to "in out" if any discriminants or bounds need to be read. Presumably, that is why Ada initially made the choice it did. In any case, the runtime incompatiblity creating by generally making such a change would be intolerable, so it is much too late to contemplate that.
The only other alternative to requiring null to be passed would be to declare that the value of an out parameter whose actual is a view conversion and whose value does not pass a membership test for the subtype of the parameter is abnormal. In that case, reading the value within the subprogram makes the program erroneous (while assigning it first is fine). This would work but it introduces erroneous execution where none is really needed (the passing null solution involves no erroneous execution).
---
Note that we don't explicitly talk about converting between access types with different representations. We're expecting that an implementation with such representations will include them in conversion and membership checking in an appropriate manner. For instance, if one assumes that converting a bit pointer to a byte pointer raises an exception if the bit address is not an even byte, then so long as that is reflected in a membership test, all is well.
It's true that there doesn't seem to be any justification within the language to raise an exception in such a case, but it seems nasty to simply destroy an access value if the target type cannot properly represent the address. (Note that a similar thing can happen on a segmented machine, such as the original 8086.) One assumes that Ada implementations care enough about correctness to avoid user-beware cases. If they don't, it's not the language's job to try to fix them. [Author's note: We may want to add an AARM note somewhere in 4.6 to note this possibility, and possibly in 4.5.2 as well.]
!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.

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

Questions? Ask the ACAA Technical Agent