AI22-0040-1

[a][b]

 

!standard 7.6.1(3)                                    22-10-18  AI22-0040-1/04

!standard 4.4(9.7)

!standard 6.6.1(22.12)

!standard 7.6(18)

!standard 7.6.1(13.1)

!class binding interpretation 22-03-22

!status Corrigendum 1-2022  22-09-09

!status ARG Approved  10-0-0  22-09-09

!status work item 22-03-04

!status received 22-03-01

!priority Medium

!difficulty Medium

!qualifier Omission

!subject Finalization and Implicit Loops

!summary

We recommend the addition of a suitably-defined "iterative context" to the set of constructs that can be master constructs, to prevent the “piling up” of temporaries during constructs such as a quantified_expression or array aggregate.  We also recommend a permission to allow immediate finalization of function-result temporaries that are copied into a newly-created object, rather than being built in place.

!issue

We wonder how to properly handle cases where there is no master, but there is an implicit loop which might be producing lots of temporaries that need finalization. Here is the classic case -- a quantified expression where the expression creates one or more controlled temporaries upon each iteration:

 All_Positive : constant Boolean := (for all E of C => F (E).B > 0);

 

Here we presume that F(E) returns a controlled object, with a component whose value we want to check.  As the language is currently defined, the declaration is a master, and the quantified expression as a whole is a master, but the individual expressions inside the quantified expression are not, so officially these controlled temporaries should "pile up" and all be finalized when the quantified expression is finally complete.

Requiring finalization to be deferred for so long is clearly bad news from a memory point of view, and adds complexity to the compiler.  

Should we allow the compiler to reclaim temporaries on each iteration within such an "iterative context"? (Yes)

!recommendation

We recommend the addition of a suitably-defined "iterative context" to the set of constructs that can be master constructs. There is already a notion of "repeatedly evaluated" expressions, as part of defining "known on entry" in 6.1.1, Preconditions and Postconditions, paragraphs 22.12/5 to 22.15/5:

The following subexpressions are repeatedly evaluated:

This seems to be missing the case of the key_expression and the expression of an iterated_element_association, but otherwise matches pretty much what we want for an iterative context.

So... It seems better to introduce a term for this sort of thing defined in one place (probably 4.4), and then use it to define "repeatedly evaluated" as well as the new kind of master.

By allowing expressions in iterative contexts to be master constructs, then there is no need for temporaries to "pile up."

We also recognized the need for one additional permission.  Currently in 3.10.2(10.1/3-10.2/5) we define the "master of the function call" for a function whose (composite) result is used to initialize an object:

The accessibility level of the result of a function call is that of the master of the function call, which is determined by the point of call as follows:

If the result type at the point of the function (or access-to-function type) declaration is a composite type, and the result is used (in its entirety) to directly initialize part of an object, the master is that of the object being initialized. In the case where the initialized object is a coextension (see below) that becomes a coextension of another object, the master is that of the eventual object to which the coextension will be transferred.

This ensures that any pointers inside the function result have an accessibility level that allows them to be safely included within the target object.  Later in the RM (7.6(17.1/3)) we permit such results to be "built in place" (b-i-p), and in fact require b-i-p if the result is of an immutably limited type.

B-i-p is good, because there are no function-result temporaries to be worried about.  However, in cases where the master of the function call is somewhat long-lived, but the result object is *not* built in place, we now have a temporary result object that officially won't be finalized until, potentially, much later.  But holding onto that temporary is likely to be a pain.  Better would be to allow it to be finalized sooner.  There is already special wording that applies to this case, in 3.10.2(14.4/3) which says that if such a temporary object has a coextension, it is transferred to the target object, and is no longer "owned" by the temporary (anonymous) result object:

... If the allocated object is a coextension of an anonymous object representing the result of an aggregate or function call that is used (in its entirety) to directly initialize a part of an object, after the result is assigned, the coextension becomes a coextension of the object being initialized and is no longer considered a coextension of the anonymous object. ...

So we are all set up to finalize this anonymous object without worrying about its coextensions, etc.  But alas, we are not "officially" allowed to, because when the object was created, it was associated with the master of the function call, and so isn't expected to be finalized before that master is complete.  But as above, that is undesirable from both a performance and compiler complexity point of view.  Hence, we recommend an implementation permission, probably in front of 7.6(18/3), as the first implementation permission, immediately after the discussion of "built in place", to permit immediate finalization of such objects.

!wording

Add after 4.4(9.7/5), which defines "operative constituent":

An /iterative context/ is a context for an expression that might be evaluated multiple times, and includes the following:

Replace 6.1.1(22.12/5-22.15/5) (shown above) with the simpler rule:

Any subexpression of an expression that appears in an iterative context (see 4.4) is repeatedly evaluated.

Add the following permission before 7.6(18/3):

If the result of a function call is used (in its entirety) to directly initialize an object, but the result is not built in place, the anonymous object representing the result of the function call may be finalized and become nonexistent immediately after the initialization of the newly-created object is complete.

Modify 7.6.1(3/5) (which defines the constructs that are master constructs):

...; [or ]an expression, function_call, or range that is not part of an enclosing expression, function_call, range, or simple_statement other than a simple_return_statement{; or an expression or <> that appears in an iterative context (see 4.4)}.

Modify 7.6.1(13.1/3) and add an AARM note:

In the case of an expression {or <>} that is a master {construct}, finalization of any (anonymous) objects occurs after completing evaluation of the expression {or <>} and all use of the objects, prior to starting the execution of any subsequent construct {or iteration}.

{AARM Discussion: Within a reduction expression, this includes any such anonymous objects associated with a call on the reducer subprogram for each value of the value_sequence.}

!discussion

“Piling up” of temporaries would require a complex implementation, as in many cases the number of iterations is not known at compile time. Heap allocation might even be required, especially in cases where the number of iterations cannot be predicted (as in container iterators and iterators with filters).

It should be noted that this issue occurs even in Ada 95 constructs, such as array aggregates:

    return (1 .. Computed_Length => Func.C);

 

where Func returns a controlled object.

It also should be noted that the proposed solution is (slightly) incompatible, as a construct like:

   return Some_Array'(others => Cont_Func(2));

 

where Cont_Func is a function returning a container, would become illegal as the temporary function result would not live as long as the aggregate, which is required for the (implicit) call of Constant_Reference in the container indexing.

This incompatibility is probably a good thing, as the alternatives are keeping an unknown number of function result temporaries around (likely requiring heap allocation), or letting the container go away while someone might still be holding onto (and perhaps referencing) a pointer into it. The first is an obvious performance and implementation problem, and the latter is allowing erroneous execution for an innocuous construct.

Some of these cases can be eliminated by eliminating the temporaries using the various build-in-place permissions. But we don’t want the language to effectively require the use of those permissions.

!example

Here is the example given in the !issue section:

 All_Positive : constant Boolean := (for all E of C => F (E).B > 0);

 

With this change, the expression F(E).B > 0 appears in an iterative context, and as such can act as a master for any temporaries not needed by subsequent iterations.

In addition, in a case where such an expression is being used to initialize a newly created object, such as in:

 Aggr : constant Array_Of_Controlled := [for I in 1 .. 100 => F(I)];

 

where F(I) returns a controlled object which the compiler does not build in place, after the object returned by F is copied into the aggregate (and adjusted), the temporary can be immediately finalized (and cease to exist), thanks to the added permission.

The need for this extra permission does not only come up in iterative contexts.  It can also be as simple as:

 X : constant Has_Controlled_Part := F(Y);

 

presuming that the compiler does not build the result of F(Y) directly in X.  The master of the call on F is determined by the master of X, which might be quite long-lived.  We would clearly want to reclaim the temporary result object of the call on F(Y) sooner than that.  This permission enables that.

!ACATS test

Controlled types that have side effects on appropriate counters, etc. during finalization can be used to detect when finalization occurs.  In the first example above, finalization should occur before the subsequent iteration.  In the second and third examples, finalization is allowed to occur before the Aggr object as a whole, or the X object, is finalized.

!appendix

From: Tucker Taft

Sent: Tuesday, March 1, 2022  7:53 PM

Steve Baird, Tuck, and Randy have been having a long discussion about how to properly handle cases where there is no master, but there is an implicit loop which might be producing lots of temporaries that need finalization.  Here is the classic case -- a quantified expression where the expression creates one or more controlled temporaries upon each iteration:

    All_Positive : constant Boolean := (for all E of C => F (E).B > 0);

Here we presume that F(E) returns a controlled object, with a component whose value we want to check.  As the language is currently defined, the declaration is a master, and the qualified expression as a whole is a master, but the individual expressions inside the qualified expression are not, so officially these controlled temporaries should "pile up" and all be finalized when the qualified expression is finally complete.

Requiring finalization to be deferred for so long is clearly bad news from a memory point of view, and adds complexity to the compiler.   We would like to allow the compiler to reclaim temporaries on each iteration within such an "iterative context."  So this leads us to propose the addition of a suitably-defined "iterative context" to the set of constructs that can be master constructs.

While researching how to do this, we noticed there is already a notion of "repeatedly evaluated" expressions, as part of defining "known on entry" in 6.1.1, Preconditions and Postconditions, paragraphs 22.12/5 to 22.15/5:

22.12/5

   {AI12-0280-2} The following subexpressions are repeatedly evaluated:

22.13/5

{AI12-0280-2} A subexpression of a predicate of a quantified_expression;

22.14/5

{AI12-0280-2} A subexpression of the expression of an array_component_association;

22.15/5

{AI12-0280-2} A subexpression of the expression of a container_element_association.

This seems to be missing the case of the key_expression and the expression of an iterated_element_association, but otherwise matches pretty much what we want for an iterative context.

So... It seems better to introduce a term for this sort of thing defined in one place (probably 4.4), and then use it to define "repeatedly evaluated" as well as the new kind of master.

Hence, in 4.4, I would suggest we add, probably after 4.4(9.7/5) which defines "operative constituent":

An /iterative context/ is a context for an expression that might be evaluated multiple times, and includes the following:

Then replace 6.1.1(22.12/5-22.15/5) (shown above) with the simpler rule:

Any subexpression of an expression that appears in an iterative context (see 4.4) is /repeatedly evaluated/.

and then modify 7.6.1(3/5) which defines the constructs that are master constructs:

...; [or ]an expression, function_call, or range that is not part of an enclosing expression, function_call, range, or simple_statement other than a simple_return_statement{; or an expression or <> that appears in an iterative context (see 4.4)}.

With the above changes, the expressions in iterative contexts can now be master constructs, and so there is no need for temporaries to "pile up."

We also recognized the need for one additional permission.  Currently in 3.10.2(10.1/3-10.2/5) we define the "master of the function call" for a function whose (composite) result is used to initialize an object:

10.1/3

{AI05-0234-1} The accessibility level of the result of a function call is that of the master of the function call, which is determined by the point of call as follows:

10.2/5

{AI05-0234-1} {AI12-0402-1} If the result type at the point of the function (or access-to-function type) declaration is a composite type, and the result is used (in its entirety) to directly initialize part of an object, the master is that of the object being initialized. In the case where the initialized object is a coextension (see below) that becomes a coextension of another object, the master is that of the eventual object to which the coextension will be transferred.

This ensures that any pointers inside the function result have an accessibility level that allows them to be safely included within the target object.  Later in the RM (7.6(17.1/3)) we permit such results to be "built in place" (b-i-p), and in fact require b-i-p if the result is of an immutably limited type.

B-i-p is good, because there are no function-result temporaries to be worried about.  However, in cases where the master of the function call is somewhat long-lived, but the result object is *not* built in place, we now have a temporary result object that officially won't be finalized until, potentially, much later.  But holding onto that temporary is likely to be a pain.  Better would be to allow it to be finalized sooner.  There is already special wording that applies to this case, in 3.10.2(14.4/3) which says that if such a temporary object has a coextension, it is transferred to the target object, and is no longer "owned" by the temporary (anonymous) result object:

... If the allocated object is a coextension of an anonymous object representing the result of an aggregate or function call that is used (in its entirety) to directly initialize a part of an object, after the result is assigned, the coextension becomes a coextension of the object being initialized and is no longer considered a coextension of the anonymous object. ...

So we are all set up to finalize this anonymous object without worrying about its coextensions, etc.  But alas, we are not "officially" allowed to, because when the object was created, it was associated with the master of the function call, and so isn't expected to be finalized before that master is complete.  But as above, that is undesirable from both a performance and compiler complexity

point of view.  Hence, we recommend an implementation permission, probably in front of 7.6(18/3), as the first implementation permission, immediately after the discussion of "built in place":

If the result of a function call is used to initialize an object, but the result is not built in place, the anonymous object representing the result of the function call may be finalized immediately and become nonexistent after the initialization of the newly-created object is complete.

Clearly we need an AI to make this more official, but there has been a lot of discussion about finalization happening in other contexts as part of implementation efforts, and we wanted to get this proposal out so it can be included in these implementation discussions.

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

From: Bob Duff

Sent: Wednesday, March 2, 2022 6:35 AM

> Steve Baird, Tuck, and Randy have been having a long discussion about how to properly handle cases where there is no master, but there is an implicit loop which might be producing lots of temporaries that need finalization.  Here is the classic case -- a quantified expression where the expression creates one or more controlled temporaries upon each iteration:

>

>         All_Positive : constant Boolean := (for all E of C => F (E).B >

> 0);

>

> Here we presume that F(E) returns a controlled object, with a component whose value we want to check.  As the language is currently defined, the declaration is a master, and the qualified expression as a whole is a master, but the individual expressions inside the qualified expression are not, so officially these controlled temporaries should "pile up" and all be finalized when the qualified expression is finally complete.

>

> Requiring finalization to be deferred for so long is clearly bad news from a memory point of view, and adds complexity to the compiler.   We would like to allow the compiler to reclaim temporaries on each iteration within such an "iterative context."  So this leads us to propose the addition of a suitably-defined "iterative context" to the set of constructs that can be master constructs.

It might be worth pointing out that this "piling up" would pretty much require a heap-based data structure, because we don't always know the number of items ahead of time.  (That's why I like the "for all" example instead of an aggregate example.)

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

From: Tucker Taft

Sent: Wednesday, March 2, 2022  8:19 AM

Thanks, Bob, for the review.  I realized the implementation permission reads oddly as written due to the (mis)placement of "immediately", and should have been:

If the result of a function call is used to initialize an object, but the result is not built in place, the anonymous object representing the result of the function call may be finalized and become nonexistent immediately after the initialization of the newly-created object is complete.

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

From: Tucker Taft

Sent: Wednesday, March 2, 2022  3:05 PM

Steve reminds me that he would like to see an AARM note to clarify what can be finalized when an expression master is finalized, particularly one in an iterative context.

Here is a slight adjustment to 7.6.1(13.1/3) plus the addition of an AARM note:

In the case of an expression {or <>}  that is a master, finalization of any (anonymous) objects occurs after completing evaluation of the expression {or <>} and all use of the objects, prior to starting the execution of any subsequent construct {or iteration}.

{AARM Discussion: Within a reduction expression, this includes any such anonymous objects associated with a call on the reducer subprogram for each value of the value_sequence.}

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

From: Randy Brukardt

Sent: Thursday, March 3, 2022  1:13 AM

> > Steve Baird, Tuck, and Randy have been having a long

> discussion about how to properly handle cases where there is no

> master, but there is an implicit loop which might be producing lots of

> temporaries that need finalization.  Here is the classic case -- a

> quantified expression where the expression creates one or more

> controlled temporaries upon each iteration:

> >

> >         All_Positive : constant Boolean := (for all E of C => F (E).B >

> > 0);

> >

> > Here we presume that F(E) returns a controlled object, with

> a component whose value we want to check.

It probably should be noted that this occurs even in Ada 95 constructs, such

as array aggregates:

        return (1 .. Computed_Length => Func.C);

where Func returns a controlled object.

It also should be noted that the proposed solution is (slightly)

incompatible, as a construct like:

   return Some_Array'(others => Cont_Func(2));

where Cont_Func is a function returning a container, would become illegal as

the temporary function result would not live as long as the aggregate, which

is required for the (implicit) call of Constant_Reference in the container

indexing.

This incompatibility is a probably good thing, as the alternatives are

keeping an unknown number of function result temporaries around, or letting

the container would go away while someone might still be holding onto (and

perhaps referencing) a pointer into it. The first is an obvious performance

and implementation problem, and the latter is allowing erroneous execution

for an inoccuous construct.

> It might be worth pointing out that this "piling up" would

> pretty much require a heap-based data structure, because we

> don't always know the number of items ahead of time.  (That's

> why I like the "for all" example instead of an aggregate example.)

As noted above, one doesn't always know the number of items in an aggregate,

either, especially when it is an unconstrained array type with dynamically

determined bounds. That's true even in Ada 95.

Of course, if the compiler eliminates the temporaries using the various

build-in-place permissions, this isn't a problem in Ada 95 cases, but the

language shouldn't be effectively requiring the use of those permissions.

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

From: Tucker Taft

Sent: Thursday, March 3, 2022  1:05 PM

This has been turned into a draft AI using the "new" ARG website and its "active AIs" folder:

   AI22-0040-1 Finalization and Implicit Loops

Please use Google docs to record any specific comments or suggestions.

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

From: Bob Duff

Sent: Friday, March 4, 2022  7:25 AM

For:

        P(F(G(X));

is your permission intended to allow the formal of F (initialized from the result of G) to be finalized after F returns, but before P is called.  It should, I think, for uniformity with the loop-y expression cases.

And if so, then why do we need to mention the loop-y expressions?

Randy wrote:

> It probably should be noted that this occurs even in Ada 95

> constructs, such as array aggregates:

>

>         return (1 .. Computed_Length => Func.C);

>

> where Func returns a controlled object.

Yes, good point.

> It also should be noted that the proposed solution is (slightly)

> incompatible, as a construct like:

>

>        return Some_Array'(others => Cont_Func(2));

I wonder if it's actually incompatible with any real compiler.

> > It might be worth pointing out that this "piling up" would pretty

> > much require a heap-based data structure, because we don't always

> > know the number of items ahead of time.  (That's why I like the "for

> > all" example instead of an aggregate example.)

>

> As noted above, one doesn't always know the number of items in an

> aggregate, either, especially when it is an unconstrained array type

> with dynamically determined bounds. That's true even in Ada 95.

By "head of time" I do not mean "at compile time".  I think for the aggregate cases, one could compute the number of finalizable temps first, then allocate an array of them, then compute the component.  The index values can also have finalizable temps, but we know how many.

That doesn't require heap, but it's still a huge implementation headache.

> Of course, if the compiler eliminates the temporaries using the

> various build-in-place permissions, this isn't a problem in Ada 95

> cases, but the language shouldn't be effectively requiring the use of those permissions.

Agreed.

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

From: Tucker Taft

Sent: Friday, March 4, 2022  9:29 AM

For:

    P(F(G(X));

is your permission intended to allow the formal of F (initialized from

the result of G) to be finalized after F returns, but before P is

called.  It should, I think, for uniformity with the loop-y expression

cases.

And if so, then why do we need to mention the loop-y expressions?

I am unsure what you are talking about here.  The permission in 7.6 in the draft AI says:

If the result of a function call is used to initialize an object, but the result is

not built in place, the anonymous object representing the result of the function

call may be finalized and become nonexistent immediately after the

initialization of the newly-created object is complete. 

I don't see how that applies to your P(F(G(X)) example.  If possible, please follow up by adding comments directly into the google-docs version of the AI, rather than using this e-mail thread.  We are trying to "dog food" the google-doc-based process, and these e-mail threads can quickly get unwieldy.  

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

From: Tucker Taft

Sent: Friday, March 4, 2022  12:58 PM

> I am unsure what you are talking about here.

Now I understand what you meant -- in the example, I talked about an array aggregate to illustrate the problem, but a much simpler example would also show the problem.  I have added one to the AI.

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

From: Randy Brukardt

Sent: Saturday, March 5, 2022  1:15 AM

>For:

>

>   P(F(G(X));

>

>is your permission intended to allow the formal of F (initialized from

>the result of G) to be finalized after F returns, but before P is

>called.  It should, I think, for uniformity with the loop-y expression

>cases.

>

>And if so, then why do we need to mention the loop-y expressions?

 

My understanding was that the permission only applied to calls that could be build-in-place, since those are the only ones that have a possibly extended lifetime (such that the object might exceed the lifetime of the master of the expression).

 

There's no build-in-place context in the above, so I don't see how it could or should apply in such cases. If you had used an allocator rather than a procedure, then it could have applied. For instance:

 

         P := new Some_Type'(F(G(X));

 

I'd be against allowing willy-nilly finalization of return objects; I only agreed to the permission because it's fairly clearly that iterative contexts have an implementation problem which needs some sort of solution. I don't want to see such permissions in contexts that don't have an implementation need (it would make the implementation more complex to try to clean up G prematurely) -- it just makes portability harder (and a greater likelihood of compiler errors introducing erroneous execution).

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

From: Randy Brukardt

Sent: Saturday, March 5, 2022  1:34 AM

> > It also should be noted that the proposed solution is (slightly)

> > incompatible, as a construct like:

> >

> >        return Some_Array'(others => Cont_Func(2));

>

> I wonder if it's actually incompatible with any real compiler.

How do you mean? The incompatibility is a direct consequence of the (static) accessibility check on the actual of an explicitly aliased parameter. There is an ACATS test that checks that implementations make that check, so it's unlikely that any implementation is failing to make the check at all.

I suppose it would be possible for an implementation to (incorrectly) reject all function calls being passed to expliiclty aliased parameters, even in contexts (like assignment) where they are legal. I would have expected someone to have complained about that as it is rather annoying to say something like the above is illegal in any context (that certainly wasn't the intent).

One hopes that no implementation is just allowing this sort of construct and then finalizing early anyway, as that would allow objects to be used after freeing.

Note that Janus/Ada does not have a problem (other than possible memory

exhaustion) with any "piling-up" currently, because it uses heap-based temporaries in these cases, and those are never cleaned up in the middle of an expression (we have no way to clean up some but not all temporaries). But I could see problems in the future when build-in-place is used more generally (and of course Janus/Ada doesn't implement any of the explicitly aliased parameter stuff).

...

> > As noted above, one doesn't always know the number of items in an

> > aggregate, either, especially when it is an unconstrained array type

> > with dynamically determined bounds. That's true even in Ada 95.

>

> By "head of time" I do not mean "at compile time".  I think for the

> aggregate cases, one could compute the number of finalizable temps

> first, then allocate an array of them, then compute the componenent.  

> The index values can also have finalizable temps, but we know how

> many.

>

> That doesn't require heap, but it's still a huge implementation

> headache.

I don't quite see how you can allocate an unknown number of objects at run-time without using heap. I suppose you could expand the stack somehow, but with the extra stack overflow checks that would be needed, the extra tracking needed to restore, etc., it really wouldn't save much over heap allocation (heap allocation isn't usually that expensive, at least if you implement your own heap).

I prefer to use build-in-place and other permissions to get rid of the temporaries, rather than trying to save a few cycles allocating them. Both things are quite complex to do (getting rid of temporaries or using stack allocation more), so I'd rather spend my efforts on the one that permanently eliminates the issues. YMMV.

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

From: Bob Duff

Sent: Tuesday, March 22, 2022  11:59 AM

[Editor’s note: Forwarded from Google Docs to ARG list at 8:10 PM.]

If we have something like:

X := (for all Y of blah => F(...) = G(...));

If F'Result is finalizable we want to finalize it every time around the loop.

Why don't we simply add an Implementation Permission that allows an implementation to finalize an object if it will never again be touched (for all possible subsequent inputs)?

For these loop-y expressions, the compiler can easily prove that the temp created on each iteration is never touched again after that iteration, so it can finalize on each iteration. We don't need to add any verbiage to the RM regarding loop-y expressions ("iterative

contexts") for this issue.

For a similar case:

P(F(...) = G(...));

I'm not sure what an implementation should do. Finalize F'Result and G'Result after "=" returns, and before calling P? That might be simplest, given that it's uniform with the above "loop-y"

implementation, and it seems most natural to me. Finalize F'Result and G'Result after P returns? That might be simpler because it reduces the number of places needing finalization actions (which was the original ARG reasoning, shown to be incorrect in the loop-y expression cases).

I think the implementation should be allowed, but not required, to finalize those function results before calling P.

----------------

I think if A takes an aliased formal parameter that requires finalization and returns something with an access discriminant, then A(B(...)) can return an obj whose discrim points at B or some subcomponent thereof. That seems like a very strange thing to do, and should be illegal to make my above suggestion work. Surely the result of B should not be allowed to "escape"

outside of A. That is, the result of B should not exist after A returns.

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

From: Randy Brukardt

Sent: Tuesday, March 22, 2022  8:41 PM

> If we have something like:

>

> X := (for all Y of blah => F(...) = G(...));

>

> If F'Result is finalizable we want to finalize it every time around

> the loop.

The proposed rules have that effect. The weird special case only applies when a function is used as a whole to initialize something, and that doesn't happen for F or G. So the newly introduced master applies to the result of F and G, so they get finalized on each iteration.

> Why don't we simply add an Implementation Permission that allows an

> implementation to finalize an object if it will never again be touched

> (for all possible subsequent inputs)?

That's the "let Ada have garbage collection" permission, which has been proposed repeatedly for the last 20 years (at least). The problem is that such a permission is not simple at all to define, especially if you don't want to introduce problems into existing Ada code.

It should be obvious that "never again be touched" is not a technical description, so something formal would be needed for that. And that would have to be careful about the extended lifetime of some function results, build-in-place, and probably more.

We also would have to decide if we would allow a different thread to finalize an object (as in a garbage collection), or have some limitation.

Similarly for the locations where a finalization can happen.

Fundamentally, analysis gets harder the more places that you allow objects to be finalized. Ada's current model is deterministic -- there is only one place where a particular object is supposed to be finalized. If one adds more places to that, then it becomes ever harder to determine what actually is happening (complicating analysis for both humans and programs).

An unrestricted permission such as the one you describe above essentially would make almost all existing Ada code using controlled types incorrect, in that everything that a finalizer could touch would have to be synchronized (as one would have no idea which thread would finalize objects of the type or when).

Even if you restricted it to the same thread, you would have problems with finalizations occurring while a composite object that the finalization uses is being modified. With the possibility of build-in-place, it is quite likely that such objects could be abnormal momentarily, and one hopes that no finalizations occur while that's the case. With the current rules, that is indeed the case (unless an exception interrupts an assignment, of course), while any such guarantees would be lost with such a permission.

Ergo, if we add such a permission, nearly every object used by a finalizer would need to be protected somehow, and it is unlikely that is the case for most existing Finalize routines. (Of my code, only Claw does such protection, and it was a nightmare to work out all of the deadlocks with that.)

> For these loop-y expressions, the compiler can easily prove that the

> temp created on each iteration is never touched again after that

> iteration, so it can finalize on each iteration. We don't need to add

> any verbiage to the RM regarding loop-y expressions ("iterative

> contexts") for this issue.

Sure, but as noted above, that doesn't take into account the effect on how one should write finalizers.

> For a similar case:

>

> P(F(...) = G(...));

>

> I'm not sure what an implementation should do. Finalize F'Result and

> G'Result after "=" returns, and before calling P? That might be

> simplest, given that it's uniform with the above "loop-y"

> implementation, and it seems most natural to me. Finalize F'Result and

> G'Result after P returns? That might be simpler because it reduces the

> number of places needing finalization actions (which was the original

> ARG reasoning, shown to be incorrect in the loop-y expression cases).

The latter is what is currently required by the language.

> I think the implementation should be allowed, but not required, to

> finalize those function results before calling P.

That is precisely the sort of case that I worry about. If the call of P has some effect on the data used by the finalization of F and G, the effect of the program could be very different. I don't know how one could create useful finalizers and still prevent the possibility of such effects. (Yes, I agree those cases would be rare. Which makes them all the more dangerous, since they are not very likely to show up in testing.)

> ----------------

>

> I think if A takes an aliased formal parameter that requires

> finalization and returns something with an access discriminant, then

> A(B(...)) can return an obj whose discrim points at B or some

> subcomponent thereof. That seems like a very strange thing to do, and

> should be illegal to make my above suggestion work. Surely the result

> of B should not be allowed to "escape"

> outside of A. That is, the result of B should not exist after A

> returns.

Do you think A(I)(J) is a strange thing to do?

Well, if A is a container of containers, then the above expression expands

into:

          Reference (Reference (A, I).Element.all, J).Element.all

...where Reference is a function call and Element is an access discriminant.

My point being that this sort of "transitivity" is necessary to allow containers to be composable -- it's fundamental to the container design.

The entire reason we have aliased parameters and these access parameter "tricks" is so that we can safely return parts of a container with full compile-time checking that no one is holding onto the returned value too long. Without it, the containers would be rather unsafe as anyone could squirrel away an access to an element, even if the element was removed or the container itself ceased to exist.

It would be going in the wrong direction (rapidly!) to remove that capability.

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

From: Randy Brukardt

Sent: Friday, October 21, 2022  11:41 PM

The intent is that the added permission applies in cases where the implementation already has the freedom to use build-in-place (or not), so the permission does not add any additional non-determinism. In particular, the permission is justified by the 3.10.2(10.2/5) rule. However, it was noted that that rule only applies when the function result is used  “in its entirety”. Therefore, the permission also should only apply in such cases.

We believe that most cases where only a part of the result is used are illegal (as the lifetime of the result would be too short) or the object is already required to be immediately reclaimed. But the risk of introducing additional non-determinism in cases where that wasn’t intended is too great. So the phrase “(in its entirety)” is added to the new permission in order that it aligns with the other, similar rules.

This was considered an Editorial Review change.

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

[a]If we have something like:

X := (for all Y of blah => F(...) = G(...));

If F'Result is finalizable we want to finalize it

every time around the loop.

Why don't we simply add an Implementation Permission that allows an

implementation to finalize an object if it will never again be touched

(for all possible subsequent inputs)?

For these loop-y expressions, the compiler can easily prove that the

temp created on each iteration is never touched again after that

iteration, so it can finalize on each iteration. We don't need to add

any verbiage to the RM regarding loop-y expressions ("iterative

contexts") for this issue.

For a similar case:

P(F(...) = G(...));

I'm not sure what an implementation should do. Finalize F'Result and

G'Result after "=" returns, and before calling P? That might be

simplest, given that it's uniform with the above "loop-y"

implementation, and it seems most natural to me. Finalize F'Result and

G'Result after P returns? That might be simpler because it reduces the

number of places needing finalization actions (which was the original

ARG reasoning, shown to be incorrect in the loop-y expression cases).

I think the implementation should be allowed, but not required, to

finalize those function results before calling P.

----------------

I think if A takes an aliased formal parameter that requires

finalization and returns something with an access discriminant,

then A(B(...)) can return an obj whose discrim points at B or

some subcomponent thereof. That seems like a very strange

thing to do, and should be illegal to make my above suggestion

work. Surely the result of B should not be allowed to "escape"

outside of A. That is, the result of B should not exist after

A returns.

@taft@adacore.com @baird@adacore.com @dismukes@adacore.com @squirek@adacore.com

[b]We looked at all of those ideas in the 76 private messages before this was proposed. But a proper discussion of philosophy cannot done here; I'll answer on the ARG list.