Version 1.4 of ai12s/ai12-0074-1.txt

Unformatted version of ai12s/ai12-0074-1.txt version 1.4
Other versions for file ai12s/ai12-0074-1.txt

!standard 4.6(56)          13-07-08 AI12-0074-1/03
!standard 6.4.1(12)
!standard 6.4.1(13.1/3)
!class binding interpretation 13-06-09
!status work item 13-07-08
!status ARG Approved 9-0-0 13-06-15
!status work item 13-06-09
!status received 13-04-30
!priority Medium
!difficulty Hard
!qualifier Omission
!subject View conversions and scalar out parameters passed by copy
!summary
Scalar view conversion values are well defined if the Default_Value aspect of the target type is specified.
!question
6.4.1(12-15) reads in part:
For an out parameter that is passed by copy, the formal parameter object is created, and:
... For a scalar type that has the Default_Value aspect specified, the formal parameter is initialized from the value of the actual, without checking that the value satisfies any constraint or any predicate
This presupposes that "the value of the actual" exists and is well defined.
How does this work in the case where the actual parameter is a view conversion? Consider:
with text_io; use text_io; procedure bad_conv1 is type Defaulted_Integer is new Integer with Default_Value => 123;
procedure foo (x : out integer) is begin null; end;
y : long_long_float := long_long_float'last; -- or, for a variation, leave y uninitialized begin foo (defaulted_integer (y)); -- Now illegal. put_line (long_long_float'image (y)); end;
with text_io; use text_io; procedure bad_conv2 is
type mod3 is mod 3 with default_value => 0; type mod5 is mod 5;
procedure foo (xx : out mod3) is begin null; end;
yy : mod5 := 4; begin foo (mod3 (yy)); -- Now illegal. put_line (mod5'image (yy)); end;
In this case, the converted value might not (probably won't) fit into the parameter representation. What should happen? (The calls on Foo are illegal.)
!recommendation
(See !summary.)
!wording
Modify 4.6(56):
Reading the value of the view yields the result of converting the value of the operand object to the target subtype (which might raise Constraint_Error), except if the object is of an {elementary}[access] type and the view conversion is passed as an out parameter; in this latter case, the value of the operand object is used to initialize the formal parameter without checking against any constraint of the target subtype ({as described more precisely in}[see] 6.4.1).
[Editor's note: It's not clear to me that this is sufficiently vague to avoid conflict with 6.4.1.]
Modify AARM 4.6(56.a):
This ensures that even an out parameter of an {elementary}[access] type is initialized reasonably.
Append to end of 6.4.1 Legality Rules section:
If the mode is out, the type of the formal parameter is a a scalar type that has the Default_Value aspect specified, and the actual parameter is a view conversion, then
- the type of the operand of the conversion shall have the
Default_Value aspect specified; and
- there shall exist a type (other than a root numeric type) that is
an ancestor of both the target type and the operand type.
In addition to the places where Legality Rules normally apply (see 12.3), this rule applies also in the private part of an instance of a generic unit.
AARM note: This rule is needed in order to ensure that a well-defined parameter value is passed in.
Replace 6.4.1(13.1/3)
For a scalar type that has the Default_Value aspect specified, the formal parameter is initialized from the value of the actual, without checking that the value satisfies any constraint or any predicate;
with
For a scalar type that has the Default_Value aspect specified, the formal parameter is initialized from the value of the actual, without checking that the value satisfies any constraint or any predicate, except in the following case: if the actual parameter is a view conversion and either
- there exists no type (other than a root numeric type) that
is an ancestor of both the target type and the type of the operand of the conversion; or
- the Default_Value aspect is unspecified for the type of the
operand of the conversion
then Program_Error is raised;
[Note: The Program_Error case can only arise in the body of an
instance of a generic. Implementations that macro-expand generics can always detect this case when the enclosing instance body is expanded.]
!discussion
The value of an elementary view conversion is that of its operand.
We want the value of an elementary view conversion to be a well defined value of the target type of the conversion if the target type is either an access type or a scalar type for whose Default_Value aspect has been specified.
4.6(56) is supposed to define this, but it isn't close to correct. First, it was never updated to include predicates and null exclusions as 6.4.1(13/3) was. Second, it doesn't include the rules for scalar types with Default_Values specified at all. Third, it doesn't explain what happens if the original value does not fit in the representation of the target type (as in the example in the question).
Note that the "does not fit" case already exists in Ada, all the way back to Ada 83. Specifically, if an implementation has multiple representations for access types, and the view conversion is going from the larger representation to the smaller one, then the problem could occur. [This happened historically; the segmented architecture of the 16-bit 8086 led to compilers supporting both long (with segments) and short (without segments) pointers. Similarly, the Unisys U2200 compiler that the author worked on supported Ada pointers (machine addresses) and C pointers (byte addresses, a pair of a machine address and a byte offset).]
Clearly, the "does not fit" problem was unusual for access types, so it's not surprising that it was never solved. However, it becomes much more important for scalar types with Default_Values, especially when the source object is of a type that does not have a Default_Value (and thus might be uninitialized).
To ensure that the value of a scalar view conversion is well-defined, we disallow such scalar view conversions in cases where either
- the set of values of the type of the operand and of the
target type need not be the same; or
- an object of the type of the operand might not have a well
defined value.
The rule that was chosen to accomplish this is that the two types must have a common ancestor type and, in the scalar case, the operand type's Default_Value aspect must be specified.
This is expressed both as a Legality Rule (which handles most cases statically) and as a runtime check (which handles certain obscure cases involving generic instance bodies which make it past the legality rule). For an implementation which macro-expands instance bodies, the outcome of this runtime check is always known statically.
The runtime check would be triggered in examples like the following:
declare type T is range 0 .. 100; type T_With_Default is new T with Default_Value => 0;
generic type New_T is new T; package G is end G;
package body G is X : T;
procedure P (X : out New_T) is begin X := 0; end; begin P (New_T (X)); -- conversion raises Program_Error end G;
package I is new G (T_With_Default);
begin null; end;
This Legality Rule is compatible with previous versions of Ada as they do not have the Default_Value aspect. No calls on inherited subprograms will be affected, as 13.1(10) ensures that the Default_Value aspect is the same for the derived type and the parent type, and of course the types are related in this case. That means that all affected view conversions will be explicit in the program.
We do not use a Legality Rule to handle the more minor issue with access types, as that would be incompatible with existing Ada code.
[Editor's note: Steve and I strongly disagree on how to handle the access type case. Steve argues that the language is well-defined and that any compiler that choses an access representation that cannot be converted in both directions without information lose is simply wrong. I argue that such cannot be the intent of the language, as it would effectively require all access types to be represented the same. In the case of a machine where interfacing to a language like C requires expensive byte-pointers rather than cheaper word-pointers, it would prevent Ada from using the word pointers at all if it wants to support C interfacing. A similar issue would arise if some access types needed to support x86 segments (which would be far more expensive than unsegmented access types). If Steve's intrepretation holds, then the only way to have a useful compiler in these situations would be to ignore the standard. That's not a direction that we want to go.
I still believe that it should be a Bounded Error to do an untagged elementary view conversion for any target type with an implicit initial value if the representations of the types differ; the possible results being Constraint_Error, Program_Error, or works properly (no information loss). (And I wouldn't bother with the runtime check for the generic cases above, as the bounded error would cover them.) This way, a compiler only need to make a check if a representation change is involved, and any result will not fit. I would not include the value in the bounded error conditions, so a safety-critical compiler could always raise Program_Error if any case would fail; alternatively, the check could be made on the value (this is what Janus/Ada's code generator actually does, whether the language allows it or not, since we're not going to lose information or generate wildly expensive code just to make Steve happy).]
---
An alternative solution was to initialize the parameter with the Default_Value in this case. However, that would potentially "deinitialize" the actual object in the view conversion case:
type Small is range 0 .. 255 with Default_Value => 0;
procedure P (Do_It : in Boolean; Result : out Small) is begin if Do_It then Result := 10; end if; end P;
Obj : Positive := 1;
P (Do_It => False, Result => Small(Obj)); -- (1)
The call to P at (1) would raise Constraint_Error upon return, even though the previously initialized value of Obj is properly initialized and it wasn't changed. This does not seem acceptable, especially as the similar rules for access types do not have this effect.
!ACATS Test
An ACATS test will be needed to test whatever solution is adopted.
!ASIS
No ASIS effect.
!appendix

From: Steve Baird
Sent: Tuesday, April 30, 2013  6:30 PM

We've got

   For an out parameter that is passed by copy, the formal
   parameter object is created, and:
     ...
     For a scalar type that has the Default_Value aspect specified,
     the formal parameter is initialized from the value of the actual,
     without checking that the value satisfies any constraint or any
     predicate

This presupposes that "the value of the actual" exists and is well defined.

How does this work in the case where the actual parameter is a view conversion?
Perhaps the Default_Value should be copied in in cases like

   with text_io; use text_io;
   procedure bad_conv1 is
     type Defaulted_Integer is new Integer
       with Default_Value => 123;

     procedure foo (x : out integer) is begin null; end;

     y : long_long_float := long_long_float'last;
      -- or, for a variation, leave y uninitialized
   begin
    foo (defaulted_integer (y));
    put_line (long_long_float'image (y));
   end;

   with text_io; use text_io;
   procedure bad_conv2 is

     type mod3 is mod 3 with default_value => 0;
     type mod5 is mod 5;

     procedure foo (xx : out mod3) is begin null; end;

     yy : mod5 := 4;
   begin
    foo (mod3 (yy));
    put_line (mod5'image (yy));
   end;

If copy-in involves converting a (frequently uninitialized) value, that would
not be good - this feature was not supposed to make programs less reliable.

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

From: Steve Baird
Sent: Wednesday, May 1, 2013  1:37 AM

>   For an out parameter that is passed by copy, the formal
>   parameter object is created, and:
>     ...
>     For a scalar type that has the Default_Value aspect specified,
>     the formal parameter is initialized from the value of the actual,
>     without checking that the value satisfies any constraint or any
>     predicate

I just discovered this and said "ouch". How comes the formal parameter is not
initialized from the default value, which is certainly what a casual user would
expect?

(didn't find anything about this in the AARM or the AIs)

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

From: Robert Dewar
Sent: Wednesday, May 1, 2013  7:27 AM

> I just discovered this and said "ouch". How comes the formal parameter
> is not initialized from the default value, which is certainly what a
> casual user would expect?

I agree this makes more sense (btw, it's what GNAT happens to do now I believe).

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

From: Bob Duff
Sent: Wednesday, May 1, 2013  7:34 AM

> >   For an out parameter that is passed by copy, the formal
> >   parameter object is created, and:
> >     ...
> >     For a scalar type that has the Default_Value aspect specified,
> >     the formal parameter is initialized from the value of the actual,
> >     without checking that the value satisfies any constraint or any
> >     predicate
> >
> I just discovered this and said "ouch". How comes the formal parameter
> is not initialized from the default value, which is certainly what a
> casual user would expect?

That would make more sense, I think, but that's not how it works for records.  A
record with defaulted components gets passed in for mode 'out', so I guess
Default_Value was designed by analogy.

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

From: Robert Dewar
Sent: Wednesday, May 1, 2013  7:38 AM

> That would make more sense, I think, but that's not how it works for
> records.  A record with defaulted components gets passed in for mode
> 'out', so I guess Default_Value was designed by analogy.

The behavior for records is a bit strange, but it is more efficient, and easy to
implement.

It was a bad idea to extend the analogy

Especially given the nasty issues it raises as noted by Steve

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

From: Tucker Taft
Sent: Wednesday, May 1, 2013  7:47 AM

All access values are copied in so that is another reason scalars use the same
rule.

Sent from my iPhone

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

From: Bob Duff
Sent: Wednesday, May 1, 2013  8:07 AM

> All access values are copied in so that is another reason scalars use
> the same rule.

Good point.  I'm not sure that's the best rule either, but the analogy there is
even more apt.

> Sent from my iPhone

Sent from my Dell computer.

> Especially given the nasty issues it raises as noted by Steve

Well, I can't get too excited about interactions with view conversions of
scalars.  That's a weird feature anyway.  Is it even useful?

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

From: Randy Brukardt
Sent: Wednesday, May 1, 2013  1:00 PM

> That would make more sense, I think, but that's not how it works for
> records.  A record with defaulted components gets passed in for mode
> 'out', so I guess Default_Value was designed by analogy.

I think the analogy was with parameters of access types (which are elementary)
rather than parameters of record types. We essentially copied the model of
access types (which of course always have a default value of Null) rather than
invent a new one.

The problem, of course, is that not all scalar types have a default value, while
that is true of access types. So these sorts of problems can appear.

I think the *best* solution would be to ban view conversions between types that
differ in whether they have default_values. That would mostly eliminate the
problem; probably we should ban conversions from other kinds of types in this
case as well. After all, the only reason that these conversions exist is to
support inheritance for type derivation, and that does not require a view
conversion of a float to an integer!

The problem with the *best* solution is that it would be a contract model
violation. Thus, I would suggest instead that any such conversion raise
Program_Error (that would eliminate Steve's concern about making things more
fragile). After all, all that would have to be done to eliminate such a
conversion is to introduce a temporary.

As far as initializing with the default value, I don't see anything particularly
logical about that. You're passing an object that presumably has a value (of
something), and that value suddenly disappears even when the parameter is unused
by the routine? That's going to cause more bugs than it fixes. This sort of
structure sometimes occurs in my code:

    procedure Get_Data (From : in Some_Container;
                        Key  : in Key_Type;
                        Data : out Data_Type;
                        Found: out Boolean) is
    begin
	  if Exists (From, Key) then
             Found := True;
             Data := From (Key);
        else
             Found := False;
             -- Data unchanged here.
        end if;
    end Get_Data;

    declare
        Found : Boolean;
        Data  : Data_Type := Initial_Value;
    begin
        Get_Data (A_Container, Some_Key, Data, Found);
        -- Use Data here and ignore Found.
    end;

Changing the untouched result of Data simply because the type has a
Default_Value specified (Get_Data probably is declared in a generic, after all)
seems bizarre.

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

From: Gary Dismukes
Sent: Wednesday, May 1, 2013  1:22 PM

> > I just discovered this and said "ouch". How comes the formal
> > parameter is not initialized from the default value, which is
> > certainly what a casual user would expect?
>
> I agree this makes more sense (btw, it's what GNAT happens to do now I
> believe).

Actually what GNAT does is deinitialize the actual.

In any case, I agree with Randy that it's a bad idea to default initialize the
formal, as you don't want an initialized actual to be deinitialized in the case
where the formal is not assigned.  (Some other solution may be needed for the
weird view conversion case, though such cases are pretty uncommon.)

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

From: Jean-Pierre Rosen
Sent: Wednesday, May 1, 2013  1:29 PM

> As far as initializing with the default value, I don't see anything
> particularly logical about that.

I always considered that the model of out parameters is one of an unitialized
local variable. Simple, and easy to explain.

The benefit of that is that you are telling the caller that it is perfectly safe
to call your subprogram with an uninitialized actual, precisely because you want
to initialize it.

If for some reason you use the previous value of the actual (including because
you want to return the previous value unchanged, as in your example), use an
in-out parameter.

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

From: Randy Brukardt
Sent: Wednesday, May 1, 2013  6:02 PM

Which means you need two subprograms, one that allows passing uninitialized
values, and one that allows leaving the value untouched. If you pass an "in out"
parameter an uninitialized value, you run the risk of a stray Constraint_Error
(a problem I know well, since most of my code has been running afoul of this
problem since Janus/Ada started strictly following the Ada 95 checking rules).
To fix this in my code, I've changed a lot of routines to take "out" parameters.
But if that change also changes the behavior of calls that pass initialized
values, then it becomes almost impossible to fix this problem short of ensuring
that every value that can be passed to a routine is always initialized (which is
a lot of useless code).

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

From: Jean-Pierre Rosen
Sent: Thursday, May 2, 2013  3:42 AM

> Which means you need two subprograms, one that allows passing
> uninitialized values, and one that allows leaving the value untouched.
> If you pass an "in out" parameter an uninitialized value, you run the
> risk of a stray Constraint_Error

It's an interesting problem... The whole issue comes from "-- Data unchanged
here.", where you want the "unchanged" to include invalid values. The heart of
the problem is that you want to pass an invalid value in, without raising
Constraint_Error. An out parameter allows you to do that, precisely on the
assumpting that the invalid value cannot be used.

Rather than relying on an out parameter being transmitted, I think I'd rather
write:
   Get_Data (Cont, Key, Temp, Found);
   if Found then
      My_Data := Temp;
   end if;

At least, the expected behaviour is much more explicit to the reader.

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

From: Jean-Pierre Rosen
Sent: Thursday, May 2, 2013  3:46 AM

>> All access values are copied in so that is another reason scalars use
>> the same rule. -Tuck
> Good point.  I'm not sure that's the best rule either, but the analogy
> there is even more apt.

Access values are special. The natural behavior would be to initialize out
parameters to null, but if null is excluded, it is necessary to find a non null,
non invalid value - and the previous value of the parameter is the only such
thing available. This does not apply to scalars, especially if an explicit
default value is specified.

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

From: Bob Duff
Sent: Thursday, May 2, 2013  8:48 AM

> Access values are special. The natural behavior would be to initialize
> out parameters to null, but if null is excluded, it is necessary to
> find a non null, non invalid value - and the previous value of the
> parameter is the only such thing available.

No, if the 'out' formal subtype excludes null, but the actual is null, we pass
in null without raising C_E.  That's exactly analogous to Steve's example with
scalars.

Same if the formal has "Predicate => Subtype_Name /= null".

>...This does not apply to scalars,
> especially if an explicit default value is specified.

The only difference is that access types always have a default, whereas scalars
have a default only if Default_Value was specified.

> Rather than relying on an out parameter being transmitted, I think I'd
> rather write:
>    Get_Data (Cont, Key, Temp, Found);
>    if Found then
>       My_Data := Temp;
>    end if;

Yes, that's what I would write, too.

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

From: Tucker Taft
Sent: Thursday, May 2, 2013  11:09 AM

If you look in 4.6, para 56, you will see special handling for view conversions
of OUT parameters of an access type. This same paragraph should have been
modified to say the same thing for scalar types with a Default_Value specified.

>> Access values are special. The natural behavior would be to
>> initialize out parameters to null, but if null is excluded, it is
>> necessary to find a non null, non invalid value - and the previous
>> value of the parameter is the only such thing available.
>
> No, if the 'out' formal subtype excludes null, but the actual is null,
> we pass in null without raising C_E.  That's exactly analogous to
> Steve's example with scalars. ...

Right.  Access values and Scalar types with Default_Value specified should be
treated identically, as far as I can tell.  We just missed 4.6(56) when we
defined Default_Value, as far as I can tell.

The only teensy problem I see here is if the formal parameter is represented
with fewer bits than that required for the Default_Value of the actual
parameter's type.  The view conversion will be difficult to accomplish into this
smaller space.  This generally doesn't come up with access values, though I
suppose it could if one were a "fat pointer" and the other was a "thin pointer."

This sounds like a fun topic to discuss in the Berlin ARG. ;-)

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

From: Randy Brukardt
Sent: Thursday, May 2, 2013  5:55 PM

> If you look in 4.6, para 56, you will see special handling for view
> conversions of OUT parameters of an access type.
> This same paragraph should have been modified to say the same thing
> for scalar types with a Default_Value specified.

Thanks Tuck, for pointing this out. This seems just like a restating of the
original 6.4.1(13) -- and it's wrong even for access types because it doesn't
mention predicates and null exclusions. I suppose we need something like
6.4.1(13.1/3) as well.

But this doesn't seem to address Steve's problem, which is what if the value
simply doesn't fit into T?

...
> The only teensy problem I see here is if the formal parameter is
> represented with fewer bits than that required for the Default_Value
> of the actual parameter's type.

We already have covered this for the normal case (AARM 6.4.1(13.c/3) says "out
parameters need to be large enough to support any possible value of the base
type of T"). I recall some Tucker person bringing this up at the second St. Pete
Beach meeting at the Sirata (I think that's where it happened).

I don't think that the Default_Value of the actual has any bearing on this,
though, it's just the type of the actual and whether the *formal* has a
Default_Value specified.

> The view conversion will be difficult to accomplish into this smaller
> space.  This generally doesn't come up with access values, though I
> suppose it could if one were a "fat pointer"
> and the other was a "thin pointer."

And that's the crux of Steve's problem. What happens if the value simply can't
be made to fit (as in his Long_Long_Float'First example)?

And you are right, that this is not a new problem and doesn't specifically have
much to do the new features -- they just made the issue a lot more likely and
probably pushed it from a pathology to a likelihood. [Note that the Janus/Ada
U2200 compiler had two kinds of access values: C Pointers that included byte
addresses (not native on a 36-bit word machine) and Ada pointers that didn't (we
always ensured that allocated and aliased objects were aligned, thus we could
use the substantially cheaper pointers). We needed the C Pointers in order to
interface to the Posix subsystem, of course. I don't know what would have
happened if someone used an "out" parameter view conversion of a C Pointer to an
Ada Pointer -- perhaps the byte address part would have been dropped on the
floor -- but it probably wasn't 100% kosher.]

> This sounds like a fun topic to discuss in the Berlin ARG. ;-)

Surely funner than Adam's anonymous access allocated task issues, although I
think we can get away with a one-word change there if we wish. (And I'm sure
we're going to prefer that to duplicating all of the anonymous access rules into
the tasking section.)

Anyway, I think this case (the larger object into the smaller out parameter) has
to be a bounded error, so that compilers can detect it. The only other
alternative is to raise an exception if the actual value doesn't fit (we're not
going to change the semantics of out parameters at this late date, even if J-P
thinks it's a good idea). And that would mean that whether an exception is
raised depends on the value and for uninitialized things, would be
unpredictable. (This has to be a Bounded Error rather than a Legality Rule as
otherwise we'd have nasty generic contract issues. I suppose a pair of Legality
and Program_Error would work as it does for accessibility but that seems more
complex than necessary.)

More importantly, the only reason this feature exists is to provide a simple
explanation of how inherited derived subprogram calls work; in all other cases,
an explicit temporary makes more sense. And the Bounded Error I'm proposing
cannot happen for a derived type, as you can't change anything that would
trigger this Bounded Error for a derived type that has derived subprograms
because of the infamous 13.1(10/3). Any case where the programmer wrote this
explicitly would be better written with an explicit temporary (so they can leave
it uninitialized if that's OK, or initialize explicitly however they need).

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

Questions? Ask the ACAA Technical Agent