AI22-0047-1

!standard 4.5.10(24/5)                                    22-08-08  AI22-0047-1/01

!standard 4.5.10(25/5)

!standard 4.5.10(28/5)

!class Ramification 22-08-08

!status work item 22-08-08

!status received 22-08-04

!priority Low

!difficulty Easy

!qualifier Clarification

!subject Initialization of and saving the result back in an accumulator during reduction

!summary

Clarify with an AARM note that the phrase "saving the result as the new value of the accumulator" appearing in 4.5.10(25/5) and a similar phrase "with the result saved back in the overall accumulator" in 4.5.1(28/5) have the semantics of an assignment when Reduce is a function.  Clarify that if the accumulator subtype is indefinite, then it is constrained by, and (if class-wide) takes its tag from, the Initial_Value.

!issue

The description of the dynamic semantics of Reduction Expressions talks about "saving" the value of each call on the Reduce subprogram back into the accumulator (RM 4.5.10(25/5, 28/5)).  Does this have the semantics of an assignment when the Reduce subprogram is a function? (Yes.)

Should this be clarified somehow? (Yes.)

!recommendation

Because the wording is trying to capture the semantics of both the case where Reduce is a function and Reduce is a procedure, the somewhat ambiguous term "saving" was used in the dynamic semantics.  We should clarify with an AARM note that "saving" in the function case is performed via an assignment to the accumulator of the result returned by the Reduce function.  In the procedure case, the implementor of the Reduce procedure will presumably use an assignment to update the accumulator, though it might actually be a set of assignments to the components of the accumulator, if the accumulator is a composite type or uses a level of indirection.

!wording

Add after 4.5.10(24/5):

AARM Ramification: If Accum_Type is an indefinite subtype, the accumulator is constrained by, and if class-wide, takes its tag from the Initial_Value.

Add after 4.5.10(25/5):

AARM To be honest: In the case where Reduce is a function, "saving the result as the new value of the accumulator" has the semantics of an assignment of the function result into the accumulator, including associated finalization/adjustment as appropriate.

Add after 4.5.10(28/5):

AARM To be honest: In the case where Reduce is a function, "with the result saved back in the overall accumulator" has the semantics of an assignment of the function result into the accumulator, including associated finalization/adjustment as appropriate.

!discussion

To minimize implementation burden and avoid surprises, we do not expect any special semantics for the implementation of a reduction accumulator.  It should be implemented as a normal variable of the subtype Accum_Type, initialized by its Initial_Value, and normal assignment, with any associated finalization/adustument, should be used to "save" the result of a Reduce function as the new value of the accumulator.

Of course it would be great if a compiler could warn a user who makes the mistake of using an accumulator subtype that would result in almost certain failure.  Almost certainly an unconstrained array subtype, or a discriminated subtype without defaults, would represent such a mistake.

!example

The implication of using normal "constrained-by-initial-value" and "assignment" semantics are apparent if Accum_Type is an unconstrained array subtype such as String, as this is almost certain to fail with a Constraint_Error, because the accumulator in such a case would be constrained by its initial value, which is the empty string in this case, and so any further assignment to the accumulator of the result of calling the reduction function "&" will fail unless the result is still an empty string.

The correct approach is to use a structure that can grow as the resulting concatenation grows, such as an Unbounded_String or a Vector.

procedure Foo is
   function F (N : Natural) return String is
   begin
      return String'(1 .. N => (if N mod 2 = 0 then 'X' else 'Y'));
   end F;

   S : constant String := [for I in 1 .. 5 => F (I)]'Reduce ("&", "");
begin
   pragma Assert (S'Length = 15);
end Foo;

 

The above will fail during the evaluation of 'Reduce as soon as the result of "&" is anything but an empty string.

!ACATS test

It would be appropriate to have a test of a concatenating reduction expression, using both String and Unbounded_String, where the first is certain to raise Constraint_Error, and the latter should work.

!appendix

Stephen Baird <baird@adacore.com> Thu, Aug 4, 2022 at 4:29 PM

To: Tucker Taft <taft@adacore.com>, Randy Brukardt <randy@rrsoftware.com>

I was discussing reduction expressions with Ed, Bob, and Gary and a question came up.

GNAT raises Constraint_Error executing the following example. It is not clear to me whether this is correct.

with Text_Io;
procedure Foo is
    function F (N : Natural) return String is
    begin
       Text_Io.Put_Line ("In F, N =" & N'Image);
       return String'(1 .. N =>

           (if N mod 2 = 0 then 'X' else 'Y'));
    end;

   function Cat (S1, S2 : String) return String renames "&";
   --  eliminate any overload resolution
   --  questions by introducing a rename

   S : constant String :=

        [for I in 1 .. 5 => F (I)]'Reduce (Cat, "");
begin
   Text_Io.Put_Line (S);
end Foo;

 

We declare an accumulator object initialized to the initial value and then later on we get

Constraint_Error when we try to update the accumulator with a value that has a different

length.

When RM 4.5.10(25) says

    "saving the result as the new value of the accumulator"

does this refer to something more than an assignment operation?

If this step can, for example, change the bounds of an array-valued

accumulator, then that fact seems worth an AARM note. GNAT implements this step

as an assignment. Is that a bug?

FWIW, this notion of a more general form of update reminds me of the

for Elem of Some_Container_With_An_Indefinite_Element_Subtype loop ...
end loop;

case, where the tag/bounds/discriminants of Elem might change from one iteration to the next.

What do you think?

Thanks,

-- Steve

AdaCore Mail - reduction expressions question 8/8/22, 3:10 PM

 To: Stephen Baird <baird@adacore.com>

Cc: Randy Brukardt <randy@rrsoftware.com>

This was intended to use regular assignment, so the accumulator needs to be something like an Unbounded_String if you are doing concatenations. There was no expectation that a new accumulator object was being created on each step. The memory management of that would be a real pain, and would be one of those things that give embedded- systems folks nightmares. Even if the Reducer is a procedure, the presumption is that there is a single accumulator object, initialized (and presumably taking its constraint if the accum subtype is indefinite) from the initial expression.

Perhaps an AARM note would help, or the use of the word "assign" at least for the case when Reducer is a function. When Reducer is a procedure, the assignment presumably happens inside the procedure, but the accumulator object is in either case, a single variable allocated at the point of the reduction expression, and not something that morphs on each reduction step into something else.

-Tuck

Stephen Baird <baird@adacore.com> Thu, Aug 4, 2022 at 5:42 PM

To: Tucker Taft <taft@adacore.com>

Cc: Randy Brukardt <randy@rrsoftware.com>

Thanks for the clarification.

  I do think the AARM ought to state explicitly that there is an assignment going on in the function case, since this can involve Adjust/Finalize calls and predicate checks, in addition to constraint checks.

It should also state explicitly that if the accumulator subtype is indefinite, then the accumulator object is constrained by its initial value. And perhaps we need to separately mention that the tag comes from the initial value in the class-wide case; note that the class-wide

case is not mentioned in the definition of "constrained by its initial value" in 3.3.1 (although it is mentioned in AARM 3.3.1(33.b)).

Sound reasonable? -- Steve

-- Steve

To: Stephen Baird <baird@adacore.com>

Cc: Randy Brukardt <randy@rrsoftware.com>

Mon, Aug 8, 2022 at 2:31 PM

On Thu, Aug 4, 2022 at 5:42 PM Stephen Baird <baird@adacore.com> wrote:

.... And perhaps we need to separately mention that the tag

comes from the initial value in the class-wide case; note that the class-wide

case is not mentioned in the definition of "constrained by its initial value" in 3.3.1 (although it is mentioned in AARM 3.3.1(33.b)).

This is defined "officially" in 3.9(22):

The tag of an object of a class-wide tagged type is that of its initialization expression.

So this one seems pretty unambiguous.

-Tuck

...