!standard 6.4.1(5.1/4) 20-06-07 AI12-0377-1/02 !standard 6.4.1(5.2/4) !standard 6.4.1(5.3/4) !standard 6.4.1(13.2/4) !standard 6.4.1(13.3/4) !class Amendment 20-04-20 !status work item 20-04-20 !status received 20-04-14 !priority Low !difficulty Medium !subject View conversions and out parameters of types with Default_Value revisited !summary An actual of an out parameter that is a view conversion is illegal if either the target or operand type has Default_Value specified while the other does not. Program_Error is raised if this occurs where one of the types is a generic formal type. !problem Consider this example: procedure View_Conversion_With_Default_Value is type Defaulted_Integer is new Integer with Default_Value => 123; procedure P (X : out Integer) is begin null; -- X happens to be unassigned end P; X : Defaulted_Integer; Y : Defaulted_Integer := 456; begin P (Integer (X)); -- Deinitialize X? P (Integer (Y)); -- Deinitialize Y? end View_Conversion_With_Default_Value; Even though the type of X and Y has specified Default_Value, the value of the actual isn't copied into the formal in this case (it falls under the rule of 6.4.1(15)), leaving the formal parameter uninitialized. Upon return, the value of the formal will be converted to Defaulted_Integer and assigned to the actual object. If the formal wasn't assigned to within the procedure, as in the above example, then the (uninitialized) value that comes back will deinitialize the actual object, which is the sort of thing that shouldn't normally be possible for an object whose type has a specified Default_Value. !proposal (See Summary.) !wording Replace 6.4.1(5.1/3) with: If the mode is out, the actual parameter is a view conversion, and the type of the formal parameter is a scalar type, then either: * the target and operand type both do not have the Default_Value aspect specified; or * the target and operand type both 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. [Editor's note: This removes the access part of this rule, and makes a version apply to all view conversions.] Revert 6.4.1(13.1/4) to its previous version, namely: 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. Delete 6.4.1(13.2-13.4/4), and AARM 6.4.1(13.d/4). Add after 6.4.1(15): * Furthermore, if the type is a scalar type, and the actual parameter is a view conversion, then Program_Error is raised if either the target or the operand type has the Default_Value aspect specified, unless they both have the Default_Value aspect specified, and there is a type (other than a root numeric type) that is an ancestor of both the target type and the operand type. !discussion "Deinitialization" here cannot introduce an invalid value into an object that the compiler assumed to be valid (the conversion back to the actual would still do a subtype check, which would cause Constraint_Error if the value is invalid. Note that the check would be required since one cannot assume anything about out parameters). Even so, it seems dangerous enough that we should take steps to prevent it. The best solution is to make any view conversion used in an out parameter illegal if one type has Default_Value and the other does not. Note that the repeal of 13.1(10/3) by AI12-0376-1 means that having the types be related no longer prevents problems (as a derived type can define a Default_Value when the parent type does not have one), so we would have to do something with this rule in any case. The alternative of passing the default value in problematic cases would make the deinitialization noted in the problem worse (since it would clobber the current value of the object unconditionally, even if it would be in range), so that is not a viable solution to this problem. !ASIS No ASIS effect. !ACATS test An ACATS B-Test is needed to check that the new rules are enforced, rather than the previous rules. !appendix From: Gary Dismukes Sent: Tuesday, April 14, 2020 5:43 PM Recently, while fixing a GNAT problem related to passing view conversions to out-mode parameters when Default_Value is involved (related to the rules added in AI12-0074), it occurred to me that there's a similar situation that can arise when the formal type doesn't have aspect Default_Value, but the type of the operand of the view conversion does have the aspect. (AI12-0074 addressed the case of formal types that specific Default_Value, requiring the type of an operand of a view conversion actual to also specify Default_Value, and this is sort of the reverse of that.) Consider this example, which is a minor variation of the one in the AI: procedure View_Conversion_With_Default_Value is type Defaulted_Integer is new Integer with Default_Value => 123; procedure P (X : out Integer) is begin null; -- X happens to be unassigned end P; X : Defaulted_Integer; Y : Defaulted_Integer := 456; begin P (Integer (X)); -- Deinitialize X? P (Integer (Y)); -- Deinitialize Y? end View_Conversion_With_Default_Value; Even though the type of X and Y has specified Default_Value, the value of the actual isn't copied into the formal in this case (it falls under the rule of 6.4.1(15)), leaving the formal parameter uninitialized. Upon return, the value of the formal will be converted to Defaulted_Integer and assigned to the actual object. If the formal wasn't assigned to within the procedure, as in the above example, then the (uninitialized) value that comes back will deinitialize the actual object, which is the sort of thing that shouldn't normally be possible for an object whose type has a specified Default_Value. You could argue that you shouldn't have a formal of mode out that isn't assigned to, but that's kind of beside the point, because the normal guarantee of Default_Value, that variables of the type always have a good value, is violated in this case, so we clearly have a safety hazard. To avoid this, the rules either need to require the value of the actual to be assigned to the formal going in, or such a conversion needs to be rejected. I was initially leaning towards the former, but Tuck has convinced me that this should be illegal. Basically we don't want to impose the penalty of preventing an optimization like transforming a procedure with a single elementary out formal into a function returning the parameter, just to protect against what is going to be the rare case of passing a view conversion of an operand with Default_Value. So it seems that we need to augment the rules of 6.4.1(5.1-5.3), which require the type of the operand of a view conversion to have Default_Value when the formal's type has Default_Value. We need to also disallow the case where a view conversion operand's type has Default_Value but the formal's type doesn't. I believe we'll also need to add something along the lines of the rules in 6.4.1(13.1-13.4) that require Program_Error be raised, to address call cases within generic bodies when formal types are involved (those rules were also added by AI12-0074). **************************************************************** From: Tucker Taft Sent: Tuesday, April 14, 2020 7:49 PM We have discussed this internally at AdaCore, and concluded that we should make it illegal to pass a scalar type with Default_Value in a view conversion to an OUT formal parameter without a Default_Value. Requiring "copy in" is not practical given the way that OUT parameters of a scalar type are sometimes implemented (such as transforming the procedure to a function and returning the OUT parameter as a function result). **************************************************************** From: Randy Brukardt Sent: Tuesday, April 21, 2020 7:57 PM > Recently, while fixing a GNAT problem related to passing view > conversions to out-mode parameters when Default_Value is > involved (related to the rules added in AI12-0074), it > occurred to me that there's a similar situation that can > arise when the formal type doesn't have aspect Default_Value, > but the type of the operand of the view conversion does have > the aspect. (AI12-0074 addressed the case of formal types > that specific Default_Value, requiring the type of an operand > of a view conversion actual to also specify Default_Value, > and this is sort of the reverse of that.) I'm not throughly convinced that this problem (in any form) is worth worrying about, since it can't cause an invalid value to be introduced to the program (at least in a correct implementation). If the implementation is assuming that the actual object is valid, then a constraint check is necessary at the back assignment and that would prevent any invalid values from being stored. The value would indeed be junk, but that's what was written (and it happens for all scalar out parameters). Anyway, assuming that we do want to keep this illegal in any case, we need to cover *this* case as well. Attached is an attempt at doing that, and also handling the access problem (with a solution close to what GNAT is currently doing). I suspect that the AI and especially the wording will need some wordsmithing. **************************************************************** [Editor's note: Some of the e-mails in the following thread discussed topics in both this AI and AI12-0378-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 mode is out, the actual parameter is a view conversion, and the type of the formal parameter is a scalar type, then either: * the target and operand type do not have the Default_Value aspect specified; or * the target and operand type both 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. **************************************************************** From: Randy Brukardt Sent: Wednesday, April 29, 2020 3:32 PM BTW, Claire, if you could provide an example of the problem that you were concerned about with the AI12-0377-1 proposal, it would be appreciated. My incomplete understanding of your concern made it appear that the only solution would be to ban all view conversions of types with Default_Value unless the defaults were the same. That would prevent declaring (or at least using) any derived types with different defaults (if there are primitive operations) -- but that was specifically the case that Arno wanted us to allow as he has customer code that depends upon it. It doesn't seem possible to thread that needle! **************************************************************** From: Erhard Ploedereder Sent: Wednesday, April 29, 2020 4:57 PM > If the mode is out, the actual parameter is a view conversion, and > the type of > the formal parameter is a scalar type, then either: > >   * the target and operand type do not have the Default_Value aspect >     specified; or I doubt that a Float and a Boolean, both without a Default_Value, are o.k. in a view conversion :-) (There is a convertability constraint missing). **************************************************************** From: Tucker Taft Sent: Wednesday, April 29, 2020 5:18 PM ... >> If the mode is out, the actual parameter is a view conversion, and >> the type of >> the formal parameter is a scalar type, then either: >> >> * the target and operand type do not have the Default_Value aspect >> specified; or > > I doubt that a Float and a Boolean, both without a Default_Value, are > o.k. in a view conversion :-) (There is a convertability constraint > missing). This is the dynamic semantics. The legality rules have already done their thing, so we only need to explain what happens with legal view conversions. **************************************************************** From: Erhard Ploedereder Sent: Thursday, April 30, 2020 8:46 AM >> I doubt that a Float and a Boolean, both without a Default_Value, are >> o.k. in a view conversion :-) (There is a convertability constraint >> missing). >> > This is the dynamic semantics. The legality rules have already done their > thing, so we only need to explain what happens with legal view conversions. > o.k. Understood. Or maybe only half of it, because the next bullet > * the target and operand type both 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. includes a compatibility constraint as well, which ought to have been checked by static semantics as well. (That part of the wording triggered my comment in the first place, a "why here, but not there?"-question). **************************************************************** From: Claire Dross Sent: Thursday, April 30, 2020 3:28 AM Honestly, I think I will need some help to understand the rationale of the rules for passing of OUT parameters with default values. Yesterday, we said that we did not want a null procedure to deinitialize a scalar variable whose type has a Default_Value aspect. But I don't really understand why. Surely null procedures will deinitialize variables which don't have this aspect, and this is a bounded error, right? So to say it otherwise, I think procedure P (X : out T) is begin null; end P; where T is an integer type without a Default_Value is wrong. Why should we do anything to support correctly (reject correctly) the corner case of call to such a function on a view conversion? **************************************************************** From: Jeff Cousins Sent: Thursday, April 30, 2020 3:57 AM To be honest, I agree with Claire. To not assign anything to an out parameter is plain wrong so I don't see why we should go to a lot of trouble to get it to return something sensible in certain circumstances. (A former colleague used somewhat stronger language). **************************************************************** From: Tucker Taft Sent: Thursday, April 30, 2020 8:08 AM One of the fundamental "promises" associated with types with a default, is they never become de-initialized. **************************************************************** From: Erhard Ploedereder Sent: Thursday, April 30, 2020 8:28 AM To me, the foremost reason in favor of the AI is that I would like to rely on the fact that any variable with specified default_value is guaranteed to always be initialized. No buts, ifs, and whens, or special cases or corners that I need to check for after all. Just like non-null types .... no buts, ifs, except whens.... And as to risk: a deinitialized Integer is almost as bad as a deinitialized access value. All I need to do is use it as index into an array, where matching subtype properties are statically established and hence the index check unnecessary. (I can even exploit that security hole by placing the "right value" into place by a preceeding call.) **************************************************************** From: Claire Dross Sent: Thursday, April 30, 2020 8:51 AM > To me, the foremost reason in favor of the AI is that I would like to > rely on the fact that any variable with specified default_value is > guaranteed to always be initialized. No buts, ifs, and whens, or special > cases or corners that I need to check for after all. But do we achieve that by the above? Here is an example: procedure Test_Out_Param with SPARK_Mode is type My_Neg is new Integer range Integer'First .. -1 with Default_Value => -1; type My_Pos is new Integer range 1 .. Integer'Last with Default_Value => 1; procedure P (X : out My_Neg) is begin null; end P; Y : My_Pos; begin P (My_Neg (Y)); end Test_Out_Param; With the change proposed in this AI, I get that after a call to P, Y indeed is still properly initialized. But what about inside P? Certainly X is not in the bounds of My_Neg, and the fact that its initial value is set by the default of some other type or not does not change anything about that... **************************************************************** From: Tucker Taft Sent: Thursday, April 30, 2020 9:04 AM Being initialized, but out of range, and having a random uninitialized value, are two different things. A compiler cannot assume an OUT parameter is initialized to an in-range value. But if it has a default, it can assume it is not a random bit pattern, so a constraint check, for example, will have a well-defined result. **************************************************************** From: Bob Duff Sent: Thursday, April 30, 2020 9:20 AM > To me, the foremost reason in favor of the AI is that I would like to > rely on the fact that any variable with specified default_value is > guaranteed to always be initialized. No buts, ifs, and whens, or > special cases or corners that I need to check for after all. > > Just like non-null types .... no buts, ifs, except whens.... Right, that's the property this AI is trying to preserve. > And as to risk: a deinitialized Integer is almost as bad as a > deinitialized access value. All I need to do is use it as index into > an array, where matching subtype properties are statically established > and hence the index check unnecessary. (I can even exploit that > security hole by placing the "right value" into place by a preceeding > call.) True in Ada 83, but now it's a bounded error, so "A(I) := ..." must either modify some element of A, or raise an exception if I is invalid. **************************************************************** From: Claire Dross Sent: Thursday, April 30, 2020 9:22 AM > Being initialized, but out of range, and having a random uninitialized > value, are two different things. A compiler cannot assume an OUT parameter > is initialized to an in-range value. But if it has a default, it can > assume it is not a random bit pattern, so a constraint check, for example, > will have a well-defined result. I must say the difference on integer types does not seem obvious to me. And the problem raised by Erhard seems to still occur? **************************************************************** From: Bob Duff Sent: Thursday, April 30, 2020 11:49 AM > I must say the difference on integer types does not seem obvious to > me. And the problem raised by Erhard seems to still occur? No, adding to your earlier example: procedure Test_Out_Param with SPARK_Mode is type My_Neg is new Integer range Integer'First .. -1 with Default_Value => -1; type My_Pos is new Integer range 1 .. Integer'Last with Default_Value => 1; A : array (My_Neg range -1 .. -1) of Integer := (others => 0); procedure P (X : out My_Neg) is begin A (X) := 123; -- Constraint_Error raised here. end P; Y : My_Pos; begin P (My_Neg (Y)); end Test_Out_Param; The above is required to raise C_E rather than trash some memory location. GNAT does that correctly (at least for this example). The compiler is not allowed to eliminate checks based purely on the subtype's range -- it must also prove that the value is valid. This was an Ada 95 change. **************************************************************** From: Bob Duff Sent: Thursday, April 30, 2020 11:53 AM Here's another variation on that example: procedure Test_Out_Param with SPARK_Mode is type My_Neg is new Integer range Integer'First .. -1 with Default_Value => -1; type My_Pos is new Integer range 1 .. Integer'Last with Default_Value => 1; procedure P (X : out My_Neg) is begin X := -1; end P; Y : My_Pos; begin P (My_Neg (Y)); -- Constraint_Error raised on copy back. end Test_Out_Param; **************************************************************** From: Randy Brukardt Sent: Thursday, April 30, 2020 2:02 PM >Just like non-null types .... no buts, ifs, except whens.... Actually, there is one "but" with null exclusions, one that I noticed working on this AI: Ptr : Some_Ptr := null; procedure P (A : out not null Some_Ptr) is -- Can't assume that A is not null here. end P; P (Ptr); A null exclusion on an out parameter cannot be assumed to be true, since there is no way to initialize it with a non-null value if the actual is in fact null (as in this example). We surely don't want to be conjuring a junk object out of the ether just so we can pass it on the possible chance that someone might read it. And we don't want to be raising exceptions for passing a value that probably never will be used. Which probably goes to show that there is no rule that has no exceptions. :-) **************************************************************** From: Tucker Taft Sent: Thursday, April 30, 2020 2:16 PM For OUT parameters, you generally have to assume the constraints are *not* satisfied on the way in. The point about having a default value, as do all access objects, is that you can presume it has a *well defined* value. The constraints on an "out" parameter are checked only on the way "out" so you should interpret an out parameter as always being a completely unconstrained object initially, that has some constraints you had better satisfy before you do a return. **************************************************************** From: Randy Brukardt Sent: Thursday, April 30, 2020 2:45 PM Right. But that means that whatever you can assume about every other object in an Ada program does not apply to "out" parameters. (with the exception of the tag and accessibility checks that would be impossible to properly check on a usage, the focus of AI12-0378-1, the split from AI12-0377-1 as requested yesterday). My point to Erhard's statement is that there is always a "but" when it comes to "out" parameters. **************************************************************** From: Richard Wai Sent: Thursday, April 30, 2020 2:59 PM I always saw such null excluded out parameters as expressing a contract on the subprogram. So a null exclusion on an “out” parameter is a contract which states that the subprogram promises to set the parameter to something not null. Similarly if you had an out parameter of some constrained subtype, that’s a contract on the subprogram to produce a value of that subtype. I don’t see anything wrong with this: Type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); Subtype Workday is Day range Monday .. Friday; Procedure Next_Meeting (On_Day: out Workday); My_Busy_Day: Day := Saturday; Next_Meeting (My_Busy_Day); The contract is that Next_Meeting should only give me a Workday. But I shouldn’t be stopped from using a parameter of Day, especially if it is not already set to a value in the range of Workday.. **************************************************************** From: Erhard Ploedereder Sent: Friday, May 1, 2020 8:10 AM Interesting .... but maybe there was a bug in the example? The semantics that you describe seems too be "in out" semantics. See -- for inserts and remarks ... >> I must say the difference on integer types does not seem obvious to >> me. And the problem raised by Erhard seems to still occur? > > No, adding to your earlier example: > > procedure Test_Out_Param with SPARK_Mode is > type My_Neg is new Integer range Integer'First .. -1 with > Default_Value => -1; > type My_Pos is new Integer range 1 .. Integer'Last with > Default_Value => 1; > > A : array (My_Neg range -1 .. -1) of Integer := (others => 0); -- added another array B : array (My_Pos range 1 .. 1) of Integer := (others => 0); > > procedure P (X : out My_Neg) is > begin > A (X) := 123; -- Constraint_Error raised here. -- I would have expected that the Default_Value -1 applies and A(-1) -- is perfectly fine, but B (X) := 123; -- has to raise Constraint_Error for B(-1) > end P; > > Y : My_Pos; > begin > P (My_Neg (Y)); -- has to raise Constraint_Error on the way out, when X (=-1) gets -- written to Y per copy semantics for scalars > end Test_Out_Param; **************************************************************** From: Bob Duff Sent: Friday, May 1, 2020 10:28 AM > Interesting .... but maybe there was a bug in the example? The > semantics that you describe seems too be "in out" semantics. Right, 'out' parameters sometimes pass information IN, which is strange. > > A (X) := 123; -- Constraint_Error raised here. > > -- I would have expected that the Default_Value -1 applies and A(-1) > -- is perfectly fine, but That's not an unreasonable expectation, but I guess that's not what language designers decided. The goal was "if it has a default, it can't get deinitialized", and never mind if the default is outside the bounds of the subtype, as we see in these weird 'out' param cases. > B (X) := 123; -- has to raise Constraint_Error for B(-1) > > > end P; > > > > Y : My_Pos; > > begin > > P (My_Neg (Y)); > > -- has to raise Constraint_Error on the way out, when X (=-1) gets > -- written to Y per copy semantics for scalars If it were assigned to -1 inside the procedure, then it would raise C_E on copy back. But if it's not assigned at all (which is a weird thing to do), then it remains +1, which copies back without C_E. Not sure I would have designed it that way... **************************************************************** From: Erhard Ploedereder Sent: Friday, May 1, 2020 11:16 AM > Right, 'out' parameters sometimes pass information IN, which is > strange. Well, yes, sometimes, but not the value in the scalar case with copy semantics. My reading of the Manual says that the value is uninitialized in the absence of Default_Value (8.6.1 (15)), or initialized to a specfified Default_Value (8.6.1.(13)). There is no third case. In consequence, the final value is just that if X is not assigned to in the body. So, your expectation that "1" from Y arrives at X is not justified by anything in the manual, or is it? And, if the actual value were always passed in, the Default_Value on out parameters would never engage and hence be utterly useless. It may be strange that it always engages for scalar outs, but I much prefer that to deinitialized variables despite an assertion to the contrary. **************************************************************** From: Randy Brukardt Sent: Friday, May 1, 2020 12:29 PM >> Right, 'out' parameters sometimes pass information IN, which is >> strange. >Well, yes, sometimes, but not the value in the scalar case with copy >semantics. >My reading of the Manual says that the value is uninitialized in the >absence of Default_Value (8.6.1 (15)), You mean 6.4.1(15). >or initialized to a specfified >Default_Value (8.6.1.(13)). 6.4.1(13) is about access types (and is one the the rules we need to change slightly). 6.4.1(13.1/4) is about types with Default_Value and it does not say that. It says the value of the actual is passed it without any check. That's the semantics that Ichbiah picked for out parameters like access and composites. I suppose we *could* have done something different regardless (it was a totally new feature), but it's getting rather late to change basic semantics of an Ada 2012 feature. Especially to silently change the semantics of existing, legal code -- that's the worst kind of incompatibility. >There is no third case. In consequence, the >final value is just that if X is not assigned to in the body. That's true, but you have dreamed up semantics that doesn't exist in the RM. **************************************************************** From: Erhard Ploedereder Sent: Friday, May 1, 2020 6:05 PM Sorry that I mistyped the references. I meant 6.4.1. in both cases. Here they are again in corrected form: My reading of the Manual says that the value is uninitialized in the absence of Default_Value (6.4.1 (15)), or initialized to ... (6.4.1.(13.1/4)). But you are right. I imagined semantics that never existed. I didn't even read the sentence to its conclusion because it was so obvious what the initial value would be. It is hard to believe that the semantics of Default_Value for out parameters are so misleading -- the type says that the default initial value is 1, and then means that the initial value is taken from the actual. Ok. Case closed for me, since the RM is consistent albeit very surprising. **************************************************************** From: Tucker Taft Sent: Saturday, May 2, 2020 7:46 AM >To be honest, I agree with Claire. To not assign anything to an out >parameter is plain wrong so I don't see why we should go to a lot of trouble >to get it to return something sensible in certain circumstances. (A former >colleague used somewhat stronger language). I remember Bob Morgan used to say something like: "Just because it is erroneous doesn't mean we should generate code that deletes the disk." The fact is that most programs contain code whose execution might result in erroneous execution or produce a bounded error. We provide certain guarantees so that even though the code may be buggy, you know the consequences are limited. Ada's access types have always made the guarantee that an access value is never complete junk. When we introduced the Default_Value aspect, our goal was to make the same guarantee, namely that a value of a type with a Default_Value is never complete junk. You may believe that was a mistake, but we made it many years ago, and at this point, I think we should not withdraw that guarantee. We have discovered a corner case with view conversions. I don't think that means we should say: the guarantee holds except in certain corner cases. We should come up with a reasonable rule for this corner case. **************************************************************** From: Tullio Vardanega Sent: Saturday, May 2, 2020 7:51 AM This is my view also. Not a view conversion, though. **************************************************************** From: Claire Dross Sent: Monday, May 4, 2020 1:50 AM >I don’t see anything wrong with this: > >Type Day is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); > >Subtype Workday is Day range Monday .. Friday; > >Procedure Next_Meeting (On_Day: out Workday); > >My_Busy_Day: Day := Saturday; > >Next_Meeting (My_Busy_Day); > >The contract is that Next_Meeting should only give me a Workday. But I >shouldn’t be stopped from using a parameter of Day, especially if it is not >already set to a value in the range of Workday.. What is more dubious is that nothing prevents me from not setting On_Day to a Workday in Next_Meeting, and so, even if day has a default value. ****************************************************************