Project

General

Profile

Accumulators

An accumulator is a 4GL structure which collects one piece of data upon each pass through an iterative loop and calculates a particular statistic upon the collected data. For instance, an accumulator might calculate an average numeric value or determine a lexicographical maximum value from a list of strings. Its data may be derived from a mutable variable, a data source or from a complex expression which uses a combination of both variables and data source.

During conversion, FWD will identify each accumulator and convert it to a specialized instance, depending on the kind of the accumulated statistics; this instance, in some cases, will be explicitly used to trigger the accumulation or in other circumstances will be triggered automatically.

The conversion rules try to simplify the converted accumulator code to the maximum extent possible; although in some terms this simplification might indicate that writing compatible 4GL code by hand was made easy, we don't recommend using accumulators outside the 4GL source code. Also, note that there are some accumulator properties which are not fully supported at the time of this writing, which includes:

  • accumulation expressions can be used as "variables" and can be referenced directly in Progress without the ACCUMULATE statement or the ACCUM function. This poses a parse-time problem but it also has consequences that are harder to implement since such vars can be assigned!
  • For COUNT accumulators, if the expression contains a field reference and the buffer currently does not reference any records (i.e. it was not yet retrieved or it was not found), this 'not-existent' field will be counted. So, accumulation must be triggered before retrieving the record.
  • In the DISPLAY with aggregate-phrase case, the clause which sets the aggregator's label is not currently supported.
  • The short name for the AVERAGE accumulator (AVG) is not yet supported by the conversion rules.

This chapter will focus on describing how the accumulators (and their sub-accumulator versions) are converted and how the conversion rules have addressed all found behavior, discovered during extensive testing.

Accumulator Expression

The expression used by an accumulator to aggregate data can be one of the following:

  1. simple expressions: a variable or a a buffer field. For buffer field cases, it is required for the referenced buffer to be in an active scope.
  2. a constant value.
  3. complex expressions, which combine variables, constants and a field.

The accumulation will be performed in the same manner for all expression types: the expression will be evaluated and will be aggregated (to the primary result and to the sub-result, if needed). Beside providing the actual aggregated data, the accumulated expression (together with the aggregate type) is used to uniquely identify a certain accumulator. So, to implement the 4GL behavior, the conversion rules will compare each expression using its syntax as it appears in the 4GL sources: expressions as n + 1 and 1 + n will be treated as two distinct expressions, even though they are semantically equivalent.

When complex expressions are used, they need to be evaluated each time the accumulation is performed (as the variable's or field's value might be change). So, the conversion rules will emit a distinct field (at the same block as the accumulator's field) which instantiates an anonymous class. This anonymous class is an extension of some FWD utility classes which extend the com.goldencode.2j.util.Resolvable interface and will evaluate the expression each time accumulation performs. The following table shows which internal FWD class is used, depending on the expression's type (all classes are part of the com.goldencode.2j.util package):

Expression Type integer decimal character date logical
FWD Class IntegerExpression DecimalExpression CharacterExpression DateExpression LogicalExpression

Once a complex expression is used to define an accumulator, the converted name for the field which defines this expression will follow the accumExpr# pattern, where # is a counter used to index all complex expressions which define accumulators.

Aggregate Phrase

The aggregate phrase is responsible for determining the type of the accumulator. For each accumulator, when used with the ACCUMULATE statement, ACCUM function or with a DISPLAY statement, its aggregate phrase, together with the accumulated expression are responsible for identifying the accumulator across the 4GL code.

The aggregate phrase in some cases can have a short and a long form - the conversion rules will handle both of them in the same way; in this chapter, only the short form will be used when referring to a certain accumulator. Even if the aggregate phrase contains multiple aggregators, the conversion rules will emit a distinct accumulator instance for each combination of aggregator type and expression.

For all aggregators, 4GL supports sub-accumulators, which present majors differences when they are used in a DISPLAY with aggregate phrase (please see the DISPLAY Statement with Aggregate Phrase section) or their aggregate phrase has a BY break-group clause (see the ACCUMULATE Statement section for details). If the BY break-group clause is not specified at their definition, the sub-accumulators behave the same as their full versions.

The conversion process will use the aggregate phrase to convert each accumulator to a sub-class of the p2j.com.goldencode.util.Accumulator class. The following table presents all aggregators supported by 4GL, their mapping to the FWD implementation class and the type of the expression which can be used with such accumulators:

Long Name Short Name Converted Class Expression 4GL Type Description
AVERAGE AVG util.AverageAccumulator integer
decimal
recid
Computes the average value of the given expression.
COUNT - util.CountAccumulator any type Counts the number of loop iterations performed.
TOTAL - util.TotalAccumulator integer
decimal
recid
Performs a numeric total across all iterations of a loop.
MINIMUM MIN util.MinimumAccumulator any type Determines the minimum value from among all expressions' values across all iterations of a loop.
MAXIMUM MAX util.MaximumAccumulator any type Determines the maximum value from among all expressions' values across all iterations of a loop.
SUB-AVERAGE SUB-AVG util.AverageAccumulator integer
decimal
recid
Same as AVG, with the difference that it provides the average value for each break group.
SUB-COUNT - util.CountAccumulator any type Same as COUNT, with the difference that it provides the count for each break group.
SUB-TOTAL - util.TotalAccumulator integer
decimal
recid
Same as TOTAL, with the difference that it provides the total value for each break group.
SUB-MINIMUM SUB-MIN util.MinimumAccumulator any type Same as MIN, with the difference that it provides the minimum value for each break group.
SUB-MAXIMUM SUB-MAX util.MaximumAccumulator any type Same as MAX, with the difference that it provides the maximum value for each break group.

From the conversion's point of view, the aggregator type and the accumulated expressions are only used to map the accumulator to the appropriate FWD class and to create an distinct accumulator for each unique (aggregator type, expression) pair. The conversion differences are related to how and where the accumulator is used - inside the ACCUMULATE statement, the ACCUM function, with a DISPLAY statement, inside a nested block or other cases - each of them will be explained in detail in the next sections. The point of this notice is that the next sections will not focus on repeating the same example or detail for each and every accumulator; instead, it will use only one or few aggregator types and the reader must understand that, unless noted otherwise, the conversion rules apply to all of them.

During conversion, the following APIs will be emitted in the converted code depending on how the accumulator was used. As they reside in the accumulator's base class util.Accumulator, they are common for all aggregator types. For more details about why each API is emitted, please see their associated sections.

API Details Section
void addBreakGroup(Resolvable) Track a new break group category within this sub-accumulator. Takes as parameter the key which will be used to manage and retrieve results for the specified break group. ACCUMULATE Statement
void accumulate() This method may be called one or more times per loop iteration, either directly by client code, or indirectly by a query with which this accumulator is associated.
Upon invocation, it checks whether accumulation is enabled or not. If so, it evaluates the expression and then performs an implementation-specific action to update the accumulator's internal state.
If a the result must be reset, because the result set being iterated entered a new break group, this is done before the new data item is accumulated.
ACCUMULATE Statement
void setSubOnly() Marks this accumulator as a sub-only accumulator. This call will be emitted only when aggregate phrase contains also a BY break-group clause. ACCUMULATE Statement
DISPLAY Statement with Aggregate Phrase
void reset() When a block contains a nested accumulator reference, a call to this method will be emitted to notify the block that it uses this accumulator, so that the accumulation value for all parents block will be updated accordingly. Block Behavior for Accumulators
void ifEnded() This method is used to notify the accumulator that an IF statement which contains the ACCUM statement or a DISPLAY with aggregate phrase has ended. Block Behavior for Accumulators
void postponeReset() When an IF statement which contains the ACCUM statement or DISPLAY with aggregate phrase is started (and the accumulation statement is not also enclosed within a block with an implicit looping property), the accumulator must be reset when the enclosed accumulation did not execute. Block Behavior for Accumulators
void postponeUnknown() When an IF statement which contains the ACCUM statement or DISPLAY with aggregate phrase is started (and also the accumulation statement is not also enclosed within a block with an implicit looping property), the accumulator must be set to UNKNOWN when the enclosed accumulation did not execute. Block Behavior for Accumulators

When instantiating an accumulator of a certain type, the conversion rules will emit constructors depending on the aggregator and expression type, as presented in the next table. The c'tors are grouped together by their usage, as a c'tor signature may apply to more than one aggregator type, with the same semantics.

API Details
AverageAccumulator(Class)
TotalAccumulator(Class)
MinimumAccumulator(Class)
MaximumAccumulator(Class)
This c'tor is emitted only when a DISPLAY with aggregate phrase statement is encountered. Each c'tor takes as parameter the type of the accumulated expression. For more details, please see the DISPLAY Statement with Aggregate Phrase section of this chapter.
AverageAccumulator(int)
AverageAccumulator(double)
TotalAccumulator(int)
TotalAccumulator(double)
MinimumAccumulator(int)
MinimumAccumulator(double)
MinimumAccumulator(String)
MinimumAccumulator(boolean)
MaximumAccumulator(int)
MaximumAccumulator(double)
MaximumAccumulator(String)
MaximumAccumulator(boolean)
If the accumulated expression is a constant value, then this constant will be accumulated on each iteration. Depending on the aggregator's type, it can receive either only numeric constants or also string and logical constants.
AverageAccumulator(BaseDataType)
TotalAccumulator(BaseDataType)
MinimumAccumulator(BaseDataType)
MaximumAccumulator(BaseDataType)
When the accumulated expression is a simple variable, a reference to this variable is saved by the accumulator. On each accumulation, the variable will be evaluated (to retrieve its value) and this value will be accumulated.
AverageAccumulator(Resolvable)
TotalAccumulator(Resolvable)
MinimumAccumulator(Resolvable)
MaximumAccumulator(Resolvable)
In cases of complex expressions or when a buffer field is used, a Resolvable instance is used to encapsulate the expression - this forces the expression or field to be evaluated each time the accumulation is performed.
CountAccumulator() The COUNT accumulator is a special aggregator which doesn't rely on the expression when the accumulator is performed. The expression is used only during conversion, to uniquely distinguish between two COUNT accumulators. Each time the accumulation is performed, the internal counter for this accumulator will be incremented.

Following are a few cases which show when these c'tors are generated by the conversion rules. At this time, these examples want to prove only how the converted c'tors and the accumulator's expression gets converted, so other details will be ignored, as they will be treated in the next sections.

Example 1:

def var i as int.
...
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
   ...
});
...

Details:

The TotalAccumulator(BaseDataType) c'tor is emitted, as the accumulated expression is a plain 4GL variable.

Example 2:

def var i as int.
...
repeat i = 1 to 3:
   accum i (average).
end.

Converted code:

...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final AverageAccumulator accumAvg0 = new AverageAccumulator(i);
   ...
});
...

Details:

The AverageAccumulator(BaseDataType) c'tor is emitted, as the accumulated expression is a plain 4GL variable.

Example 3:

def var i as int.
...
repeat i = 1 to 3:
   ...
   accum 1 (total).
   ...
end.

Converted code:

...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(1);
   ...
});
...

Details:

The TotalAccumulator(int) c'tor is emitted, as the accumulated expression is an integer constant.

Example 4:

def var i as int.
...
repeat i = 1 to 3:
   ...
   accum 1.0 (total).
   ...
end.

Converted code:

...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(1.0);
   ...
});
...

Details:

The TotalAccumulator(double) c'tor is emitted, as the accumulated expression is a decimal constant.

Example 5:

def var i as int.
...
repeat i = 1 to 3:
   ...
   accum “test” (min).
   ...
end.

Converted code:

...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final MinumumAccumulator accumMin0 = new MinimumAccumulator(“test”);
   ...
});
...

Details:

The MinimumAccumulator(String) c'tor is emitted, as the accumulated expression is a string constant.

Example 6:

def var i as int.
...
repeat i = 1 to 3:
   accum i + 1 (total).
end.

Converted code:

ToClause toClause5 = new ToClause(i, 1, 3);

repeatTo("loopLabel5", toClause5, new Block()
{
   IntegerExpression accumExpr0 = new IntegerExpression()
   {
      public integer execute()
      {
         return plus(i, 1);
      }
   };

   final TotalAccumulator accumTotal0 = new TotalAccumulator(accumExpr0);
   ...
});

Details:

The TotalAccumulator(Resolvable) c'tor is emitted, as the accumulated expression is a complex expression. Note how the accumExpr0 defines an anonymous class by extending the IntegerExpression class (as this is an integer expression). Each time accumulation is performed, accumExpr0.execute() will be called to evaluate the expression.

Example 7:

def var i as int.
...
repeat i = 1 to 3:
   accum i + 1 (count).
end.

Converted code:

ToClause toClause6 = new ToClause(i, 1, 3);

repeatTo("loopLabel6", toClause6, new Block()
{
   final CountAccumulator accumCount0 = new CountAccumulator();
   ...
});

Details:

The CountAccumulator() default c'tor is emitted regardless of how the expression looks. When a complex expression is involved, it will be ignored, as it doesn't need to be evaluated.

Example 8:

for each book:
   ...
   accum book.sold-qty (total).
   ...
end.

Converted code:

forEach("loopLabel8", new Block()
{
   ...
   FieldReference fieldRef0 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal0 = new TotalAccumulator(fieldRef0);
   ...
});

Details:

When the accumulated data comes from a buffer field, the generated c'tor will take as parameter a field reference (similar to how the DISPLAY statement displays a field). Each time accumulation is performed, the field's current value will be retrieved and accumulated.

Example 9:

def var i as int.
...
for each book
break by book.book-title:
   ...
   accum book.cost (average by book.book-title).
   ...
end.

Converted code:

forEach("loopLabel10", new Block()
{
   ...
   FieldReference fieldRef0 = new FieldReference(book, "cost");

   final AverageAccumulator accumAvg0 = new AverageAccumulator(fieldRef0);
   ...
});

Details:

When accumulating data from a buffer field, the AverageAccumulator(Resolvable) c'tor is emitted, as the field's value needs to be evaluated on each iteration. This example computes the average cost for each book.

Example 10:

for each book
break by book.publisher:
   ...
   display book.publisher book.sold-qty (total).
   ...
end.

Converted code:

AverageAccumulator accumDisp0 = new AverageAccumulator(decimal.class);
...
forEach("loopLabel11", new Block()
{
   ...
});

Details:

When defining the accumulator using the aggregate phrase with a DISPLAY statement, the conversion rules must emit an instance field for the external procedure; in this case, the field must not have access to the actual buffer field being accumulated (as the buffer is not yet in scope), so the TotalAccumulator(decimal.class) c'tor is emitted. This example displays the publishers, the quantity of sold books and the total number of sold books.

Naming and Scoping

As mentioned in the previous sections, an accumulator is determined to be unique using its type and the accumulated expression. Beside this, 4GL keeps separate scopes for all accumulators defined in the external procedures, internal procedures, functions or triggers - if an accumulator is defined for the same expression and aggregator type in one or more such blocks, 4GL will treat each occurrence as being in a distinct scope and they will be treated as distinct accumulators by the conversion rules. Note that the EDITING blocks (with the PROMPT-FOR, UPDATE or SET statements) do not have isolated scopes, so when used inside an editing block, the accumulator will scope to the editing block's parent.

During conversion, an accumulator is converted to an instance field for the external procedure class or for the block which encloses all references of this accumulator, depending on some rules. The name of the instance field will be determined using the aggregator's type and a counter.

The accumulator's converted name starts with the accum prefix followed by a string depending on the aggregator type and ends with an unique counter, to distinguish between two accumulators of the same type. The name of the accumulator is determined based on rules found in the following table. The # character represents an unique counter which counts all accumulators of the given type.

Aggregator Type Converted Name Details
AVG
SUB-AVG
accumAvg# Uniquely identifies AVG and SUB-AVG accumulators.
COUNT
SUB-COUNT
accumCount# Uniquely identifies COUNT and SUB-COUNT accumulators.
TOTAL
SUB-TOTAL
accumTotal# Uniquely identifies TOTAL and SUB-TOTAL accumulators.
MIN
SUB-MIN
accumMin# Uniquely identifies MIN and SUB-MIN accumulators.
MAX
SUB-MAX
accumMax# Uniquely identifies MAX and SUB-MAX accumulators.
DISPLAY with aggregate phrase accumDisp# This distinct name is used for all accumulators first defined in a DISPLAY with aggregate phrase. They are not split into accumulator types, instead the # counter will count and uniquely identify all such accumulators.

Note that if an accumulator is used in both the ACCUMULATE statement and in a DISPLAY with aggregate phrase statement, the accumulator's converted name will be determined based on where the accumulator is first used: if it's the ACCUMULATE statement, then the converted name will be accum<type>#; else, the converted name will be accumDisp# .

The accumulator will be converted to an instance field for the external procedure or for an inner block, following these rules:

  1. the accumulator will be promoted to an instance field for the external procedure if the ACCUM function is used with a FORM, PUT or EXPORT statement.
  2. the accumulator will be promoted to an instance field for the external procedure if the accumulator is used in a DISPLAY with aggregate phrase statement.
  3. the accumulator will be promoted to an instance field for the external procedure if the ACCUM function is used directly in the external procedure, internal procedure, function or trigger.
  4. the nearest block which encloses all references of this accumulator, in all other cases.

The following examples will use the ACCUMULATE statement (using the accum <expression> (<type>) syntax) and the DISPLAY with aggregate phrase to define an accumulator and explain how the instance field is named and scoped; please see the ACCUMULATE Statement and the DISPLAY Statement with Aggregate Phrase sections for details about how these statements get converted or, for cases where the ACCUM function is used (using the ACCUM <type> <expression> syntax), please see the ACCUM Function section for details. The examples will show only how the accumulator is named and scoped, without details about how the other statements get converted.

Example 1:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (total).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
   ...
});

Details:

The TOTAL accumulator for the i variable is converted to the accumTotal0 field. As the accumulator was used only in the REPEAT block, it was converted to an instance field for this block.

Example 2:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (count).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final CountAccumulator accumCount0 = new CountAccumulator();
   ...
});

Details:

The COUNT accumulator for the i variable is converted to the accumCount0 field. As the accumulator was used only in the REPEAT block, it was converted to an instance field for this block.

Example 3:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (max).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final MaximumAccumulator accumMax0 = new MaximumAccumulator(i);
   ...
});

Details:

The MAX accumulator for the i variable is converted to the accumMax0 field. As the accumulator was used only in the REPEAT block, it was converted to an instance field for this block.

Example 4:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (min).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final MinimumAccumulator accumMin0 = new MinimumAccumulator(i);
   ...
});

Details:

The MIN accumulator for the i variable is converted to the accumMin0 field. As the accumulator was used only in the REPEAT block, it was converted to an instance field for this block.

Example 5:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (average).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   final AverageAccumulator accumAvg0 = new AverageAccumulator(i);
   ...
});

Details:

The AVERAGE accumulator for the i variable is converted to the accumAvg0 field. As the accumulator was used only in the REPEAT block, it was converted to an instance field for this block.

Example 6:

def var i as int init 0.
...
repeat i = 1 to 3:
   display i (total).
end.
...

Converted code:

TestFrame0 frame0 = GenericFrame.createFrame(TestFrame0.class, "");

TotalAccumulator accumDisp0 = new TotalAccumulator(integer.class);
...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   public void init()
   {
      frame0.openScope();
      ...
      accumTotal0.reset();
   }
   ...
});

Details:

The TOTAL accumulator for the i variable, as it was used in an aggregate phrase for the DISPLAY statement, is converted to the accumDisp0 field for the external procedure class (it follows the frame definition).

Example 7:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   accum i (total).
   B2:
   repeat j = 1 to 3:
      display i (total).
   end.
end.
...

Converted code:

...
repeatTo("B1", toClause1, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
   ...
   public void body()
   {
      accumTotal0.accumulate();
      ...
      repeatTo("B2", toClause2, new Block()
      {
         public void init()
         {
            frame0.openScope();
            ...
            accumTotal0.reset();
         }
         ...
      });
   }
});

Details:

As the accumulator is first used with the ACCUMULATE statement, the converted name will be accumTotal0 and not accumDisp0. See how the accumulator is determined to be unique using only the expression and its type, so the inner block accumulates data for the same accumulator as the parent block. The accumulator is converted to instance field for the block which encloses all accumulator and frame references, so in this case will be block B1.

Example 8:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   display i (total).
   B2:
   repeat j = 1 to 3:
      accum i (total).
   end.
end.
...

Converted code:

AccumCas5b2Frame0 frame0 = GenericFrame.createFrame(AccumCas5b2Frame0.class, "");

TotalAccumulator accumDisp0 = new TotalAccumulator(j);
...
...
repeatTo("B1", toClause0, new Block()
{
   public void init()
   {
      frame0.openScope();
      ...
      accumDisp0.reset();
   }

   public void body()
   {
      ...
      repeatTo("B2", toClause1, new Block()
      {
         public void init()
         {
            accumDisp0.reset();
         }

         public void body()
         {
            accumDisp0.accumulate();
         }
      });
   }
});

Details:

If the order is reversed and the accumulator is first used with in the DISPLAY statement, the converted name will be accumDisp0. Also, as the accumulator was used with the DISPLAY statement first, it was converted to an instance field for the external procedure.

Example 9:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum 1 + i (total).
   accum i + 1 (total).
end.
...

Converted code:

ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel0", toClause0, new Block()
{
   IntegerExpression accumExpr0 = new IntegerExpression()
   {
      public integer execute()
      {
         return plus(1, i);
      }
   };

   final TotalAccumulator accumTotal0 = new TotalAccumulator(accumExpr0);

   IntegerExpression accumExpr1 = new IntegerExpression()
   {
      public integer execute()
      {
         return plus(i, 1);
      }
   };

   final TotalAccumulator accumTotal1 = new TotalAccumulator(accumExpr1);
   ...
});

Details:

Even if the 1 + i and i + 1 are semantically equivalent, this doesn't mean that they are the same accumulator; the conversion rules will emit two distinct accumulators, for each expression - these will be accumTotal0 and accumTotal1, with their associated expression, accumExpr0 and accumExpr1. In this case, both accumulators are converted to instance field for the REPEAT block, as they are not referenced outside of it.

Example 10:

def var i as int.

procedure proc1.
   B1:
   repeat i = 1 to 3:
      accum i (total).
   end.
end.

B2:
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal1 = new TotalAccumulator(i);
...
ToClause toClause1 = new ToClause(i, 1, 3);

repeatTo("B2", toClause1, new Block()
{
     ...
});
...
public void proc1()
{
   internalProcedure(new Block()
   {
      public void body()
      {
         ToClause toClause0 = new ToClause(i, 1, 3);

         repeatTo("B1", toClause0, new Block()
         {
            final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
            ...
         });
      }
   });
}

Details:

The two accumulators, even if they are for the same variable, they are in different scopes - this will result in two accumulators, one defined in the proc1 internal procedure and one defined in the external procedure.

Example 11:

def var i as int.

function func1 return int.
   B1:
   repeat i = 1 to 3:
      accum i (total).
   end.
   return 0.
end.

B2:
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal1 = new TotalAccumulator(i);
...
ToClause toClause1 = new ToClause(i, 1, 3);

repeatTo("B2", toClause1, new Block()
{
   ...
});
...
public integer func1()
{
   return integerFunction(new Block()
   {
      public void body()
      {
         ToClause toClause0 = new ToClause(i, 1, 3);

         repeatTo("B1", toClause0, new Block()
         {
            final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
            ...
         });

         returnNormal(0);
      }
   });
}

Details:

The two accumulators, even if they are for the same expression and of the same type, they are in different scopes - this will result in two accumulators, one defined in the func1 function and one defined in the external procedure.

Example 12:

def var i as int.

on endkey anywhere do:
   B1:
   repeat i = 1 to 3:
      accum i (total).
   end.
end.

B2:
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal1 = new TotalAccumulator(i);
...
ToClause toClause1 = new ToClause(i, 1, 3);

repeatTo("B2", toClause1, new Block()
{
   ...
});
...
public class TriggerBlock0
extends Trigger
{
   public void body()
   {
      ToClause toClause0 = new ToClause(i, 1, 3);

      repeatTo("B2", toClause0, new Block()
      {
         final TotalAccumulator accumTotal0 = new TotalAccumulator(i);
         ...
      });
   }
}

Details:

The two accumulators, even if they are for the same expression and of the same type, they are in different scopes - this will result in two accumulators, one defined in the trigger and one defined in the external procedure.

Example 13:

def var i as int.

update j editing:
   B1:
   repeat i = 1 to 3:
      accum i (total).
   end.
end.

B2:
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal0 = new TotalAccumulator(i);

updateEditing(frame0, elementList0, "editingLabel0", new Block()
{
   public void body()
   {
      ToClause toClause0 = new ToClause(i, 1, 3);

      repeatTo("B2", toClause0, new Block()
      {
         ...
      });
   }
});

ToClause toClause1 = new ToClause(i, 1, 3);

repeatTo("B2", toClause1, new Block()
{
   ...
});

Details:

In this case, as the EDITING block does not affect the accumulator scope, the two accumulator statements reference the same accumulator. So, a single accumulator will be instantiated for the external procedure class.

Example 14:

def var i as int.
...
repeat i = 1 to 3:
   accum i (total).
end.
message string(accum total i).

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal0 = new TotalAccumulator(i);
...
ToClause toClause0 = new ToClause(i, 1, 3);

repeatTo("loopLabel1", toClause0, new Block()
{
   ...
});

message(valueOf((integer) accumTotal0.getResult()));

Details:

As the accumulator was used with the ACCUM function in the external procedure, it will be emitted as an instance field for the external procedure.

Example 15:

def var i as int.

procedure proc1.
   repeat i = 1 to 3:
      accum i (total).
   end.
   message string(accum total i).
end.

Converted code:

integer i = new integer(0);

TotalAccumulator accumTotal0 = new TotalAccumulator(i);
...
public void proc1()
{
   internalProcedure(new Block()
   {
      public void body()
      {
         ToClause toClause0 = new ToClause(i, 1, 3);

         repeatTo("loopLabel0", toClause0, new Block()
         {
            ...
         });

         message(valueOf((integer) accumTotal0.getResult()));
      }
   });
}

Details:

As the accumulator was used with the ACCUM function in the proc1 procedure, it will be emitted as an instance field for the external procedure (note that the accumulator still remains scoped only to the proc1 procedure in 4GL terms).

ACCUMULATE Statement

The only way to define an accumulator in 4GL is by using the ACCUMULATE statement or an aggregate phrase with a DISPLAY statement; without first being defined by one of these two statements, the accumulator does not exist and can't be used in the ACCUM function.

The ACCUMULATE statement is used to calculate one or more aggregate values from a certain expression. Its short form is ACCUM and must not be confused with the ACCUM function. The syntax of this statement is:

ACCUMULATE { expression (aggregate-phrase) } . . .

where the expression can be any valid 4GL expression with its type compatible with the specified aggregate phrase(s). For this case, the aggregate phrase has the following form:

{ AVG | COUNT | TOTAL | MIN | MAX | SUB-AVG | SUB-COUNT | SUB-TOTAL | SUB-MIN | SUB-MAX } . . .
{ BY break-group } . . .

For each expression, 4GL allows the declaration of multiple aggregators, via the aggregate phrase. The conversion process will take into consideration that each expression may have more than one aggregator specified and, in these cases, will trigger the generation of an unique accumulator instance for each combination of (expression, aggregator).

The BY break-group can be used only with query loops. For a certain accumulated expression, multiple break groups can be defined - the only constraint is that for each break group specified with the aggregate phrase, there must be a BREAK BY entry defined at the query loop.

When using the BY break-group clause there are a few cases which do not impact the converted code, but they are worth mentioning:

  • if the BY break-group clause is set for a full accumulator, it will be possible to retrieve both the aggregated result for the current group and the grand total (when the BY break-group clause is not specified for the ACCUM function).
  • If the ACCUMULATE statement does not specify a BY break-group clause for this sub-accumulator, then when retrieving its result using the ACCUM function the BY break-group clause must not be set - in this case, the sub-accumulator is not a real sub-accumulator and it will behave as a full accumulator.
  • if the ACCUMULATE statement does specify a BY break-group clause and this is a sub-accumulator, the data will be aggregated only for each group (the aggregated result for the entire data set will not be available in this block, will be available only after this block has finished).
  • the sub-only properties of a sub-accumulator are scoped starting only with the block where the sub-accumulator is first defined. So, if its full version is used in a parent block and its sub-accumulator version is used in a child block, the accumulator will maintain sub-accumulator properties only in the child block.

In 4GL, accumulation can be performed only inside blocks which have the loop property; thus, the ACCUMULATE statement and the DISPLAY with aggregate phrase can be used only when they are nested in one of the following blocks: REPEAT, DO ... {TRANSACTION | ON END-KEY UNDO, LEAVE}, FOR {EACH | LAST | FIRST}. If it does not have as parent (direct or not) a block with the loop property, the compilation and the conversion of the 4GL source code will fail. Each time a block which uses (on any nesting level) a certain accumulator is encountered, an Accumulator.reset() call is added in the block's init() method - see the Block Behavior for Accumulators section for details about why this call is added.

When accumulating data, if there is more than one ACCUMULATE statement (as a direct child) for the same accumulator in certain block, 4GL will accumulate data only for the first encountered statement. FWD solves this by leaving in the converted code, for a certain accumulator, only the first ACCUMULATE statement's occurence - all subsequent direct references will discarded. Also, in 4GL the accumulators are undoable, but this doesn't affect the converted code; instead, the FWD runtime will take care of rolling back the accumulated data, when an iteration is rolled back.

Example 1:

def var i as int.
...
repeat i = 1 to 3:
   accum i (total).
end.

Converted code:

...
repeatTo("loopLabel0", toClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
   }
});

Details:

This example shows how the statement gets converted with the REPEAT block. See the accumTotal0.accumulate() call added in the REPEAT's body() method.

Example 2:

def var i as int.
...
do i = 1 to 3 transaction:
   accum i (total).
end.

Converted code:

...
doTo(TransactionType.FULL, "loopLabel0", toClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
   }
});

Details:

When the DO ... TRANSACTION block is found as parent, the conversion rules emit the same accumulator code as for the REPEAT case.

Example 3:

def var i as int.
...
do i = 1 to 3 on end-key undo, leave:
   accum i (total).
end.

Converted code:

...
doTo(TransactionType.SUB, "loopLabel0", toClause0, onPhrase0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
   }
});

Details:

When the DO ... ON END-KEY UNDO, LEAVE block is found as parent, the converted rules emit the same accumulator code as for the REPEAT case.

Example 4:

for each book:
   accum book.sold-qty (total).
end.

Converted code:

forEach("loopLabel4", new Block()
{
   AdaptiveQuery query0 = null;

   FieldReference fieldRef0 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal0 = new TotalAccumulator(fieldRef0);

   public void init()
   {
      RecordBuffer.openScope(book);
      query0 = new AdaptiveQuery(book, (String) null, null, "book.bookId asc");
      query0.addAccumulator(accumTotal0);
      accumTotal0.reset();
   }

   public void body()
   {
      query0.next();
      accumTotal0.accumulate();
   }
});

Details:

This example shows how the statement gets converted with the FOR EACH block. In this case, the accumulator must be registered with the query, so that the next time a new record is retrieved, the accumTotal0.accumulate() call will know that a new iteration is in effect and it will trigger accumulation.

Example 5:

for first book:
   accum book.sold-qty (total).
end.

Converted code:

forBlock("blockLabel0", new Block()
{
   RandomAccessQuery query1 = null;

   FieldReference fieldRef1 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal2 = new TotalAccumulator(fieldRef1);

   public void init()
   {
      RecordBuffer.openScope(book);
      query1 = new RandomAccessQuery(book, (String) null, null, "book.bookId asc");
      accumTotal2.reset();
   }

   public void body()
   {
      query1.first();
      accumTotal2.accumulate();
   }
});

Details:

In the FOR FIRST block case, the accumulator does get registered with the query, all accumulator-related converted code is the same as with the REPEAT block.

Example 6:

for last book:
   accum book.sold-qty (total).
end.

Converted code:

forBlock("blockLabel1", new Block()
{
   RandomAccessQuery query2 = null;

   FieldReference fieldRef2 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal3 = new TotalAccumulator(fieldRef2);

   public void init()
   {
      RecordBuffer.openScope(book);
      query2 = new RandomAccessQuery(book, (String) null, null, "book.bookId asc");
      accumTotal3.reset();
   }

   public void body()
   {
      query2.last();
      accumTotal3.accumulate();
   }
});

Details:

In the FOR LAST block case, the accumulator does get registered with the query, all accumulator-related converted code is the same as with the REPEAT block.

Example 7:

define query qBook for book.
open query qBook for each book.
get first qBook.
do while available(book) transaction:
   accum book.sold-qty (total).
   get next qBook.
end.

Converted code:

RecordBuffer.openScope(book);
query0.assign(new AdaptiveQuery(book, (String) null, null, "book.bookId asc"));
query0.open();
query0.first();

LogicalExpression whileClause0 = new LogicalExpression()
{
   public logical execute()
   {
      return RecordBuffer.isAvailable(book);
   }
};

doWhile(TransactionType.FULL, "loopLabel0", whileClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      query0.next();
   }
});

Details:

This example is just a more complicated example of the DO ... TRANSACTION case: it shows how the statement gets converted when the DO block iterates over a query - the code added for the accumulator is the same as in the previous examples.

Example 8

def var i as int.
...
repeat i = 1 to 3:
   accum i (total).
   ...
   accum i (total).
end.

Converted code:

...
repeatTo("loopLabel1", toClause1, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      i.assign(plus(i, 1));
   }
});

Details:

The conversion rules will emit only the first ACCUMULATE statement call, and will remove all subsequent ones. Note how the code is emitted only for the first ACCUMULATE statement.

Example 9:

def var i as int.
def var j as int.
...
B1:
repeat i = 1 to 3:
   accum i (total).
   ...
   B2:
   repeat j = 1 to 3:
      accum i (total).
   end.
end.

Converted code:

repeatTo("B1", toClause0, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(i);

   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();

      ...
      repeatTo("B2", toClause1, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            accumTotal0.accumulate();
         }
      });
   }
});

Details:

This examples proves that 4GL ignores only multiple accumulator references which are directly nested in a block. As the second ACCUMULATE call is not a direct child of the B1 block, it will not be removed.

Example 10:

for each book
break by book.publisher:
   accum book.sold-qty (total by book.publisher).
end.

Converted code:

forEach("loopLabel1", new Block()
{
   PresortQuery query1 = null;

   FieldReference byExpr0 = new FieldReference(book, "publisher");

   public void init()
   {
      query1 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query1.enableBreakGroups();
      query1.setNonScrolling();
      query1.addSortCriterion(byExpr0);
      query1.addAccumulator(accumTotal0);
      accumTotal0.addBreakGroup(byExpr0);
      accumTotal0.reset();
   }

   public void body()
   {
      query1.next();
      accumTotal0.accumulate();
   }
});

Details:

A BY clause can be used with the ACCUMULATE statement only if it is nested in a query block and it contains a BREAK BY clause for the same group. In this case, see how the break group is registered with the accumulator (via the addBreakGroup call) and also how it is registered with the query (via the addAccumulator call), so that it will be notified when a new break group is entered. Note that this is not a sub-accumulator, so using the ACCUM function to retrieve the result will give either the grand total or the group total, depending on whether the BY clause was specified or not.

Example 11:

for each book:
   accum book.sold-qty (sub-total).
end.

Converted code:

forEach("loopLabel8", new Block()
{
   AdaptiveQuery query0 = null;

   FieldReference fieldRef0 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal0 = new TotalAccumulator(fieldRef0);

   public void init()
   {
      RecordBuffer.openScope(book);
      query0 = new AdaptiveQuery(book, (String) null, null, "book.bookId asc");
      query0.addAccumulator(accumTotal0);
      accumTotal4.reset();
   }

   public void body()
   {
      query0.next();
      accumTotal0.accumulate();
   }
});

Details:

If a sub-accumulator is used without a BY break-group clause, it will behave as a full-accumulator and the conversion rules will emit the same code as for its full version.

Example 12:

for each book
break by book.publisher:
   accum book.sold-qty (sub-total by book.publisher).
end.

Converted code:

forEach("loopLabel2", new Block()
{
   PresortQuery query2 = null;

   FieldReference byExpr0 = new FieldReference(book, "publisher");

   public void init()
   {
      query2 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query2.enableBreakGroups();
      query2.setNonScrolling();
      query2.addSortCriterion(byExpr0);
      query2.addAccumulator(accumTotal0);
      accumTotal0.setSubOnly();
      accumTotal0.addBreakGroup(byExpr0);
      accumTotal0.reset();
   }

   public void body()
   {
      query2.next();
      accumTotal0.accumulate();
   }
});

Details:

When using a sub-accumulator and a BY break-group clause, the accumulator will be marked as sub-accumulator for this block (see the setSubOnly() call added in the block's init() method). The cumulative aggregated result will be available only after the block has finished.

Example 13:

def var i as int.
def var j as int.

repeat i = 1 to 3:
  accum i (total min max average count).

  repeat j = 1 to 3:
      accum i (total).
  end.
end.

Converted code:

...
repeatTo("loopLabel2", toClause2, new Block()
{
   final TotalAccumulator accumTotal0 = new TotalAccumulator(i);

   final MinimumAccumulator accumMin0 = new MinimumAccumulator(i);

   final MaximumAccumulator accumMax0 = new MaximumAccumulator(i);

   final AverageAccumulator accumAvg0 = new AverageAccumulator(i);

   final CountAccumulator accumCount0 = new CountAccumulator();

   public void init()
   {
      accumTotal0.reset();
      accumMin0.reset();
      accumMax0.reset();
      accumAvg0.reset();
      accumCount0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      accumMin0.accumulate();
      accumMax0.accumulate();
      accumAvg0.accumulate();
      accumCount0.accumulate();

      ...
      repeatTo("loopLabel3", toClause3, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            accumTotal0.accumulate();
         }
      });
   }
});

Details:

As the aggregate phrase can contain multiple aggregators for the same expressions, the conversion rules will emit distinct accumulator instances for each aggregator type.

DISPLAY Statement with Aggregate Phrase

4GL provides a shortcut to display the accumulated expression's value and its aggregated (sub-)result, by using an aggregate phrase with the DISPLAY statement. The syntax of this case is:

DISPLAY { expression (aggregate-phrase) } . . .

where the aggregate phrase has a syntax similar with the ACCUMULATE statement case:

{ AVG | COUNT | TOTAL | MIN | MAX | SUB-AVG | SUB-COUNT | SUB-TOTAL | SUB-MIN | SUB-MAX }
[ LABEL aggr-label ]
{ BY break-group }

Here, the aggr-label is an optional attribute which sets the label for the row where the aggregated result will be displayed (note that this clause is not supported at the time of this writing).

When multiple aggregators are specified for a certain expression, they will be displayed always in the same order, regardless of the order specified in the 4GL code; this order comes from a hard-coded weight set internally by 4GL for each aggregator. When the aggregated result is displayed, the row containing this data will have a default label set to it, which can be seen in the next table. Note that the weight and default label for the sub-accumulators is the same as the weight and default label for their full versions.

Aggregator Weight Default Label
TOTAL
SUB-TOTAL
1 TOTAL
COUNT
SUB-COUNT
2 COUNT
MAX
SUB-MAX
3 MAX
MIN
SUB-MIN
4 MIN
AVG
SUB-AVG
5 AVG

When an accumulator is used in an aggregate phrase with the DISPLAY statement, there are some special cases which need to be considered, depending on how the aggregate phrase was defined. Although some of them are handled by the FWD runtime, they are worth mentioning, to understand why these were not handled in the converted code:

  • If the DISPLAY statement is never executed (thus no accumulation is performed) but the block is iterated at least once, the frame will still display the total or sub-total results, once the block is finished. This is handled by the FWD runtime, as each accumulator is registered with the frame and the frame is notified when the block is finished.
  • A certain accumulator can be used with different DISPLAY statements, within the same block, but the accumulation is triggered only for the first DISPLAY call. Note that this is currently not supported by FWD.
  • The accumulators registered with a DISPLAY statement may never accumulate. Thus, as access to the actual expression may not be available, their data type will be set on instantiation by the conversion rules and this type will be identical to the result type of the accumulated expression. More, this type is identical to the type of the widget with which this accumulator is associated in the frame .
  • If a BY break-group clause is specified when a full accumulator is used with a DISPLAY statement, it will display the accumulation result for each group and a final result for the entire data set. If it does not have a group specified, this will display only the final result. The conversion rules will emit the break group for the DISPLAY's aggregate phrase, but the FWD runtime will be responsible of displaying the final or group result.
  • If a BY break-group clause is specified when the sub-accumulator is used with a DISPLAY statement, it will display the accumulation result for each group, but it will not display the final result for the entire data set. If it does not have a group specified, this will display only the final result. The conversion rules will emit the break group for the DISPLAY's aggregate phrase, but the FWD runtime will be responsible of displaying the final result.
  • No explicit Accumulator.accumulate() calls will be emitted, as the accumulation will be performed only when the frame will be displayed.

As the aggregate phrase belongs to this frame, in 4GL terms this means that the accumulators defined by such aggregate phrases must be registered with the frame, when the frame enters its scope. For this, the conversion process will emit a special registration code in the init() method for the same block as where the frame's openScope() call is emitted:

...
public void init()
{
   frame.openScope();

   FrameElement[] aggrList = new FrameElement[]
   {
      new AggregatorElement(<expression0>, frame.widget(), <accum0>),
      new AggregatorElement(<expression1>, frame.widget(), <accum1>)
      ...
   };
   frame.registerAccumulators(aggrList);
   ...
   <accum0>.reset();
   <accum1>.reset();
   ...
}

When registering or displaying an accumulator, a special FrameElement subclass is used - com.goldencode.2j.ui.AggregatorElement. This class has specialized constructors similar to the Accumulator subclasses, so that it will take as parameters:

  • the expression (<expression> in the code snippet above), which may be a constant, variable, field or complex expression.
  • the frame's widget where the accumulated data will be displayed (this is the same widget as where the expression's values are displayed).
  • the Accumulator instance, <accum> in the code snippet above.

The conversion rules will emit in the aggrList array all accumulators defined with a certain frame.

Following examples will show how the accumulators are converted when their accumulation is triggered by the display of a frame:

Example 1:

def var i as int.
...
repeat i = 1 to 3:
   display i (total) with frame fa.
end.

Converted code:

TotalAccumulator accumDisp0 = new TotalAccumulator(integer.class);
...
repeatTo("loopLabel0", toClause0, new Block()
{
   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList0 = new FrameElement[]
      {
         new AggregatorElement(i, faFrame.widgeti(), accumDisp0)
      };
      faFrame.registerAccumulators(aggrList0);
      accumDisp0.reset();
   }

   public void body()
   {
      FrameElement[] elementList0 = new FrameElement[]
      {
         new AggregatorElement(i, faFrame.widgeti(), accumDisp0)
      };

      faFrame.display(elementList0);
   }
});

Details:

When using the aggregate phrase with a DISPLAY statement, the converted accumulator will follow the frame definition - so, it will be an instance field for the external procedure class (named accumDisp0 in this case). Also, the accumulator will be registered with the frame in the block's init() method (via the frame.registerAccumulators(...) call) so that the frame will know to display the final result, once the block has finished.

Example 2:

def var i as int.
...
repeat i = 1 to 3:
   display i (average) i (count) with frame fa.
end.

Converted code:

AverageAccumulator accumDisp0 = new AverageAccumulator(integer.class);

CountAccumulator accumDisp1 = new CountAccumulator();
...
repeatTo("loopLabel1", toClause1, new Block()
{
   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList1 = new FrameElement[]
      {
         new AggregatorElement(i, faFrame.widgeti(), accumDisp1),
         new AggregatorElement(i, faFrame.widgeti(), accumDisp0)
      };
      faFrame.registerAccumulators(aggrList1);
      accumDisp1.reset();
      accumDisp0.reset();
   }

   public void body()
   {
      FrameElement[] elementList1 = new FrameElement[]
      {
         new AggregatorElement(i, faFrame.widgeti(), accumDisp1),
         new AggregatorElement(i, faFrame.widgeti(), accumDisp0)
      };

      faFrame.display(elementList1);
   }
});

Details:

This example shows how multiple aggregators can be specified for the same expression - although the i variable appears two times, it will be displayed in only one widget and the result for the two aggregators will be displayed at the end of the REPEAT block, on distinct rows, by their weight. Also, see how the two aggregators are registered with the same widget.

Example 3:

for each book:
   display book.publisher book.book-title book.sold-qty (total) with frame fa.
end.

Converted code:

TotalAccumulator accumDisp2 = new TotalAccumulator(integer.class);
...
forEach("loopLabel2", new Block()
{
   AdaptiveQuery query0 = null;

   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList2 = new FrameElement[]
      {
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp2)
      };
      faFrame.registerAccumulators(aggrList2);
      RecordBuffer.openScope(book);
      query0 = new AdaptiveQuery(book, (String) null, null, "book.bookId asc");
      query0.addAccumulator(accumDisp2);
      accumDisp2.reset();
   }

   public void body()
   {
      query0.next();

      FrameElement[] elementList2 = new FrameElement[]
      {
         new Element(new FieldReference(book, "publisher"), faFrame.widgetPublisher()),
         new Element(new FieldReference(book, "bookTitle"), faFrame.widgetBookTitle()),
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp2)
      };

      faFrame.display(elementList2);
   }
});

Details:

This example shows each publisher, the sold quantity for each book and a grand total of sold books. When query loops are involved, the accumulator still needs to be registered with the query, via the query.addAccumulator() call.

Example 4:

for each book:
   display book.publisher book.sold-qty (sub-total) with frame fa.
end.

Converted code:

TotalAccumulator accumDisp3 = new TotalAccumulator(integer.class);
...
forEach("loopLabel3", new Block()
{
   AdaptiveQuery query1 = null;

   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList3 = new FrameElement[]
      {
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp3)
      };
      faFrame.registerAccumulators(aggrList3);
      RecordBuffer.openScope(book);
      query1 = new AdaptiveQuery(book, (String) null, null, "book.bookId asc");
      query1.addAccumulator(accumDisp3);
      accumDisp3.reset();
   }

   public void body()
   {
      query1.next();

      FrameElement[] elementList3 = new FrameElement[]
      {
         new Element(new FieldReference(book, "publisher"), faFrame.widgetPublisher()),
         new Element(new FieldReference(book, "bookTitle"), faFrame.widgetBookTitle()),
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp3)
      };

      faFrame.display(elementList3);
   }
});

Details:

This example is the same as the previous one and its goal is to show that sub-accumulators without a BY break-group clause are not real sub-accumulators - instead, they act as their full version. So, in this case, no accum.setSubOnly() call is emitted in the block's init() mehtod.

Example 5:

for each book
break by book.publisher:
   display book.publisher book.sold-qty (total by book.publisher) with frame fa.
end.

Converted code:

TotalAccumulator accumDisp4 = new TotalAccumulator(integer.class);
...
forEach("loopLabel4", new Block()
{
   PresortQuery query2 = null;

   FieldReference byExpr0 = new FieldReference(book, "publisher");

   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList4 = new FrameElement[]
      {
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp4)
      };
      faFrame.registerAccumulators(aggrList4);
      RecordBuffer.openScope(book);
      query2 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query2.enableBreakGroups();
      query2.setNonScrolling();
      query2.addSortCriterion(byExpr0);
      query2.addAccumulator(accumDisp4);
      accumDisp4.addBreakGroup(byExpr0);
      accumDisp4.reset();
   }

   public void body()
   {
      query2.next();

      FrameElement[] elementList4 = new FrameElement[]
      {
         new Element(new FieldReference(book, "publisher"), faFrame.widgetPublisher()),
         new Element(new FieldReference(book, "bookTitle"), faFrame.widgetBookTitle()),
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp4)
      };

      faFrame.display(elementList4);
   }
});

Details:

Full accumulators with BY break-group clause will display sub-totals for each break group and a final grand total. Note that accum.setSubOnly() is not emitted in this case either.

Example 6:

for each book
break by book.publisher:
   display book.publisher book.sold-qty (sub-total by book.publisher) with frame fa.
end.

Converted code:

TotalAccumulator accumDisp5 = new TotalAccumulator(integer.class);
...
forEach("loopLabel5", new Block()
{
   PresortQuery query3 = null;

   FieldReference byExpr1 = new FieldReference(book, "publisher");

   public void init()
   {
      faFrame.openScope();

      FrameElement[] aggrList5 = new FrameElement[]
      {
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp5)
      };
      faFrame.registerAccumulators(aggrList5);
      RecordBufaer.openScope(book);
      query3 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query3.enableBreakGroups();
      query3.setNonScrolling();
      query3.addSortCriterion(byExpr1);
      query3.addAccumulator(accumDisp5);
      accumDisp5.setSubOnly();
      accumDisp5.addBreakGroup(byExpr1);
      accumDisp5.reset();
   }

   public void body()
   {
      query3.next();

      FrameElement[] elementList5 = new FrameElement[]
      {
         new Element(new FieldReference(book, "publisher"), faFrame.widgetPublisher()),
         new Element(new FieldReference(book, "bookTitle"), faFrame.widgetBookTitle()),
         new AggregatorElement(new FieldReference(book, "soldQty"), faFrame.widgetSoldQty(), accumDisp5)
      };

      faFrame.display(elementList5);
   }
});

Details:

Finally, this is a real sub-accumulator - in this case, the setSubOnly() call in the init() method will mark it as a sub-accumulator in this block, so that it will display only group results.

Example 7

B1:
repeat i = 1 to 3:
   display i (total) with frame f1.
   B2:
   repeat j = 1 to 3:
      display i (total) with frame f2.
   end.
end.

Converted code:

...
repeatTo("B1", toClause2, new Block()
{
   final TotalAccumulator accumDisp6 = new TotalAccumulator(integer.class);

   public void init()
   {
      f1Frame.openScope();

      FrameElement[] aggrList6 = new FrameElement[]
      {
         new AggregatorElement(i, f1Frame.widgeti(), accumDisp6)
      };
      f1Frame.registerAccumulators(aggrList6);
      accumDisp6.reset();
   }

   public void body()
   {
      FrameElement[] elementList6 = new FrameElement[]
      {
         new AggregatorElement(i, f1Frame.widgeti(), accumDisp6)
      };

      f1Frame.display(elementList6);

      ...
      repeatTo("B2", toClause3, new Block()
      {
         public void init()
         {
            f2Frame.openScope();

            FrameElement[] aggrList7 = new FrameElement[]
            {
               new AggregatorElement(i, f2Frame.widgeti(), accumDisp6)
            };
            f2Frame.registerAccumulators(aggrList7);
            accumDisp6.reset();
         }

         public void body()
         {
            FrameElement[] elementList7 = new FrameElement[]
            {
               new AggregatorElement(i, f2Frame.widgeti(), accumDisp6)
            };

            f2Frame.display(elementList7);
         }
      });
   }
});

Details:

The goal of this example is to show that, if the same accumulator is used in the aggregate phrase for multiple DISPLAY statements, then in 4GL there is only one accumulator. The accumulation performed in the inner block will be seen by the parent block and, when the parent block is finished, it will display the aggregated result from both blocks.

ACCUM Function

The ACCUM function is used to retrieve the data previously aggregated with the ACCUMULATE statement or DISPLAY with aggregate phrase, for a certain expression. Note that Progress restricts this function so it can be used only if there is an ACCUMULATE statement (or DISPLAY with aggregate phrase) for this accumulator in the 4GL code prior to the function call; if there is no such call, the 4GL will fail with a compile error. As only 4GL code which passes compilation can be converted, the conversion rules will fail if, when an ACCUM function call is encountered, there is no found accumulator defined using the ACCUMULATE statement or a DISPLAY with aggregate phrase, for the same expression and aggregator tyep.

For each call, this function can return the aggregated result for only one expression and aggregator. Thus, its syntax is:

ACCUM { (aggregate-phrase) expression }

where the aggregate phrase must be under this form:

{ AVG | COUNT | TOTAL | MIN | MAX | SUB-AVG | SUB-COUNT | SUB-TOTAL | SUB-MIN | SUB-MAX }
{ BY break-group }

As mentioned before, the expression used with the ACCUM function must be in the exact form as when it was used with the ACCUMULATE statement or with the DISPLAY with aggregate phrase, even if they are semantically equivalent - i.e. price * qty-sold is not the same as qty-sold * price. The aggregate phrase can specify only one aggregator (with its associated BY break-group clause, if required). If a sub-accumulator is used and is missing the BY break-group clause, then it will behave as a full accumulator only if its definition also has no BY break-group clause. Else, if its definition has a BY break-group clause and this is a sub-accumulator, calling the ACCUM function without the BY break-group clause will return the reset value, as the aggregated result for the entire data set is not available yet (will be available once the block is finished). This does not affect converted code, this behavior is implemented in the FWD runtime.

When retrieving the accumulated result using the ACCUM function, the conversion rules will emit a distinct method call, depending on whether it needs only a result for a certain group (when a BY clause is used) or the current result, when no group is specified. For sub-results, the converted code will call the accum.getResult(Resolvable breakGroupKey) method, where the breakGroupKey is the key for which the sub-result is needed; for the full result, the converted code will call the accum.getResult() method. These methods are not defined in the p2j.com.goldencode.util.Accumulator, because their returned type depends on the type of the used aggregator; in both cases, the accum represents the name of the field which references the converted accumulator. The next table shows the returned type of the getResult methods, for each Accumulator sub-class in the FWD runtime:

Class AverageAccumulator CountAccumulator TotalAccumulator MinimumAccumulator MaximumAccumulator
Returned Type NumberType integer NumberType BaseDataType BaseDataType

Note that there are special cases when the ACCUM function returns some specific values, depending on whether accumulation was performed or not - this section will not treat these cases, but will explain only how this statement gets converted; please see the next section, Block Behavior for Accumulators, for details on how the aggregated result gets updated in different phases of block execution.

Example 1:

def var i as int init 0.
...
repeat i = 1 to 3:
   message string(accum total i).
   accum i (total).
end.
...

Conversion result:

EXPRESSION EXECUTION ERROR:
---------------------------
throwException("Unmatched expression in ACCUM function", this)
^  { Unmatched expression in ACCUM function [KW_TOTAL id <12884901944> 4:15] }

Details:

In this case, even if the ACCUM function and ACCUMULATE statement are in the same block, the accumulator is not used in an ACCUMULATE statement or DISPLAY with aggregate-phrase prior the ACCUM function call - thus, conversion fails with the noted errors. 4GL compilation error is not known at the time of this writing.

Example 2:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (total).
   message string(accum total i).
end.
...

Converted code:

TotalAccumulator accumTotal0 = new TotalAccumulator(i);
...
repeatTo("loopLabel0", toClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      message(valueOf((integer) accumTotal0.getResult()));
   }
});

Details:

In this example, accumulation is performed on each iteration of the REPEAT block. The MESSAGE statement will display the aggregated result, on each iteration. Note how the ACCUM function converts to a getResult() method call.

Example 3:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (total).
   display i accum total i with frame f1.
end.
...

Converted code:

TotalAccumulator accumTotal0 = new TotalAccumulator(i);
...
repeatTo("loopLabel1", toClause1, new Block()
{
   public void init()
   {
      f1Frame.openScope();
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();

      FrameElement[] elementList0 = new FrameElement[]
      {
         new Element(i, f1Frame.widgeti()),
         new Element((integer) accumTotal0.getResult(), f1Frame.widgetExpr2())
      };

      f1Frame.display(elementList0);
   }
});

Details:

When the ACCUM function is used with the DISPLAY statement, the result will be displayed in a special widget, for which the default label will be determined using the same rules as described in the DISPLAY with Aggregate Phrase section. As in the previous case, the conversion rules emit the same construct for the ACCUM function call - a getResult() method call. Also, the widget to which the accumulated result is displayed via the ACCUM function call follows the same rules as the a DISPLAY statement's normal widgets - see the DISPLAY Statement chapter for details.

Example 4:

...
for each book
break by book.publisher:
   accum book.sold-qty (total by book.publisher).
   if last-of(book.publisher)
     then display book.publisher accum total by book.publisher book.sold-qty.
end.
...

Converted code:

forEach("loopLabel2", new Block()
{
   PresortQuery query0 = null;

   FieldReference byExpr0 = new FieldReference(book, "publisher");

   FieldReference fieldRef0 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal1 = new TotalAccumulator(fieldRef0);

   public void init()
   {
      frame0.openScope();
      RecordBuffer.openScope(book);
      query0 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query0.enableBreakGroups();
      query0.setNonScrolling();
      query0.addSortCriterion(byExpr0);
      query0.addAccumulator(accumTotal1);
      accumTotal1.addBreakGroup(byExpr0);
      accumTotal1.reset();
   }

   public void body()
   {
      query0.next();
      accumTotal1.accumulate();
      if ((query0.isLastOfGroup(byExpr0)).booleanValue())
      {
         FrameElement[] elementList1 = new FrameElement[]
         {
            new Element(new FieldReference(book, "publisher"), frame0.widgetPublisher()),
            new Element((integer) accumTotal1.getResult(byExpr0), frame0.widgetExpr4())
         };

         frame0.display(elementList1);
      }
   }
});

Details:

When the BY break-group clause is used, the ACCUM function will retrieve the current aggregated result only for the specified break group - see how the getResult method gets as parameter the break group reference byExpr0. In this case, the FOR EACH loop will display the sold quantity for each publisher (and each publisher will appear only once, as the LAST-OF condition is used). Also, as a full accumulator is used, no setSubOnly call is emitted.

Example 5:

...
for each book
break by book.publisher:
   accum book.sold-qty (total by book.publisher).
   display book.publisher accum total book.sold-qty.
end.
...

Converted code:

forEach("loopLabel3", new Block()
{
   PresortQuery query1 = null;

   FieldReference byExpr1 = new FieldReference(book, "publisher");

   FieldReference fieldRef1 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal2 = new TotalAccumulator(fieldRef1);

   public void init()
   {
      frame1.openScope();
      RecordBuffer.openScope(book);
      query1 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query1.enableBreakGroups();
      query1.setNonScrolling();
      query1.addSortCriterion(byExpr1);
      query1.addAccumulator(accumTotal2);
      accumTotal2.addBreakGroup(byExpr1);
      accumTotal2.reset();
   }

   public void body()
   {
      query1.next();
      accumTotal2.accumulate();

      FrameElement[] elementList2 = new FrameElement[]
      {
         new Element(new FieldReference(book, "publisher"), frame1.widgetPublisher()),
         new Element((integer) accumTotal2.getResult(), frame1.widgetExpr6())
      };

      frame1.display(elementList2);
   }
});

Details:

This is similar as the previous example, except that, even if the ACCUMULATE statement has a BY clause, the ACCUM function does not - in this case, the ACCUM function will return the current aggregated result. The goal of this example is to prove that if the BY clause appears with the ACCUMULATE statement, it is not mandatory to be used with the ACCUM function too, when a full accumulator is used.

Example 6:

...
for each book
break by book.publisher:
   accum book.sold-qty (sub-total by book.publisher).
   if last-of(book.publisher)
     then display book.publisher accum sub-total by book.publisher book.sold-qty.
end.
...

Converted code:

forEach("loopLabel4", new Block()
{
   PresortQuery query2 = null;

   FieldReference byExpr2 = new FieldReference(book, "publisher");

   FieldReference fieldRef2 = new FieldReference(book, "soldQty");

   final TotalAccumulator accumTotal3 = new TotalAccumulator(fieldRef2);

   public void init()
   {
      frame2.openScope();
      RecordBuffer.openScope(book);
      query2 = new PresortQuery(book, (String) null, null, "book.publisher asc");
      query2.enableBreakGroups();
      query2.setNonScrolling();
      query2.addSortCriterion(byExpr2);
      query2.addAccumulator(accumTotal3);
      accumTotal3.setSubOnly();
      accumTotal3.addBreakGroup(byExpr2);
      accumTotal3.reset();
   }

   public void body()
   {
      query2.next();
      accumTotal3.accumulate();
      if ((query2.isLastOfGroup(byExpr2)).booleanValue())
      {
         FrameElement[] elementList3 = new FrameElement[]
         {
            new Element(new FieldReference(book, "publisher"), frame2.widgetPublisher()),
            new Element((integer) accumTotal3.getResult(byExpr2), frame2.widgetExpr8())
         };

         frame2.display(elementList3);
      }
   }
});

Details:

Here, as the SUB-TOTAL version of the TOTAL aggregator is used and also there is a BY break-group clause in both the ACCUM statement and function, this means this is a real sub-accumulator so a setSubOnly call is emitted. Also, note that the converted ACCUM function call gets as parameter the break group instance, byExpr2.

Example 7:

...
for each book
break by book.publisher:
   accum book.sold-qty (total).
   if last-of(book.publisher)
     then display book.publisher accum total by book.publisher book.sold-qty.
end.
...

Converted code:

n/a

Details:

4GL does not allow the BY clause in the ACCUM function if there is no BY clause in the ACCUMULATE statement - in such cases, the 4GL code compilation fails. Note that even if the conversion of these cases passes, this will result in runtime errors as the accumulator is not aware of the break group specified with the ACCUM function, when the result is retrieved - so, the getResult call for such cases will fail at runtime with an exception.

Block Behavior for Accumulators

4GL restricts the ACCUMULATE statements to be used only in blocks which have the loop property, as specified in the ACCUMULATE Statement section. The good news is that 4GL follows the same rules when an ACCUMULATE statement or DISPLAY with aggregate phrase is used, regardless of the kind of block used (only restriction is to be one of the blocks mentioned above). It gets complicated in cases when an accumulator aggregates its data in both a parent and a nested block, as the child block is not aware (to some extent) of the accumulation performed in the parent block. When retrieving the accumulated result in the child block, this will not include the aggregated data in the parent block and once the child block is finished, the accumulated result in the parent block will be updated with the accumulated data from the child block. Although this behavior is straightforward at a first glance, extensive testing proved that there are other hidden rules when an accumulator aggregates data in nested blocks, which affect the aggregated result reported after a nested block was executed (or not).

To understand how the accumulators behave, first we will introduce a few rules. In these rules, when a block is mentioned, it is assumed that the block has the loop property and has a direct reference to the accumulator (i.e. the accumulator reference is not nested within a child block). Also, when ACCUMULATE statement (or accumulation) is mentioned, it is to be assumed that the same behavior applies to the DISPLAY with aggregate phrase too.

  • if the aggregated result is retrieved inside the external procedure but before any block which accumulates data is initialized (i.e. the block's init() method is never executed), the result will be the unknown value. This value will be returned by the ACCUM function until (at least) a block's init() method is executed.
  • if the init() method was executed and the aggregated result is retrieved before any accumulation was performed in this block or in any inner blocks which accumulate data for the same accumulator, the aggregated result will be set to the accumulated value as it was before executing the block's init() method call. This applies for all iterations - if no accumulation is yet performed in this iteration, the aggregated result returned by the ACCUM function will be the value as it was prior this block's execution.
  • once the block in which the accumulation is executed is finished, the reported accumulated result will be the total accumulated result (for this block and all inner blocks). Here there are some sub-cases, depending on how the block was finished:
  • if the block's init() method was executed (the block was initialized) but it's body() method was never executed (thus no accumulation was performed), the reported result will be a distinct, reset value, depending on the aggregator type:
    table{margin-left:auto;margin-right:auto}.
    Aggregator AVG,SUB-AVG COUNT,SUB-COUNT TOTAL,SUB-TOTAL MIN,SUB-MIN MAX,SUB-MAX
    Reset Value 0 0 0 ? ?
  • if the block's body() was executed and accumulation was performed, then the accumulated value will be the aggregated result.
  • if the block's body() was executed but no accumulation was performed, then the accumulated value will be the reset value.
  • once a child block which had accumulated data for this accumulator is finished, any ACCUM function call (after the child block has finished) will return the accumulated result from the child block, even if the parent block has accumulated data between the child block end and the ACCUM function call time frame.

For all these cases, the conversion rules will emit an Accumulator.reset() call in the Block.init() method for the block which directly references an ACCUMULATE statement and for all its parent blocks which also have the loop property. This will reset the accumulator upon block entrance, leaving the other rules (related to backing up and returning the accumulated value prior the entrance of the block, inner block updates aggregated result for all parent blocks, etc) to the FWD runtime. For this, the FWD runtime will register the accumulator for scope notification (it will be notified each time a new block is entered, iterated, retried or finished) and also for undo notification (to roll back the accumulated data in this iteration, if the iteration is rolled back).

The following examples are split into three sections:

  • examples from 1 to 11 show how the accumulators behave when accumulation is performed while in a nested block and what happens if a nested block (which contains an ACCUMULATE statement) does not execute
  • examples from 12 to 23 show how the accumulated result is affected when the IF statement stops the execution of a nested block which contains an ACCUMULATE statement.
  • example 24 shows how the accumulator behave in recursive calls.

Example 1:

def var i as int init 0.
...
if i < 0
then
   do transaction:
      accum i (total).
   end.
message accum total i.
repeat i = 1 to 3:
   message accum total i.
   accum i (total).
end.
...

Converted code:

...
if (_isLessThan(i, 0))
{
   doBlock(TransactionType.FULL, "blockLabel0", new Block()
   {
      public void init()
      {
         accumTotal0.reset();
      }

      public void body()
      {
         accumTotal0.accumulate();
      }
   });
}
message((integer) accumTotal0.getResult());

...
repeatTo("loopLabel0", toClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
   }
});
...

Details:

In this example, the accumulator's value before any accumulated result will be unknown - so the first MESSAGE statement will display the unknown value, ?. Once it enters the REPEAT block, although the accumulator is reset, the actual reset will not be executed until the first accumulation or the block is exit - its reset is marked as “pending”. Thus, the second MESSAGE statement within the REPEAT block will always display the unknown value (as this was the accumulator's value prior the entrance of this block).

The i < 0 statement was added because 4GL doesn't allow the ACCUM function to be executed if the accumulator is not yet defined (there is no ACCUMULATE statement in the code prior to the ACCUM function call) - this is just a dummy code to define the accumulator.

Example 2:

def var i as int init 0.
def var n as int.
...
n = 0.
repeat i = 1 to n:
   accum i (total).
   ...
end.
message accum total i.
...

Converted code:

...
n.assign(0);
...
repeatTo("loopLabel1", toClause1, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      ...
   }
});

message((integer) accumTotal0.getResult());
...

Details:

Here, the REPEAT block is initialized, but its body is never executed - in this case, the block initialization will reset the accumulator, so the MESSAGE statement will display its reset value.

Example 3:

def var i as int init 0.
...
repeat i = 1 to 3:
   accum i (total).
   ...
end.
message accum total i.
...

Converted code:

...
ToClause toClause2 = new ToClause(i, 1, 3);

repeatTo("loopLabel2", toClause2, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      ...
   }
});

message((integer) accumTotal0.getResult());
...

Details:

The difference between this and example №2 is that the REPEAT block's body and accumulation actually gets executed - is such case, the result shown by the MESSAGE statement will be the actual aggregated result.

Example 4:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   B2:
   repeat j = 1 to 3:
      accum i (total).
   end.
end.
...

Converted code:

...
repeatTo("b1", toClause3, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      repeatTo("b2", toClause4, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            accumTotal0.accumulate();
         }
      });
   }
});
...

Details:

Although block B1 has a nested ACCUMULATE statement, this reference is not directly enclosed in block B1; instead, is directly enclosed in block@ B2 , but, as B1 nests B2 , the Accumulate.reset() call will be emitted for both blocks.

Example 5:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   accum i (total).
   ...
   message accum total i.
   B2:
   repeat j = 1 to 3:
      ... /* this accumulator is not used here (others may be used) */
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause5, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      ...
      message((integer) accumTotal0.getResult());

      repeatTo("b2", toClause6, new Block()
      {
         public void body()
         {
            ... /* this accumulator is not used here (others may be used) */
         }
      });

      message((integer) accumTotal0.getResult());
   }
});
...

Details:

Both MESSAGE statements will display the same result, as block B2 (regardless if it's executed or not) does not use this accumulator, so it will not reset it - no Accumulator.reset() call is emitted for block B2.

Example 6:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   accum i (total).
   ...
   message accum total i.
   B2:
   repeat j = 1 to 3:
      ... /* no accumulation is performed in this block  */
      message accum total i.
      ...
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause7, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      accumTotal0.accumulate();
      ...
      message((integer) accumTotal0.getResult());

      repeatTo("b2", toClause8, new Block()
      {
         public void body()
         {
            ... /* no accumulation is performed in this block  */
            message((integer) accumTotal0.getResult());
            ...
         }
      });

      message((integer) accumTotal0.getResult());
   }
});
...

Details:

All MESSAGE statements will display the same result, as block B2 (regardless if it's executed or not) does not perform any accumulation for this accumulator (only the ACCUM function is used), so it will not reset it - no Accumulator.reset() call is emitted for block B2.

Example 7:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   accum i (total).
   B2:
   repeat j = 1 to 3:
      message accum total i.
      accum i (total).
   end.
end.
message accum total i.
...

Converted code:

...
repeatTo("b1", toClause9, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      accumTotal0.accumulate();

      ...
      repeatTo("b2", toClause10, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            message((integer) accumTotal0.getResult());
            accumTotal0.accumulate();
         }
      });
   }
});

message((integer) accumTotal0.getResult());
...

Details:

Here, both B1 and B2 blocks accumulate data for the i (total) accumulator, thus Accumulate.reset() calls were added to the init() method of both blocks. The first MESSAGE statement will always display the accumulated result as it was before B2's init() method was executed (which is the accumulated result as in current B1 iteration), while the last MESSAGE statement will display the accumulated result from both blocks (as any data accumulated in a child block will be accumulated in all parent blocks which reference the same accumulator).

Example 8:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   accum i (total).
   B2:
   repeat j = 1 to 3:
      message accum total i.
      accum i (total).
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause11, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      accumTotal0.accumulate();
      ...
      repeatTo("b2", toClause12, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            message((integer) accumTotal0.getResult());
            accumTotal0.accumulate();
         }
      });

      message((integer) accumTotal0.getResult());
   }
});
...

Details:

This example is the same as №8, except the second MESSAGE statement will always display the accumulated result from block B2.

Example 9:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   accum i (total).
   message accum total i.
   B2:
   repeat j = 1 to 3:
      ...
      accum i (total).
      ...
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause13, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
      ...
      repeatTo("b2", toClause14, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            ...
            accumTotal0.accumulate();
            ...
         }
      });

      message((integer) accumTotal0.getResult());
   }
});
...

Details:

In this case, first MESSAGE statement will display the accumulated result for the data in the current and all prior B1 and B2 iterations, while the second MESSAGE statement will always display the accumulated result from block B2.

Example 10:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   B2:
   repeat j = 1 to 3:
      ...
      accum i (total).
      ...
   end.
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause15, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      repeatTo("b2", toClause16, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            ...
            accumTotal0.accumulate();
            ...
         }
      });

      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

Both MESSAGE statements will always display the accumulated result from block B2 since, once an inner block accumulated data for this accumulator, calling the ACCUM function in the parent block will result in reporting the accumulated result from the child block, even if accumulation is performed in the parent block. The accumulated result reported in the parent block will change only if another inner block which uses the accumulator is executed.

Example 11:

def var i as int init 0.
def var j as int init 0.
def var n as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   B2:
   repeat j = 1 to n:
      ...
      accum i (total).
      ...
   end.
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause17, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      repeatTo("b2", toClause18, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            ...
            accumTotal0.accumulate();
            ...
         }
      });

      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

This example is similar to №10, with the single difference that the B2's body is never executed; regardless, the accumulated result displayed by the two MESSAGE statements is the same - they will display the accumulator's reset value, as B2's header was executed.

IF Statement

The examples above proved how accumulation works when data is aggregated in single or nested blocks or when the block which is aggregating data is not executed. But, there are cases in which the nested block is not initialized at all, but the aggregated result is affected. This includes cases when the block is not executed because, after the condition of an IF statement has been evaluated, the code path which had a nested accumulation reference did not get executed, i.e. an ACCUMULATE statement nested in block/IF/THEN/[block]/ACCUMULATE structure did not executed because the IF condition evaluated to false (here, block is a block with the loop property - this excludes the external procedure, internal procedure, function or trigger). In these cases, when the IF's enclosed accumulation is not executed, the accumulator has its result set to either the reset or the unknown value, depending on some rules:

  • when the ACCUMULATE statement is nested in a code path like block/IF/THEN/ACCUMULATE or block/IF/ELSE/ACCUMULATE(there is no block with loop property between the IF and the inner ACCUMULATE statement) and the accumulation is never performed (as after the IF condition is evaluated, the branch which has no ACCUMULATE statement is executed), the result of this accumulator will be set to its reset value, when retrieved after the IF (and its THEN/ELSE branch) has finished.
  • when the ACCUMULATE statement is called in cases like block/IF/THEN/block/ACCUMULATE block/IF/ELSE/block/ACCUMULATE(there is a block with loop property between the IF and the inner ACCUMULATE statement) and the accumulation is never performed (as after the IF condition is evaluated, the branch which doesn't have the inner ACCUMULATE statement is executed), the result of this accumulator will be set to unknown, when retrieved after the IF (and its THEN/ELSE branch) has finished. More, once the runtime decided that it needs to report the unknown value for this accumulator as this rule was applied, the unknown value will be reported for the remainder part of this block, until the next iteration - regardless if accumulation will be performed or not in this or any child blocks.

For these two cases, the converted code will enclose the IF statement between try ... finally statements and will set some special flags within the accumulator, to let it know it will enter a block which may accumulate data and it should act accordingly if it does not accumulate the data. The converted code for such IF statements looks like (assuming either the IF's THEN or ELSE branch contain at least one reference to the accum accumulator):

4GL code:

if (condition)
then ...
else ...

Converted code:

try
{
   accum.postpone...();
   if (condition)
   {
      ...
   }
   else
   {
      ...
   }
}
finally
{
   accum.ifEnded();

}

where accum.postpone...() will be either accum.postponeReset() (when the accumulator's result must be set to its reset value if no accumulation was performed, once the IF has finished - the first condition above) or accum.postponeUnknown() (when the accumulator's result must be set to unknown if no accumulation was performed, once the IF has finished - the second condition above). Following examples will explain in more detail how the IF statement affects the accumulators:

Example 12:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   B2:
   repeat j = 1 to 3:
      ...
      if i < 0
         then accum i (total).
      ...
   end.
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause0, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      repeatTo("b2", toClause1, new Block()
      {
         public void init()
         {
            accumTotal0.reset();
         }

         public void body()
         {
            ...
            try
            {
               accumTotal0.postponeReset();
               if (_isLessThan(i, 0))
               {
                  accumTotal0.accumulate();
               }
            }
            finally
            {
               accumTotal0.ifEnded();
            }
            ...
         }
      });

      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

Here, both MESSAGE statements still display the same result because, even block B2 is executed, the inner ACCUMULATE statement is not - so, the accumulation result will be the reset value (done when executing B2's header).

Example 13:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   if i < 0
      then accum i (total).
   ...
end.
message accum total i.
...

Converted code:

...
repeatTo("b1", toClause2, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      try
      {
         accumTotal0.postponeReset();
         if (_isLessThan(i, 0))
         {
            accumTotal0.accumulate();
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      ...
   }
});

message((integer) accumTotal0.getResult());
...

Details:

Although this is similar to previous example, this proves that if the block is executed but no accumulation is performed, then the accumulator is still reset even if the block is a child of the external procedure (thus the MESSAGE statements will display the reset value).

Example 14:

def var i as int init 0.
def var j as int init 0.
...
if i < 0
  then accum i (total).
...
message accum total i.

Converted code:

EXPRESSION EXECUTION ERROR:
---------------------------
exprNames.addEntry(false, eKey, javaname)
          ^  { java.util.NoSuchElementException }

Details:

This is not a valid 4GL code, as the ACCUMULATE statement is scoped directly to the external procedure (which doesn't have loop property).

Example 15:

def var i as int init 0.
def var j as int init 0.
...
if i < 0
then
   do transaction:
      accum i (total).
   end.
...
message accum total i.

Converted code:

...
if (_isLessThan(i, 0))
{
   doBlock(TransactionType.FULL, "blockLabel0", new Block()
   {
      public void init()
      {
         accumTotal0.reset();
      }

      public void body()
      {
         accumTotal0.accumulate();
      }
   });
}
message((integer) accumTotal0.getResult());
...

Details:

In this case, even if the 4GL code is valid, the conversion rules do not bracket the converted IF statement between a try ... finally clause, as the IF is not also scoped to a block with loop property. The MESSAGE statement will display the unknown value.

Example 16:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   message accum total i.
   accum i (total).
   ...
   if i < 0
     then accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause3, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      ...
      try
      {
         accumTotal0.postponeReset();
         if (_isLessThan(i, 0))
         {
            accumTotal0.accumulate();
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the first “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeReset() call is emitted. Also, this case proves that the accumulated result reported by the last MESSAGE statement will be not be the reset value, as accumulation was already performed.

Example 17:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   if i < 0
     then accum i (total).
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause4, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      try
      {
         accumTotal0.postponeReset();
         if (_isLessThan(i, 0))
         {
            accumTotal0.accumulate();
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the first “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeReset() call is emitted. Also, this case proves that even if no accumulation is performed, once the IF statement has passed, the ACCUM function will return the reset value until accumulation is performed.

Example 18:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   message accum total i.
   accum i (total).
   if i > 0
   then do:
      message accum total i.
      accum i (total).
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause5, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      try
      {
         accumTotal0.postponeReset();
         if (_isGreaterThan(i, 0))
         {
            message((integer) accumTotal0.getResult());
            accumTotal0.accumulate();
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the first “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeReset() call is emitted. Also, this case proves that the IF statement doesn't always reset the accumulator, as the second MESSAGE statement will display the current accumulated result.

Example 19:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   message accum total i.
   accum i (total).
   ...
   if i < 0
   then
      B2:
      do transaction:
         accum i (total).
      end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause6, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      ...
      try
      {
         accumTotal0.postponeUnknown();
         if (_isLessThan(i, 0))
         {
            doBlock(TransactionType.FULL, "b2", new Block()
            {
               public void init()
               {
                  accumTotal0.reset();
               }

               public void body()
               {
                  accumTotal0.accumulate();
               }
            });
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the second “postpone on IF” condition is encountered, the IF statement is bracketed with a try … finally block and a postponeUnknown() call is emitted. Also, this case proves that even if no accumulation is performed by the IF's inner block, once the IF statement has passed, the ACCUM function will not return the unknown value, as accumulation was already performed.

Example 20:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   if i < 0
   then
      B2:
      do transaction:
         accum i (total).
      end.
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause7, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      try
      {
         accumTotal0.postponeUnknown();
         if (_isLessThan(i, 0))
         {
            doBlock(TransactionType.FULL, "b2", new Block()
            {
               public void init()
               {
                  accumTotal0.reset();
               }

               public void body()
               {
                  accumTotal0.accumulate();
               }
            });
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the second “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeUnknown() call is emitted. This example also shows that the first MESSAGE statement will never display the accumulated value as before the B1 block has initialized, but instead will always display the unknown value.

Example 21:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   message accum total i.
   accum i (total).
   ...
   if i > 0
   then
      B2:
      do transaction:
         accum i (total).
      end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause8, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      ...
      try
      {
         accumTotal0.postponeUnknown();
         if (_isGreaterThan(i, 0))
         {
            doBlock(TransactionType.FULL, "b2", new Block()
            {
               public void init()
               {
                  accumTotal0.reset();
               }

               public void body()
               {
                  accumTotal0.accumulate();
               }
            });
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the second “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeUnknown() call is emitted. This example also shows that the last MESSAGE statement will not display the unknown value, but instead will display the accumulated value from block B2(as the IF condition evaluated to true and block B2 was executed).

Example 22:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   if i > 0
   then
      B2:
      do transaction:
         accum i (total).
      end.
   message accum total i.
   accum i (total).
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause9, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      try
      {
         accumTotal0.postponeUnknown();
         if (_isGreaterThan(i, 0))
         {
            doBlock(TransactionType.FULL, "b2", new Block()
            {
               public void init()
               {
                  accumTotal0.reset();
               }

               public void body()
               {
                  accumTotal0.accumulate();
               }
            });
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the second “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeUnknown() call is emitted. This example also shows that the first MESSAGE statement will not display the unknown value, but instead will display the accumulated value from block B2 (as the IF condition evaluated to true and block B2 was executed).

Example 23:

def var i as int init 0.
def var j as int init 0.
...
B1:
repeat i = 1 to 3:
   ...
   message accum total i.
   accum i (total).
   if i > 0
   then do:
      message accum total i.
      B2:
      do transaction:
         accum i (total).
      end.
   end.
   message accum total i.
end.
...

Converted code:

...
repeatTo("b1", toClause10, new Block()
{
   public void init()
   {
      accumTotal0.reset();
   }

   public void body()
   {
      ...
      message((integer) accumTotal0.getResult());
      accumTotal0.accumulate();
      try
      {
         accumTotal0.postponeUnknown();
         if (_isGreaterThan(i, 0))
         {
            message((integer) accumTotal0.getResult());

            doBlock(TransactionType.FULL, "b2", new Block()
            {
               public void init()
               {
                  accumTotal0.reset();
               }

               public void body()
               {
                  accumTotal0.accumulate();
               }
            });
         }
      }
      finally
      {
         accumTotal0.ifEnded();
      }
      message((integer) accumTotal0.getResult());
   }
});
...

Details:

As the second “postpone on IF” condition is encountered, the IF statement is bracketed with a try ... finally block and a postponeUnknown() call is emitted. This example also shows that the second MESSAGE statement will display the current accumulated result (thus the IF doesn't always set the accumulator to unknown).

Recursive Calls

One behavior which was not yet presented is how accumulators behave during recursions; testing showed that the accumulator's value is not inherited from recursive calls (be it procedure, function or trigger). Instead, the accumulator is isolated in each recursive call and, once the recursive call has ended, any accumulated data in the current call is not propagated down the stack, as shown in the following example:

Example 24:

def var i as int init 0.
...
procedure proc1.
  def input parameter p as int.
  do transaction:
     accum i (count).
     if p <> 0
       then run proc1(p - 1).
  end.
  put screen row p + 1 string(p) + " " + string(accum count i).
end.
run proc1(10).
...

Converted code:

integer i = new integer(0);

CountAccumulator accumCount0 = new CountAccumulator();

...
proc1(new integer(10));
...

public void proc1(final integer _p)
{
   internalProcedure(new Block()
   {
      integer p = new integer(_p);

      public void init()
      {
         TransactionManager.registerUndo(p);
      }

      public void body()
      {
         doBlock(TransactionType.FULL, "blockLabel0", new Block()
         {
            public void init()
            {
               accumCount0.reset();
            }

            public void body()
            {
               accumCount0.accumulate();
               if (_isNotEqual(p, 0))
               {
                  proc1(minus(p, 1));
               }
            }
         });

         putScreen(concat(valueOf(p), " ", valueOf(accumCount0.getResult())), plus(p, 1), 1);
      }
   });
}

Details:

In this case, the ACCUM function will always return the same value and there is no special construct emitted by the conversion rules. Also, this proves that accumulators which are used inside a procedure/function are alive and can be used only as long as the procedure is executed; this means that the accumulator is not scoped to the root external procedure, but is scoped to the internal procedure or function.


© 2004-2017 Golden Code Development Corporation. ALL RIGHTS RESERVED.