!standard 6.5(24/2) 07-10-30 AI05-0050-1/02 !class binding interpretation 07-05-04 !status work item 07-05-04 !status received 07-04-20 !priority Medium !difficulty Medium !qualifier Omission !subject Return permissions are not enough for build-in-place !summary If an object is being initialized by a function call and the implementation can tell before the call that the object subtype and the function result subtype are incompatible, then the implementation should be permitted to raise an exception before the call. If the object declared in an extended return statement is unconstrained, has discriminants, and is nonlimited, the existing permission does not apply. (In this case, the discriminant values might change before the extended return statement finishes, so raising an exception when the object is declared could raise an exception that would not be raised by the full call). Note that the permission still applies to simple return statements and constrained return objects in extended return statements. All of these permissions apply to recursive calls (that is, calls to a function in a return statement). !question Should the implementation permissions of 6.5(24/2) be revised? The current permission both requires inefficient implementations in some cases and also allows an exception to be raised in cases where no exception would be raised without the permission. !recommendation (See summary.) !wording Replace 6.5(24/2) If the result subtype of a function is unconstrained, and a call on the function is used to provide the initial value of an object with a constrained nominal subtype, Constraint_Error may be raised at the point of the call (after abandoning the execution of the function body) if, while elaborating the return_subtype_indication or evaluating the expression of a return statement that applies to the function body, it is determined that the value of the result will violate the constraint of the subtype of this object. with If a call on a function is used to provide the initial value of an object of a composite type, then certain actions which would be performed after the evaluation of the function call according to the canonical semantics may be performed earlier. Consider the following three points at which the constraints of the function result may become known: - before calling the function (in the case where the function result subtype is constrained) - during the elaboration of the return object declaration of an extended_return_statement of the function - during the evaluation of the expression of a simple_return_statement of the function . If, at any of these points, it is determined that the function result value will violate either the constraint of the subtype of the object or some other intermediate constraint that would be checked during the initialization of the object, then Constraint_Error may be raised at that point. If the function call is used to provide the initial value of an initialized allocator, then any calls to System.Storage_Pools.Allocate (or System.Storage_Pools.Deallocate) which are associated with the allocation of the object may be performed at any of these points, or at the point of a non-value-returning exit from an extended return statement of the function. These permissions do not apply in the case of an extended_return_statement where the function result object is discriminated and is not known to be constrained. AARM Notes These permissions apply transitively in the case of a function call which returns the value of another function call. In the following example, declare type T is limited record ... ; end record; -- requires in-place initialization type Unconstrained is array (Positive range <>) of T; function Some_Function return T is ... ; function F1 return Unconstrained is begin return (1 .. 10 => Some_Function); end F1; function F2 return Unconstrained is begin return F1; end F2; X : Unconstrained (1..3) := F2; begin null; end; , Constraint_Error may be raised in F1 before any calls to Some_Function are executed. The "intermediate constraint" wording refers to cases like declare type T is limited record ... ; end record; -- requires in-place initialization type Unconstrained is array (Positive range <>) of T; subtype Constrained is T (1 .. 3); function Some_Function return T is ... ; function F1 return Unconstrained is begin return (1.. 10 => Some_Function); end; function F2 return Constrained is begin return F1; end if; function F3 return Unconstrained is begin return Constrained'(F1); end F3; begin begin declare X : Unconstrained := F2; begin null; end; exception when Constraint_Error => null; end; begin declare X : Unconstrained := F3; begin null; end; exception when Constraint_Error => null; end; begin declare X : Unconstrained := Constrained'(F1); begin null; end; exception when Constraint_Error => null; end; end; In all three of these cases, Constraint_Error may be raised in F1 before the execution of any calls to Some_Function. The "These permissions do not apply" clause handles the case of an extended return object with mutable discriminants. Because of this clause, the permissions of this section do not apply in the following example declare type T (Is_Big : Boolean := True) is record case Is_Big is when False => null; when True => Big_Component : String (1..1024) := (others => 'X'); end case; end record; function F return T is begin return X : T do X.Big_Component := (others => 'Y'); X := (Is_Big => False); end return; end F; Small_Object : T (Is_Big => False) := F; begin null; end; and so Constraint_Error may not be raised (which is good). Note that this revocation of permissions only applies in cases where in-place initialization is optional. This means that any difficulties associated with implementing in-place initialization without these permissions can be sidestepped. !discussion The implementation permissions of 6.5(24/2) do not go far enough. This section states: If the result subtype of a function is unconstrained, and a call on the function is used to provide the initial value of an object with a constrained nominal subtype, Constraint_Error may be raised at the point of the call (after abandoning the execution of the function body) if, while elaborating the return_subtype_indication or evaluating the expression of a return statement that applies to the function body, it is determined that the value of the result will violate the constraint of the subtype of this object. This is a fine start, but more is needed in order to efficiently implement initialization in place as required by AI95-00318-02. -------- Issue #1: If an object is being initialized by a function call and the implementation can tell before the call that the object subtype and the function result subtype are incompatible, then the implementation should be permitted to raise an exception before the call. Consider: declare type T; type Inner (D : access T) is limited null record; type T is limited record F : Inner (Outer'Access); end record; -- requires initialization in place type Unconstrained is array (Positive range <>) of T; subtype S1 is Unconstrained (1 .. Ident_Int (10)); subtype S2 is Unconstrained (1 .. Ident_Int (11)); function F return S1 is begin Text_Io.Put_Line ("F was called"); return (others => <>); end F; X : S2 := F; begin null; end; An implementation should be allowed to raise Constraint_Error before the call to F (without invoking 11.6). In the case where the result subtype of a function is a constrained definite structure, an implemention ought to be able to have a caller simply pass in a buffer address; nothing more complicated than that should be needed. In order to implement initialization in place, F must be passed the address of X. An implementation must verify before the call that S1 and S2 have compatible constraints - it would not do to pass in, for example, the address of a small object to a function which is going to fill in a large result. The question is what to do if that test fails. The simple thing would be to raise Constraint_Error at that point. The annoyingly complicated thing that the language currently seems to require is to allocate a buffer of the size the function expects, pass that buffer address into the function, finalize the returned value, and then raise Constraint_Error (or something like that). One could go further and handle cases involving qualified expressions such as Y : S1 := S2'(F); , but this doesn't seem necessary. -------- Issue #2: Initialization in place is only required for limited types, so it would be ok for an implementation to ignore the implementation permissions of 6.5(24/2) for a nonlimited type. If an implementation chooses not to do this, however, there can be problems with extended return statements which attempt to modify discriminants of the function result object after it has been initialized. Consider: subtype Index is Integer range 1 .. 20; type Rec (Length : Index := 10) is record F : String (1 .. Length); end record; function F return Rec is begin return Result : Rec do Result := (5, "12345"); Text_Io.Put_Line ("Assignment succeeded"); end return; end F; X1 : Rec (10) := F; For an in-out or out mode parameter with an unconstrained nominal subtype, the formal inherits the constraints (if any) of the actual. The same model should apply, at the discretion of the implementation, to return objects in the case where the nominal subtype of the return object is unconstrained and definite and "a call on the function is used to provide the initial value of an object with a constrained nominal subtype". This means that for the preceding example, execution of F could (at the discretion of the implementation) be abandoned (and Constraint_Error raised at the call site) at the point of the assignment statement. If the literal "10" in the declaration of X1 were replaced with some other value, then the default initialization of Result could raise Constraint_Error. There is also a similar issue in the case of a function call which is used to initialize an allocator when the designated subtype of the access type is unconstrained but allocated objects themselves are constrained. ------- Issue #3: The implementation permissions of 6.5(24/2) should apply recursively in the case where a function call returns the result of another function call. Consider: declare type T; type Inner (D : access T) is limited null record; type T is limited record F : Inner (Outer'Access); end record; -- requires initialization in place type Unconstrained is array (Positive range <>) of T; function Some_Function return T is ... ; function F1 return Unconstrained is begin return (1 .. 10 => Some_Function); end F1; function F2 return Unconstrained is begin return F1; exception when Constraint_Error => return (1 .. 3 => <>); end F2; X : Unconstrained (1..3) := F2; begin null; end; In the execution of this example, it should be ok for the return statement in F1 to raise Constraint_Error before executing any calls to Some_Function. One might argue that this freedom is already implicit in the current wording, but this does not seem to be obvious. -------- It seems that it would not make sense to propose RM wording for these issues until we reach consensus on which, if any, of these issues require any action at all. One could argue that no action is required for any them: #1 is essentially a complaint that the language requires an implementation to do something that is silly and complex, but not something that is undefined or impossible. #2 is only a concern for implementations which choose to implement initialization in place for functions with unconstrained definite non-limited discriminated result subtypes. The language only requires initialization in place in the limited case. If an implementation doesn't want to deal with the complications of implementing the given example, then maybe it shouldn't choose to implement initialization in place for this case. #3 could be viewed as requiring only a clarification of the current wording. ---- It has been argued that these permissions are already implied by 11.6 and therefore do not need to be repeated. This is simply not the case. For example, the permissions given in this section allow a constraint check to be moved into an independent subprogram. 11.6 alone would not allow this. This is necessary to support allocation of function result objects by compiler-generated callback routines, where the caller of a function passes in a reference to a storage allocation routine which is invoked when the constraints of the function result are known. If a constraint check performed by the callback routine fails, this appears to the user as though Constraint_Error were raised at the point where the callback routine was called (i.e., within the function). If the function in question is an independent subprogram, this means that the constraint check has been moved into an independent subprogram. Even if the permissions of this section were redundant, they should still be stated explicitly here for the sake of the clarity of the language definition. Reliance on these permissions is effectively required in the cases where in-place initialization is required. Section 11.6 is about permissions which enable optional optimizations - an implementer should have the option of completely ignoring 11.6. Permissions which are needed in order to correctly implement the language should be stated outside of 11.6 . ---- The original wording of 6.5(24/2) applied in the case of a scalar result type. This seems wrong. Do we really want this paragraph to apply in the case of a function whose result subtype is Standard.Float? No. That is why the revised wording includes the phrase "of a composite type". This permission refers to dynamic semantics, so there are no issues with privacy. ---- The mention of System.Storage_Pools.Deallocate is needed to allow implementations to avoid storage leaks. Consider the following example: declare type T is limited record ... ; end record; -- requires in-place initialization type Unconstrained is array (Positive range <>) of T; function F1 return Unconstrained is Result_Length : Natural := 10; begin for Really_Return in Boolean loop Result_Length := Result_Length * 2; return Result : Unconstrained (1 .. Result_Length) do ...; if not Really_Return then goto L; end return; <> null; end loop; end F1; type Unconstrained_Ref is access Unconstrained; for Unconstrained_Ref'Storage_Pool use ... ; Ptr : Unconstrained_Ref := new Unconstrained'(F1); begin ...; end; The execution of this example may involve two calls to System.Storage_Pools.Allocate. We do not want to require that implementations clean up after the first call by calling Deallocate, but we do want to give implementations that option. !ACATS test !appendix From: Stephen W. Baird Sent: Friday, April 20, 2007 2:44 PM !question Should the implementation permissions of 6.5(24/2) be extended? !discussion The implementation permissions of 6.5(24/2) do not go far enough. This section states: If the result subtype of a function is unconstrained, and a call on the function is used to provide the initial value of an object with a constrained nominal subtype, Constraint_Error may be raised at the point of the call (after abandoning the execution of the function body) if, while elaborating the return_subtype_indication or evaluating the expression of a return statement that applies to the function body, it is determined that the value of the result will violate the constraint of the subtype of this object. This is a fine start, but more is needed in order to efficiently implement initialization in place as required by AI95-00318-02 . -------- Issue #1: If an object is being initialized by a function call and the implementation can tell before the call that the object subtype and the function result subtype are incompatible, then the implementation should be permitted to raise an exception before the call. Consider: declare type T; type Inner (D : access T) is limited null record; type T is limited record F : Inner (Outer'Access); end record; -- requires initialization in place type Unconstrained is array (Positive range <>) of T; subtype S1 is Unconstrained (1 .. Ident_Int (10)); subtype S2 is Unconstrained (1 .. Ident_Int (11)); function F return S1 is begin Text_Io.Put_Line ("F was called"); return (others => <>); end F; X : S2 := F; begin null; end; An implementation should be allowed to raise Constraint_Error before the call to F (without invoking 11.6). In the case where the result subtype of a function is a constrained definite structure, an implemention ought to be able to have a caller simply pass in a buffer address; nothing more complicated than that should be needed. In order to implement initialization in place, F must be passed the address of X. An implementation must verify before the call that S1 and S2 have compatible constraints - it would not do to pass in, for example, the address of a small object to a function which is going to fill in a large result. The question is what to do if that test fails. The simple thing would be to raise Constraint_Error at that point. The annoyingly complicated thing that the language currently seems to require is to allocate a buffer of the size the function expects, pass that buffer address into the function, finalize the returned value, and then raise Constraint_Error (or something like that). One could go further and handle cases involving qualified expressions such as Y : S1 := S2'(F); , but this doesn't seem necessary. -------- Issue #2: Initialization in place is only required for limited types, so it would be ok for an implementation to ignore the implementation permissions of 6.5(24/2) for a nonlimited type. If an implementation chooses not to do this, however, there can be problems with extended return statements which attempt to modify discriminants of the function result object after it has been initialized. Consider: subtype Index is Integer range 1 .. 20; type Rec (Length : Index := 10) is record F : String (1 .. Length); end record; function F return Rec is begin return Result : Rec do Result := (5, "12345"); Text_Io.Put_Line ("Assignment succeeded"); end return; end F; X1 : Rec (10) := F; For an in-out or out mode parameter with an unconstrained nominal subtype, the formal inherits the constraints (if any) of the actual. The same model should apply, at the discretion of the implementation, to return objects in the case where the nominal subtype of the return object is unconstrained and definite and "a call on the function is used to provide the initial value of an object with a constrained nominal subtype". This means that for the preceding example, execution of F could (at the discretion of the implementation) be abandoned (and Constraint_Error raised at the call site) at the point of the assignment statement. If the literal "10" in the declaration of X1 were replaced with some other value, then the default initialization of Result could raise Constraint_Error. There is also a similar issue in the case of a function call which is used to initialize an allocator when the designated subtype of the access type is unconstrained but allocated objects themselves are constrained. ------- Issue #3: The implementation permissions of 6.5(24/2) should apply recursively in the case where a function call returns the result of another function call. Consider: declare type T; type Inner (D : access T) is limited null record; type T is limited record F : Inner (Outer'Access); end record; -- requires initialization in place type Unconstrained is array (Positive range <>) of T; function Some_Function return T is ... ; function F1 return Unconstrained is begin return (1 .. 10 => Some_Function); end F1; function F2 return Unconstrained is begin return F1; exception when Constraint_Error => return (1 .. 3 => <>); end F2; X : Unconstrained (1..3) := F2; begin null; end; In the execution of this example, it should be ok for the return statement in F1 to raise Constraint_Error before executing any calls to Some_Function. One might argue that this freedom is already implicit in the current wording, but this does not seem to be obvious. -------- It seems that it would not make sense to propose RM wording for these issues until we reach consensus on which, if any, of these issues require any action at all. One could argue that no action is required for any them: #1 is essentially a complaint that the language requires an implementation to do something that is silly and complex, but not something that is undefined or impossible. #2 is only a concern for implementations which choose to implement initialization in place for functions with unconstrained definite non-limited discriminated result subtypes. The language only requires initialization in place in the limited case. If an implementation doesn't want to deal with the complications of implementing the given example, then maybe it shouldn't choose to implement initialization in place for this case. #3 could be viewed as requiring only a clarification of the current wording. **************************************************************** From: Tucker Taft Sent: Friday, April 20, 2007 4:05 PM ... > In the case where the result subtype of a function is a constrained definite > structure, an implemention ought to be able to have a caller > simply pass in a buffer address; nothing more complicated than that should > be needed. > > In order to implement initialization in place, F must be passed the address of > X. An implementation must verify before the call that S1 and S2 have compatible > constraints - it would not do to pass in, for example, the address of a small > object to a function which is going to fill in a large result. I had presumed that for cases like this, one would use an implementation based on the existing "OUT" parameter model, where there is a "constrained" bit and an object whose discriminants are of interest if the object is constrained. The function would check the constrained bit, and if True, check to be sure the discriminants match. If they don't match, it would propagate an exception. On the other hand, for OUT parameters, there is a check before the function is called if the formal subtype is constrained, so one could argue that that is a precedent for raising the exception before the call occurs if the return subtype is constrained and so is the target object, and the subtypes don't match. > The question is what to do if that test fails. The simple thing would be to > raise Constraint_Error at that point. The annoyingly complicated thing that > the language currently seems to require is to allocate a buffer of the size > the function expects, pass that buffer address into the function, finalize the > returned value, and then raise Constraint_Error (or something like that). Where does the language require this? If the model is "initialize in place," then allocating a buffer the size the function "expects" is clearly inappropriate. I think the language presumes you never allocate "extra" space when calling these functions. You might be assigning the function call into a component of another object, so the space might already "exist" in that sense. So the function can't "expect" a space of a particular size, it must deal with what is the nominal subtype of the target object. In any case, I like your suggested generalization, but in the absence of that, functions returning constrained subtypes of limited types whose first subtype is unconstrained must deal with the possibility that the target object has the wrong constraints. Note that this applies to functions returning arrays as well. It certainly seems annoying to have to pass in a 'constrained bit when the result subtype is constrained. There is no precedent for having to do something like that for arrays. > One could go further and handle cases involving qualified expressions > such as > > Y : S1 := S2'(F); > > , but this doesn't seem necessary. I would think if we allow the optimization, then we should be fairly flexible about it. ,,, > There is also a similar issue in the case of a function call which is > used to initialize an allocator when the designated subtype of the > access type is unconstrained but allocated objects themselves are constrained. I think a nastier situation occurs when the default value of the discriminant does *not* match that of the target object, but the assignment produces a value that *does* match. For example: X1 : Rec(5) := F; This is a case where if you do the optimization, constraint_error is raised. If you don't do the optimization, then the function call succeeds and X1 is appropriately initialized. I think this means that for nonlimited types, if the result object is unconstrained, you must *not* do the optimization on the off chance that the final value of the discriminants at the point of the actual return to the caller *do* match the target's nominal subtype. > ------- > > Issue #3: > > The implementation permissions of 6.5(24/2) should apply recursively in > the case where a function call returns the result of another function call. I always presumed that they did. Clarification should be provided if the words imply otherwise. > ... > It seems that it would not make sense to propose RM wording for these issues > until we reach consensus on which, if any, of these issues require > any action at all. > > One could argue that no action is required for any them: > #1 is essentially a complaint that the language requires an > implementation to do something that is silly and complex, but > not something that is undefined or impossible. It is pretty bad for functions returning arrays. I think your generalization should be allowed. > #2 is only a concern for implementations which choose to implement > initialization in place for functions with unconstrained definite > non-limited discriminated result subtypes. The language only > requires initialization in place in the limited case. > If an implementation doesn't want to deal with the complications of > implementing the given example, then maybe it shouldn't choose to > implement initialization in place for this case. I think we need to *disallow* the optimization for nonlimited types when the return object is unconstrained. It seems very bad if the optimization results in a Constraint_Error that wouldn't occur if you implemented the "canonical" semantics. > #3 could be viewed as requiring only a clarification of the current > wording. I hope it is just a clarification, but if we are going to have to reword this section a bit anyway, we might as well be explicit about nested calls. **************************************************************** From: Randy Brukardt Sent: Friday, April 20, 2007 5:08 PM > I think a nastier situation occurs when the default > value of the discriminant does *not* match that of the > target object, but the assignment produces a value > that *does* match. For example: > > X1 : Rec(5) := F; > > This is a case where if you do the optimization, > constraint_error is raised. If you don't do the > optimization, then the function call succeeds > and X1 is appropriately initialized. I think this > means that for nonlimited types, if the result > object is unconstrained, you must *not* do the > optimization on the off chance that the final > value of the discriminants at the point of the > actual return to the caller *do* match the target's > nominal subtype. I don't get it (although I didn't read this very carefully). If you have a constrained object, you have to pass in those constraints with the object. (That is, initialization of constraints and of the rest of the object might be done separately, and in separate places.) Indeed, this example is pretty much the *only* case where I'd even consider doing the optimization. (I'd never consider it for unconstrained types, nor for assignments; it's just too messy in those cases.) If it can't be done here, then it is worthless for non-limited types (at least for those with any sort of discriminants or bounds), and then why are we talking about it at all? **************************************************************** From: Tucker Taft Sent: Friday, April 20, 2007 5:35 PM I would disallow the optimization if the object declared in the extended return statement is *unconstrained* and nonlimited. This means that its discriminant values might change before the extended return statement finishes. I would allow it if the return object is constrained by its initial value, or if it is nominally constrained. **************************************************************** From: Stephen W. Baird Sent: Friday, April 20, 2007 6:05 PM > > The question is what to do if that test fails. The simple thing would > > be to raise Constraint_Error at that point. The annoyingly complicated > > thing that the language currently seems to require is to allocate a > > buffer of the size the function expects, pass that buffer address into > > the function, finalize the returned value, and then raise Constraint_Error > > (or something like that). > > Where does the language require this? You're right, it really doesn't. I expressed my opinion that "In the case where the result subtype of a function is a constrained definite structure, an implemention ought to be able to have a caller simply pass in a buffer address; nothing more complicated than that should be needed". I think it follows from this premise that the language effectively imposes this requirement because there really isn't any other reasonable alternative available (since we are not currently allowed to raise the exception before the call). If you don't accept the premise, then I agree that this behavior is not required. > In any case, I like your suggested generalization, ... ... which would allow the simple implementation I described. We agree on the important point. > > Issue #2 > > I think a nastier situation occurs when the default > value of the discriminant does *not* match that of the > target object, but the assignment produces a value > that *does* match. For example: > > X1 : Rec(5) := F; > > This is a case where if you do the optimization, > constraint_error is raised. If you don't do the > optimization, then the function call succeeds > and X1 is appropriately initialized. I think this > means that for nonlimited types, if the result > object is unconstrained, you must *not* do the > optimization on the off chance that the final > value of the discriminants at the point of the > actual return to the caller *do* match the target's > nominal subtype. Good point. I agree with your recommendations regarding all three issues. **************************************************************** From: Randy Brukardt Sent: Tuesday, October 30, 2007 7:10 PM [Commenting on version /02 of the AI.] I don't see much problem with allowing this permission for scalar types (although I doubt it would be used much). The issue with Deallocate is just the tip of the iceberg; any object that needs finalization has similar issues if it gets constructed far enough to "exist", as there is no permission to do the finalization early. That's touched on in another AI (AI05-0066-1), although I suspect that we'll end up with a solution that doesn't fix it. I think any object that is "inaccessible" should be able to be finalized at the leaving of any master (so long as it doesn't contain any tasks that haven't terminated -- waiting for tasks at random points would be a no-no). The reason for restricting to the leaving of any master prevents Finalize from being called in the middle of an operation (it shouldn't be necessary to protect it against multitasking issues in an otherwise single-tasking environment). A similar permission for Deallocate is also needed. I don't see any reason to clutter this one spot with something that needs to cover the entire language. ****************************************************************