Project

General

Profile

Blocks

A block represents the basic 4GL feature which structures or groups code. There are two major types of block: top level blocks and inner blocks. A block groups code for several purposes:

  • flow of control
  • transaction processing
  • scoping related operations (certain 4GL resources such as buffers or frames have behavior that depends on the entry, iteration and exit from the block to which they are associated or "scoped")

This chapter will discuss the Java replacement for block processing, block properties, conditions and related facilities. For details on how the control-flow features convert, please see the Control Flow chapter. For details on transaction processing and UNDO, see the Transactions chapter. For details on query-related semantics, see the chapters in Part 5.

Introduction

In the 4GL, code can be structured into related sections called blocks. In Progress v9, the following types of blocks are available:

Block Type Supported
external procedure top-level Yes
internal procedure top-level Yes
user-defined functions top-level Yes
trigger top-level Yes
DO inner Yes
FOR inner Yes
EDITING inner Yes
REPEAT inner Yes

None of the block types introduced after Progress v9 are supported at this time.

After conversion, the information for each block is split into a "header" and a "body". All code that is executed in a block is contained in the block body. The description of the block itself and any explicitly controlled options are defined in the block header.

The lines of code in a block's body are executed "top to bottom". All code in the same block will share the same behavior in terms of transaction processing. Absent any explicit changes to the flow of control (by language statements that cause a flow of control change) or the generation of a condition (such as an ERROR) which will cause the flow of control to change, all code in a given block is executed (if the block body is entered) or all the code is not executed (if the block body is not entered).

Some blocks can be made to iteratively execute the code contained in the block body. Such blocks are known as looping blocks. Progress provides blocks that can explicitly loop based on programmer controlled expressions in the block header. All of these looping blocks are supported in the converted code.

Blocks support transaction processing which is the ability to undo (reverse) edits to variables or to the database which occurred within the scope of that block, when a condition is raised or when an explicit UNDO statement is encountered. Some blocks can have their support for transactions explicitly coded via the TRANSACTION keyword in the block header.

All blocks except for the DO block have some default behavior when abnormal conditions occur. Inner blocks (except for EDITING) allow that behavior to be explicitly specified via ON phrases in the block header.

Some blocks provide retry support which is the re-execution of a block with the same (original) state as the optional action in response to an abnormal condition. The support for retry is not limited to looping blocks. Even non-looping blocks can support retry.

Some blocks can take parameters and be called explicitly (external procedures, internal procedures and user-defined functions) or are otherwise invoked as a callback (triggers). Triggers are special in that they are top-level blocks but they can only be invoked indirectly in response to an event (usually associated with the user interface but that is not required) that has occurred. In this document, such blocks are called "top-level blocks".

Other blocks (all forms of DO, REPEAT, FOR and EDITING) are not top-level blocks and as such can only be contained inside another block (top-level or otherwise). In this document, such blocks are called "inner blocks". Inner blocks are the only kind of block that can be labeled and that label can then be referenced in language statements that explicitly change the flow of control such as UNDO statement, LEAVE statement, NEXT statement and in ON phrases.

Some blocks can have their definitions made nested inside another block. The top level blocks (except for triggers) cannot be nested. Inner blocks can be nested. Do not confuse nesting with recursion. The top level blocks can be called recursively (they can call themselves directly or indirectly causing multiple instantiations on the call stack). But the top level blocks cannot have their definitions made on a nested basis. For example, an external procedure cannot be defined inside another external procedure.

The following summarizes these behaviors:

Block Type Looping Default Transaction Level TRANSACTION Option ON Phrases Retry Support Parameters Callable (Via Name) Labeled Nestable
external procedure no sub-transaction no no yes yes yes (filename in a RUN statement) no no
internal procedure no sub-transaction no no yes yes yes (RUN statement) no no
function no sub-transaction no no yes yes yes (in an expression) no no
trigger no sub-transaction no no yes no no no yes
DO no no transaction yes yes yes (depending on options) no no yes yes
DO TO
DO WHILE
DO TO WHILE
yes no transaction yes yes yes (depending on options) no no yes yes
REPEAT (all forms) yes sub-transaction yes yes yes no no yes yes
FOR
FOR FIRST
FOR LAST
no (unless there is an additional table defined with EACH table) sub-transaction yes yes yes no no yes yes
FOR EACH yes sub-transaction yes yes yes no no yes yes
EDITING yes sub-transaction no no yes no no yes yes (editing blocks for other frames can be directly contained and any kind of editing block can be indirectly contained)

Block Conversion

The conversion process in FWD will map each encountered block to an API in the BlockManager class. This class is used by FWD to provide a 4GL-specific runtime implementation for any Progress block. Basically, it provides an external API that allows the Progress-compatible code block semantics to be provided with a minimum of code in the caller. This class encodes the "scaffolding" necessary to properly process code blocks with the right behavior. The external interface is designed to be statically imported to make access simple, short and easy to read. The API has been designed to minimize name conflicts. The scaffolding code uses the TransactionManager extensively to implement the proper behavior.

There is an exception to the rule presented above - if a looping block can be optimized to use directly the Java looping statements, the BlockManager APIs will be bypassed and a compatible Java statement will be emitted instead. Please see the Control Flow chapter of this book for when and how this is done.

The actual 4GL code for each block will be emitted in the code of an anonymous class which extends the Block abstract class. Any runtime API in the BlockManager class which defines a 4GL block will take as parameter a Block instance, with the 4GL code to be executed. Note that both classes mentioned here and all other block-related classes are part of the com.goldencode.p2j.util package. As any converted program uses at least one or more APIs in BlockManager, the methods in this class will be statically imported in each and every converted program.

The exported APIs from the BlockManager class will be presented together with each 4GL block in the next sections. Here, we will focus on the Block abstract class: when and why its methods are overridden.

The Block abstract class allows the user to override the following methods, each having its own purpose in the final converted code: Block.init(), Block.enter() and Block.body(). Although only the body() method in this class is abstract, the other defined methods have an empty body; this approach was chosen so that each anonymous class will be able to override the init() or enter() method as needed, and not force its implementation, with an empty body.

Block.init()

Provides a "callback" to initialize state once before the body gets executed. This is guaranteed to be called AFTER the transaction manager scope opens but BEFORE the block body is ever entered. It will only be called once no matter whether the block iterates or whether there is a retry.

All block-specific fields generated during conversion (to help maintain the state of this block) will be emitted as fields declared in the anonymous class which holds the 4GL code. Mainly, here will be added the code to register record buffers, frames and variables for scope processing, initialize queries and other block-related state.

Block.enter()

This method is emitted in the converted code only in some special cases of the FOR block. It provides a "callback" to execute user-defined logic at the top of the block body but before the block body is executed. This is guaranteed to be called AFTER the transaction manager scope opens but BEFORE the block body is entered. It will be called once per iteration of the block or once if the block is not iterating. It WILL be called on retry.

Block.body()

This implements the delegated code to be executed as part of this instance.

Top Level Blocks

The top level blocks have some special properties as compared to the looping blocks - they can not be nested but they can be called recursively. Currently, some of the top-level-block arguments are not fully supported by FWD - they will be marked distinctly in the next sections.

External Procedures

In 4GL, an external procedure gathers all the code which exists in an physical 4GL program - the external procedure will include the 4GL code and all the buffer definitions, internal procedures, functions or triggers. The conversion process will convert each external procedure to a public Java class. This Java class will have as name the compatible, converted 4GL program name and also will define at least a public void execute() method, where its converted 4GL code will be emitted.

Each external procedure will be converted to a Java class, which will have the following structure:

package <package name>;

/* import statements */

public class <ConvertedClassName>
{
   /* field definitions */

   public void execute(/* argument list */)
   {
      externalProcedure(<transaction type>, new Block()
      {
         /* variable definitions */

         public void init()
         {
            /* state initialization code */
         }

         public void body()
         {
            /* converted code */
         }
      });
   }
}

Note that no constructor is emitted for the converted class - on invocation, the default constructor will always be used to instantiate the class. In cases when the 4GL program accepts parameters, they will be converted as parameters for the execute() method. Also, the execute() method will be emitted for all cases when the 4GL program needs to be executed by the 4GL code.

The 4GL program parameters are defined using DEFINE PARAMETER statements - please see the Variable Definitions section of the Data Types chapter of this book for details on how these statements get converted.

The BlockManager class provides APIs in which the entire 4GL program code (initialization and body) is emitted; these are:

externalProcedure(Block block)
externalProcedure(TransactionType lvl, Block block)

The single differences between them is that the first one assumes the default transaction level for the external procedure to be sub-transaction while the later will be emitted when the full transaction is scoped to the external procedure - please see the Transactions chapter of this book for more details. The block parameter represents the Block instance which acts as a delegate for the 4GL code.

Example 1:

[program file name: test1.p]
def var i as int.
... /* code */

Converted code:

package com.goldencode.testcases;

/* other imports */

import static com.goldencode.p2j.util.BlockManager.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in test1.p).
 */
public class Test1
{
   /* declared fields (queries, streams, frames, etc) */

   /**
    * External procedure (converted to Java from the 4GL source code
    * in test1.p).
    */
   public void execute()
   {
      externalProcedure(new Block()
      {
         integer i = new integer(0);

         public void init()
         {
            TransactionManager.register(i);
         }

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

Details:

This a simple program which shows how its converted code gets structured, assuming that it has no parameters.

Example 2:

[program file name: test2.p]
define input parameter txt as char.
define output parameter n1 as int.
define input-output parameter n2 as int.

def var i as int.

...

Converted code:

package com.goldencode.testcases;

/* other imports */

import static com.goldencode.p2j.util.BlockManager.*;

/**
 * Business logic (converted to Java from the 4GL source code
 * in test2.p).
 */
public class Test2
{
   /* declared fields (queries, streams, frames, etc) */

   /**
    * External procedure (converted to Java from the 4GL source code
    * in test2.p).
    */
   public void execute(final character _txt, final integer n1, final integer n2)
   {
      externalProcedure(new Block()
      {
         character txt = new character(_txt);

         integer i = new integer(0);

         public void init()
         {
            TransactionManager.register(txt, n1, n2, i);
            n1.assign(new integer(0));
         }

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

Details:

This example shows how the code gets structured when parameters are involved - depending on the type of parameter (input, output or input-output), the parameters might have an associated instance field with the Block implementation class (the txt field for the _txt parameter, in this case).

Internal Procedures

The syntax of a 4GL internal procedure is:

PROCEDURE proc-name
[ EXTERNAL "dllname" 
   [ CDECL | PASCAL | STDCALL ]
   [ ORDINAL n ]
   [ PERSISTENT ]
]
[ PRIVATE ]
[ IN SUPER ]:

   [ DEFINE PARAMETER ... ], ...

END.

where only the PRIVATE optional setting is supported. If an internal procedure has one of the other settings, although it will pass conversion, the settings will be ignored and they will not have an equivalency in the converted code.

The parameters for the internal procedure are defined using DEFINE PARAMETER statements - please see the Variable Definitions section of the Data Types chapter of this book.

Each internal procedure will always be converted to an instance method for the converted external procedure. The emitted code will have the following structure:

<access modifier> void <ConvertedProcedureName>(/* argument list */)
{
   internalProcedure(<transaction type>, new Block()
   {
      /* variable definitions */

      public void init(/* argument list */)
      {
         /* state initialization code */
      }

      public void body()
      {
         /* converted code */
      }
   });
}

where <access modifier> defaults to public and should be set to private only when the PRIVATE option is set in the 4GL procedure definition (note that the PRIVATE option is not currently supported). Except this, its internal structure is similar to how the execute() method for the external procedure is emitted.

The BlockManager APIs which emulate the 4GL internal procedure and where its converted 4GL code is emitted are:

internalProcedure(Block block)
internalProcedure(TransactionType lvl, Block block)

where, similar to the external procedure, the default transaction level is sub-transaction and the transaction parameter will be emitted when the full transaction is scoped to the internal procedure - please see the Transactions chapter of this book for more details.

Example 3:

procedure proc1.
   def var i as int.
   ...
end.

Converted code:

public void proc1()
{
   internalProcedure(new Block()
   {
      integer i = new integer(0);

      public void init()
      {
         TransactionManager.register(i);
      }

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

Details:

This is a simple internal procedure with no parameters.

Example 4:

procedure proc2 private.
   ...
end.

Converted code:

private void proc2()
{
   internalProcedure(new Block()
   {
      public void body()
      {
         ...
      }
   });
}

Details:

Here (even though currently the PRIVATE clause is not supported), note how the converted procedure is emitted with a private modifier, so that it can not be accessed outside the external procedure class.

User-Defined Functions

The user defined functions are similar to how internal procedures work except that it can return a specific value; also, the definition of their parameters is done in a distinct way. The syntax of such a block is:

FUNCTION function-name [ RETURNS ] data-type
[ PRIVATE ]
[ ( param1 [ , param2 ] ... ) ]

   FORWARD |
   [ MAP [ TO ] actual-name ] IN proc-handle | IN SUPER
}

where only the PRIVATE and FORWARD settings are supported. For details about how the function parameters are converted, please see the Function Parameter section of the Data Types chapter of this book.

Each user-defined function will get converted to a Java instance method defined in the external procedure class. Its converted code is emitted in a similar manner as the internal procedure is and will have the following structure:

<access modifier> <DataType> <ConvertedFunctionName>(/* argument list */)
{
   <DataType>Function(<transaction type>, new Block()
   {
      /* variable definitions */

      public void init(/* argument list */)
      {
         /* state initialization code */
      }

      public void body()
      {
         /* converted code */
      }
   });
}

where:

  • <access modifier> defaults to public and is set to private only when the PRIVATE option is set in the 4GL procedure definition.
  • <DataType> represents the FWD name of the class which is associated with the 4GL data type returned by the function.

The next table shows how, depending on the return data-type, a distinct BlockManager API is emitted for each user-defined function. For each data-type, there are two API versions: one which defaults the transaction level to sub-transaction and another one which can explicitly set the transaction level (usually emitted when the full-transaction is scoped at the function level).

Return Data-Type BlockManager API Details
character characterFunction(Block block)
characterFunction(TransactionType lvl,
Block block)
API emitted when the function returns a character value.
date dateFunction(Block block)
dateFunction(TransactionType lvl,
Block block)
API emitted when the function returns a date value.
logical logicalFunction(Block block)
logicalFunction(TransactionType lvl,
Block block)
API emitted when the function returns a logical value.
decimal decimalFunction(Block block)
decimalFunction(TransactionType lvl,
Block block)
API emitted when the function returns an decimal value.
integer integerFunction(Block block)
integerFunction(TransactionType lvl,
Block block)
API emitted when the function returns an integer value.
raw rawFunction(Block block)
rawFunction(TransactionType lvl, Block block)
API emitted when the function returns a raw value.
memptr memptrFunction(Block block)
memptrFunction(TransactionType lvl,
Block block)
API emitted when the function returns a memptr value.
handle handleFunction(Block block)
handleFunction(TransactionType lvl,
Block block)
API emitted when the function returns a handle reference.

Example 5:

function func1 returns char ().
   ...
   return ?.
end.

Converted code:

public character func1()
{
   return characterFunction(new Block()
   {
      public void body()
      {
         ...
         returnNormal(new character());
      }
   });
}

Details:

This is a simple user-defined function with no parameters. As the function returns a character value, the characterFunction API is used.

Example 6:

function func2 returns char private ().
   ...
   return ?.
end.

Converted code:

private date func2()
{
   return dateFunction(new Block()
   {
      public void body()
      {
         ...
         returnNormal(date.instantiateUnknownDate());
      }
   });
}

Details:

Here, note how the converted function is emitted with a private modifier, so that it can't be accessed outside the external procedure class. As this function returns a date value, the dateFunction API is used.

Example 7:

function func3 returns char forward.

...

function func3 returns char ().
   ...
   return ?.
end.

Converted code:

public character func3()
{
   return characterFunction(new Block()
   {
      public void body()
      {
         ...
         returnNormal(new character());
      }
   });
}

Details:

In conversion terms, if a function is declared forward, it has no impact on the converted code. As Java methods can be invoked regardless on the location of their definition (before or after the method which invokes it), the statement which declares as forward a certain function will not be emitted in the Java code.

Triggers

A trigger represents the 4GL's implementation of event handling. Currently, FWD supports only UI-related events (database events and others are not yet supported). The 4GL triggers for the UI events can be defined either using the ON statement or the TRIGGERS phrase with the DEFINE widget statement. Regular triggers are fully supported but the "inline" triggers implemented in a variable or widget definition are not implemented at this time - there is no “trigger phrase” support.

The syntax of the ON statement is:

ON event-list
{
   ANYWHERE |
   { OF widget-list [ OR event-list OF widget-list ] ... [ ANYWHERE ] }
}
{
   trigger-block |
   REVERT |
   { PERSISTENT RUN procedure [ ( input-parameters ) ] }
}

where the PERSISTENT RUN clause is not supported.

ON triggers are blocks that are executed when defined events occur in the user interface. Instead of defining triggers like internal procedures, triggers are defined "in line" with the procedure or function being processed. Though Progress treats a trigger as a distinct block type which has block properties, it is not well separated in terms of location in source code. Triggers take no parameters and are not named. Instead, they are registered as a set of events and a set of widgets which cause the trigger to be called. When any of the listed events occurs for any of the listed widgets, the trigger will be executed. Multiple triggers can be registered for the same event + widget combinations. The last registration "wins" (is active). The scoping for such registrations is at the procedure (internal/external), trigger and function level. When such a new scope is opened, duplicate trigger registrations will hide registrations from previous scopes. Likewise, the closing of a scope will remove all triggers registered in that scope, which will make previously hidden triggers "re-appear". Within a single procedure, function or trigger (yes, triggers can be nested in other triggers), duplicate registrations hide previous registrations and this will not implicitly ever be undone (without the entire scope exiting and thus removing all triggers registered in that scope). The explicit REVERT option can be used to deregister a specified trigger and then the trigger registered most recently (in this scope or a previous scope) will become the active trigger for that given event plus widget combination.

One other interesting feature of trigger registration is that it is strictly based on the flow of control of the registering code. If the flow of control never executes the branch of code in which a trigger is defined (e.g. an ELSE block that is never executed), then that trigger is not registered. This means that the registration of triggers must be kept strictly in line with the matching control flow, even if the trigger block itself is refactored into another location.

To perform trigger registration, FWD will emit calls to the following APIs in LogicalTerminal:

registerTrigger(EventList events, Class<?> trigger, Object contain)
registerTrigger(EventList events, Class<?> trigger, Object contain, boolean trans)

while the following APIs in LogicalTerminal will be used to deregister a trigger:

deregisterTrigger(EventList events)

where:

  • events represents the list of events and widgets for which this trigger executes.
  • trigger is the class of the trigger and must not be null.
  • contain is the containing object (all triggers are implemented as non-static inner classes, so they must have a reference to the containing class), which also must not be null.
  • trans is a flag which is set to true only if this trigger should start a full transaction and is set to false if the trigger should be a sub-transaction.

The EventList class supports the lists of events and widgets that define the termination of implicit or explicit WAIT-FOR statement and triggers activation. For insight details about trigger registration and deregistration works and how the EventList (associated with the event-list clause) gets converted, please see the Trigger Conversion section in the Events chapter of this book.

In terms of scoping, triggers can access resources (variables, streams, buffers and frames) that are defined in the enclosing scope. This complicates the generation of code immensely since the rules for scoping in Java are much more strict. A great deal of processing was implemented in annotations to ensure that all such resources are "promoted" to instance members in the generated business logic classes. This allows triggers and validation expressions to be emitted as inner classes while still accessing all the resources in the proper context. In particular, streams, buffers and frames were all modified to ensure that *all* instances of such resources have unique names and thus all instances can always be made into instance members. With variables, this was not desirable since the variables are so common and keeping a scoped approach yields a better result. So variables are analyzed to determine which need promotion. This promotion logic is in rules/annotations/scope_promotion.rules.

Triggers are implemented as a named inner class (each one gets a generated name) which extends Trigger. The trigger block itself is emitted inside the body() method and it is treated by default as a top-level sub-transaction block in terms of the TransactionManager. All local resources are promoted to instance members in the containing class so these resources are directly accessible. The structure of this class is:

public class TriggerBlock<index>
extends Trigger
{
   /* field definitions */

   public void init()
   {
      /* state initialization code */
   }

   public void body()
   {
      /* trigger body */
   }
}

where <index> is an internal count of all defined triggers, from top to bottom (starting from 0). As with the internal procedures or functions, the init() method is emitted when the trigger requires state initialization. When invoking a trigger, an instance of this class will be created, the init() method will be called once to initialize its state and the body() method will be invoked to execute the trigger's code.

A trigger, as is a top-level block, it acts as an external procedure: on each invocation, it must reset its entire state (variables, buffers, etc) as if this is the very first invocation. This means that no trigger state is copied from one invocation to another.

A difference from other top level blocks is the triggers can be nested. In such cases, their definition is still emitted as an inner class for the external procedure class. The difference is that the nested trigger registration is performed in the scope of the executing trigger and the nested trigger will listen for events as long as the parent trigger is executing.

Example 8:

on go anywhere do:
   message "go".
end.

on go anywhere revert.

Converted code:

public class TriggerTest1
{
   ...
   public void externalProcedure(new Block()
   {
      public void body()
      {
         ...
         EventList list0 = new EventList();
         list0.addEvent("go", true);
         registerTrigger(list0, TriggerBlock0.class, TriggerTest1.this);

         EventList list1 = new EventList();
         list1.addEvent("go", true);
         deregisterTrigger(list1);
      }
   }

   public class TriggerBlock0
   extends Trigger
   {
      public void body()
      {
         message("go");
      }
   }
}

Details:

In this example, notice how the trigger's body is emitted inside the body() method of the TriggerBlock0 inner class. The trigger's registration is done via the registerTrigger call, which gets a reference to the class defining the trigger's body (TriggerBlock0 in this case) and a reference to the currently running instance of the external procedure, TriggerTest1.this.

Example 9:

on go anywhere do:
   message "top go".
   on leave anywhere do:
      message "nested leave".
   end.
end.

Converted code:

public class TriggerTest2
{
   ...
   public void externalProcedure(new Block()
   {
      public void body()
      {
         ...
         EventList list1 = new EventList();
         list1.addEvent("go", true);
         registerTrigger(list1, TriggerBlock0.class, TriggerTest2.this);
         ...
      }
   }

   public class TriggerBlock0
   extends Trigger
   {
      public void body()
      {
         message("top go");
         EventList list2 = new EventList();
         list2.addEvent("leave", true);
         registerTrigger(list2, TriggerBlock1.class, TriggerTest2.this);
      }
   }

   public class TriggerBlock1
   extends Trigger
   {
      public void body()
      {
         message("nested leave");
      }
   }
}

Details:

When nested, the trigger body is still emitted in a inner class for the external procedure class - here, TriggerBlock0 is the body of the enclosing trigger and TriggerBlock1 is the inner class for the enclosed LEAVE trigger. The difference is on runtime - the enclosed LEAVE trigger will listen for events as long as the enclosing GO trigger is still executing; once the enclosing GO is finished, the LEAVE trigger will pop out of scope and will no longer listen for events.

Looping Blocks

The looping blocks can be made to iteratively execute the code contained in the block body. Progress provides blocks that can explicitly loop based on programmer controlled expressions in the block header - these blocks will be explained in the next sections.

Almost all of the looping blocks are managed by BlockManager APIs. But, some looping blocks are converted to compatible Java statements - in a runtime perspective, these kind of blocks are called non-managed blocks. As during the execution the control-flow can switch from a nested managed to an outer non-managed block, any managed inner block which is enclosed in one or more non-managed blocks must be aware of them - for more details about how the control-flow can switch in such cases and how the converted code looks like, please see the Control Flow chapter of this book.

The looping blocks share some common properties, and their syntax looks like:

[ label: ]
<block-type>
  <query-related clauses>
  [ variable = expression1 TO expression2 [ BY k ] ]
  [ WHILE expression ]
  [ TRANSACTION ]
  [ on-error-phrase ]
  [ on-endkey-phrase ]
  [ on-quit-phrase ]
  [ on-stop-phrase ]
  [ frame-phrase ]
...
END.

here:

  • <block-type> is one of DO, REPEAT or FOR
  • <query-related clauses> are query specifications which are distinct for each block type
  • the TO/BY and WHILE clauses are converted in a similar manner for almost all cases, please see the TO Clause and WHILE Clause sections in the Control Flow chapter of this book.
  • The ON ... phrase clauses define the action which will be performed when the specified event is caught by this block. For more details, see the ON Phrase section of this chapter.

ON Phrase

The ON phrase is block property used by 4GL to handle an unusual event that may occur during the course of processing, also called a condition. Although each looping block handles some conditions by default, explicit condition handling can be specified via the ON phrase, which has the following syntax:

ON <condition> UNDO
[ label1 ]
[ , LEAVE [ label2 ]
   | , NEXT [ label2 ]
   | , RETRY [ label1 ]
   | , RETURN { ERROR | NO-APPLY } [ return-string ]
]

where <condition> is one of the ERROR, ENDKEY, QUIT or STOP conditions.

This section will explain the conditions only in a conversion point of view. For more details about how each condition works and how the implicit labels are resolved, please see the Conditions section of the Control Flow chapter of this book.

ON phrases which specify conditions handled by a block are converted to OnPhrase instances added to an array which is passed to the BlockManager APIs via the on parameter. The OnPhrase class is used to instantiate an object which is mapped to a single ON phrase, by passing these parameters to its constructor (they are optional, depending on how the ON phrase is specified):

  • condition is a value from the BlockManager.Condition enum, using Condition.ERROR, Condition.ENDKEY, Condition.STOP and Condition.QUIT to map each of the possible handled conditions, respectively.
  • action is a value from the BlockManager.Action enum, using Action.LEAVE, Action.NEXT, Action.RETRY, Action.RETURN_NORMAL, Action.RETURN_CONSUME and Action.RETURN_ERROR to map each of the possible actions, respectively.
  • label is the label of the block that is the target of the undo
  • value is the data to be returned to the caller of the top-level block from which the control flow is going to return.

The array passed as a parameter to the BlockManager APIs is built using this structure:

OnPhrase[] onPhrase<index> = new OnPhrase[]
{
   new OnPhrase(<condition>, <action>, <label> [, <value> ]),
  [ new OnPhrase(<condition>, <action>, <label> [, <value> ]), ] ...
};

where <index> is an unique counter, starting from 0, which uniquely identifies the OnPhrase[] arrays emitted for the current 4GL program. The <condition>, <action>, <label> and <value> are the OnPhrase's constructor parameters, as explained above.

When specifying an ON phrase for a certain block, it is not mandatory to specify the label or the explicit action to be taken when that condition is raised. Instead, 4GL uses a complex set of rules to determine the block which needs to be undone and the block to which the action needs to be applied - the conversion rules will always disambiguate the targeted block label and the performed action and in the converted code they will always be explicitly set. Details about this can be found in the Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section of the Control Flow chapter of this book.

Example 1:

do on error undo:
   ...
end.

Converted code:

OnPhrase[] onPhrase0 = new OnPhrase[]
{
   new OnPhrase(Condition.ERROR, Action.RETRY, "blockLabel0")
};

doBlock(TransactionType.SUB, "blockLabel0", onPhrase0, new Block()
{
   public void body()
   {
      ...
   }
});

Details:

For a DO block, the ERROR condition is converted to a default RETRY action.

Example 2:

repeat on endkey undo:
   ...
end.

Converted code:

OnPhrase[] onPhrase1 = new OnPhrase[]
{
   new OnPhrase(Condition.ENDKEY, Action.RETRY, "loopLabel0")
};

repeat("loopLabel0", onPhrase1, new Block()
{
   public void body()
   {
      ...
   }
});

Details:

For an ENDKEY condition, the RETRY action is used as default too.

Example 3:

repeat on stop undo, leave:
   ...
end.

Converted code:

OnPhrase[] onPhrase2 = new OnPhrase[]
{
   new OnPhrase(Condition.STOP, Action.LEAVE, "loopLabel1")
};

repeat("loopLabel1", onPhrase2, new Block()
{
   public void body()
   {
      ...
   }
});

Details:

Here, a LEAVE action for the current block is executed when a STOP condition is raised.

Example 4:

repeat on error undo, return,
       on endkey undo, next:
   ...
end.

Converted code:

OnPhrase[] onPhrase4 = new OnPhrase[]
{
   new OnPhrase(Condition.ERROR, Action.RETRY, "loopLabel3"),
   new OnPhrase(Condition.ENDKEY, Action.NEXT, "loopLabel3")
};

repeat("loopLabel3", onPhrase4, new Block()
{
   public void body()
   {
      ...
   }
});

Details:

When multiple conditions are specified for a block, each one is emitted on a distinct index in the parameter array.

DO Block

The DO block in 4GL is the simplest way to group code together (as for the body of a trigger block or for a then clause). The syntax of this block is:

[ label : ]
DO
{ [ FOR record [ , record ] ... ] }
[ preselect-phrase ]
[ query-tuning-phrase ]
[ variable = expression1 TO expression2 [ BY k ] ]
[ WHILE expression ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
{ [ frame-phrase ] } :
...
END.

where the query-tuning-phrase is not yet supported by FWD.

This section will explain how the DO block gets converted and will detail the clauses only from a non control-flow point of view; for insight details, please see DO Block section from the Control Flow chapter of this book.

Depending on the options set to the DO block, this will either convert to a simple Java block or to some other Java statement (as explained in the Control Flow chapter of this book). In all other cases, the converted code will look like this:

<doBlock>([ <transaction type>, ] <label>, [ /* other parameters */, ] new Block()
{
   /* declared fields*/

   public void init()
   {
      /* state initialization code */
   }

   public void body()
   {
      /* converted 4GL code */
   }
});

where:

  • <doBlock> is one of the doBlock, doTo, doWhile or doToWhile APIs in BlockManager
  • the <transaction type> is an optional parameter which will explicitly set the transaction type (please see the Transactions chapter of this book for when it gets emitted). It defaults to no-transaction.
  • the <label> is the block's label (for details about how it gets converted, please see the Control Flow chapter of this book).
  • depending on its configuration, other parameters will be emitted in the converted code between the label and the Block implementation.
  • the init() method contains the initialization code for this block: variables, buffers, queries, streams, etc.
  • the body() method contains the converted 4GL code for this block.

When the DO block has clauses which don't allow it to be converted to a Java statement, the conversion rules will emit an API call to one of the BlockManager.do* methods. The conversion rules will emit one of these APIs considering how the DO block is used and depending on which of its associated clauses are present:

BlockManager API DO Block Syntax Details
doBlock [ label : ]
DO
{ [ FOR record [ , record ] ... ] }
[ preselect-phrase ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
{ [ frame-phrase ] } :
...
END.
In cases when the DO block contains at least one of the on-phrases or one of the FOR, preselect-phrase or TRANSACTION clauses, the doBlock API will be emitted, with the condition that it doesn't contain the TO or WHILE clauses.
In a few words, this API is emitted whenever the DO block can't be converted to a simple Java statement and it doesn't contain any looping clauses.
doTo [ label : ]
DO
{ [ FOR record [ , record ] ... ] }
[ preselect-phrase ]
variable = expression1 TO expression2 [ BY k ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
{ [ frame-phrase ] } :
...
END.
This API is emitted whenever the TO clause exists (but not the WHILE clause) and also it has one of the database-related clauses or at least one on-phrase is present.
doWhile [ label : ]
DO
{ [ FOR record [ , record ] ... ] }
[ preselect-phrase ]
WHILE expression
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
{ [ frame-phrase ] } :
...
END.
Similar to doTo, this API is emitted whenever the WHILE clause exists (but not the TO clause) and also it has one of the database-related clauses or at least one on-phrase is present.
doToWhile [ label : ]
DO
{ [ FOR record [ , record ] ... ] }
[ preselect-phrase ]
variable = expression1 TO expression2 [ BY k ]
WHILE expression
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
{ [ frame-phrase ] } :
...
END.
This API is used when both the TO and WHILE clauses are both present and also it has one of the database-related clauses or at least one on-phrase is present.

For each of the above APIs, BlockManager defines several versions which take different parameters (depending on its configuration):

  • for BlockManager.doBlock the following APIs are defined:
doBlock(String label, Block block)
doBlock(String label, OnPhrase[] on, Block block)
doBlock(String[] enclosing, String label, Block block)
doBlock(String[] enclosing, String label, OnPhrase[] on, Block block)
doBlock(TransactionType lvl, String label, Block block)
doBlock(TransactionType lvl, String label, OnPhrase[] on, Block block)
doBlock(TransactionType lvl, String[] enclosing, String label, Block block)
doBlock(TransactionType lvl, String[] enclosing, String label, OnPhrase[] on, Block block)

where:

  • lvl is the transaction level to be honored. If missing, it defaults to no-transaction.
  • enclosing is the list of non-managed block names that enclose the current block.
  • label is the block name of the current block.
  • on is the list of user-defined on phrases.
  • block is the code to be executed.
  • for BlockManager.doTo the following APIs are defined:
doTo(String label, ToClause to, Block block)
doTo(String label, ToClause to, OnPhrase[] on, Block block)
doTo(String[] enclosing, String label, ToClause to, Block block)
doTo(String[] enclosing, String label, ToClause to, OnPhrase[] on, Block block)
doTo(TransactionType lvl, String label, ToClause to, Block block)
doTo(TransactionType lvl, String label, ToClause to, OnPhrase[] on, Block block)
doTo(TransactionType lvl, String[] enclosing, String label, ToClause to, Block block)
doTo(TransactionType lvl, String[] enclosing, String label, ToClause to, OnPhrase[] on, Block block)

where the new parameters are:

  • to is the clause that defines a loop condition. The loop control variable is initialized on the first pass through and on subsequent passes it is incremented by a given factor. Before the loop body is entered, the loop control variable is compared to the termination value (which is dynamically resolved at each loop iteration). When the termination condition is met, the loop ends.
  • for BlockManager.doWhile the following APIs are defined:
doWhile(String label, LogicalExpression expr, Block block)
doWhile(String label, LogicalExpression expr, OnPhrase[] on, Block block)
doWhile(String[] enclosing, String label, LogicalExpression expr, Block block)
doWhile(String[] enclosing, String label, LogicalExpression expr, OnPhrase[] on, Block block)
doWhile(TransactionType lvl, String label, LogicalExpression expr, Block block)
doWhile(TransactionType lvl, String label, LogicalExpression expr, OnPhrase[] on, Block block)
doWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr, Block block)
doWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
        OnPhrase[] on, Block block)

where the new parameters are:

  • expr is the expression associated with the WHILE clause that defines a loop condition. This expression is evaluated every time the loop body is about to be entered. The loop body will be entered unless the expression evaluates to false.
  • for BlockManager.doToWhile the following APIs are defined:
doToWhile(String label, ToClause to, LogicalExpression expr, Block block)
doToWhile(String label, ToClause to, LogicalExpression expr, OnPhrase[] on, Block block)
doToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr, Block block)
doToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr,
          OnPhrase[] on, Block block)
doToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr, Block block)
doToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr,
          OnPhrase[] on, Block block)
doToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
          LogicalExpression expr, Block block)
doToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
          LogicalExpression expr, OnPhrase[] on, Block block)

where the expr and the to parameters have the same meaning as for the doWhile and the doTo APIs.

Example 1:

def var i as int.

if i > 0
then
   do:
      message "positive".
   end.

Converted code:

if (_isGreaterThan(i, 0))
{
   message("positive");
}

Details:

When the DO block is used only to group code together, it is converted to a simple Java block. The green color marks the 4GL and converted DO block.

Example 2:

B1:
do for book:
   message book.book-title.
end.

Converted code:

doBlock("b1", new Block()
{
   public void init()
   {
      RecordBuffer.openScope(book);
   }

   public void body()
   {
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

In cases when the DO block has record-scoping properties, the converted block must be managed by a BlockManager API. In this case, the doBlock API is emitted with the init() method containing the record scoping initialization code.

Example 3:

b2:
do:
   b3:
   do for book preselect each book:
      message book.book-title.
      leave b1.
   end.
end.

Converted code:

b2:
{
   String[] enclBlocks0 = new String[]
   {
      "b2" 
   };

   doBlock(enclBlocks0, "b3", new Block()
   {
      PreselectQuery query2 = null;

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

      public void body()
      {
         message((character) new FieldReference(book, "bookTitle").getValue());
         leave("b2");
      }
   });

   if (deferredLeave("b2"))
      break b2;
}

Details:

When a non-managed block is referenced from an inner managed block, the managed block will take as parameter the list of enclosing non-managed blocks.

REPEAT Block

This block in its simple form it is used to execute a simple repeat (infinite loop) with the given configuration. Its syntax is:

[ label : ]
REPEAT [ FOR record [ , record ] ... ]
[ preselect-phrase ]
[ query-tuning-phrase ]
[ variable = expression1 TO expression2 [ BY k ] ]
[ WHILE expression ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ] :
...
END.

where the query-tuning-phrase is not supported.

There is no case when the REPEAT block can be converted to a Java statement - in all cases, the conversion rules will emit a BlockManager.repeat* API call and will look like this:

<repeatBlock>([ <transaction type>, ] <label>, [ /* other parameters */, ] new Block()
{
   /* declared fields*/

   public void init()
   {
      /* state initialization code */
   }

   public void body()
   {
      /* converted 4GL code */
   }
});

where:

  • <repeatBlock> is one of the repeat, repeatTo, repeatWhile or repeatToWhile APIs in BlockManager
  • the <transaction type> is an optional parameter which will explicitly set the transaction type (please see the Transactions chapter of this book for when it gets emitted). It defaults to sub-transaction.
  • the <label> is the block's label (for details about how it gets converted, please see the Control Flow chapter of this book).
  • depending on its configuration, other parameters will be emitted in the converted code between the label and the Block implementation.
  • the init() method contains the initialization code for this block: variables, buffers, queries, streams, etc.
  • the body() method contains the converted 4GL code for this block.

The conversion rules will emit one of the APIs defined by the BlockManager class, considering how the REPEAT block is used and depending on which of its associated clauses are present:

BlockManager API REPEAT Block Syntax Details
repeat [ label : ]
REPEAT [ FOR record [ , record ] ... ]
[ preselect-phrase ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ] :
...
END.
In cases when the REPEAT block contains at least one of the on-phrases or one of the FOR, preselect-phrase or TRANSACTION clauses, the doBlock API will be emitted, with the condition that it doesn't contain the TO or WHILE <dfn>clauses</dfn>.
repeatTo [ label : ]
REPEAT [ FOR record [ , record ] ... ]
[ preselect-phrase ]
variable = expression1 TO expression2 [ BY k ]
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ] :
...
END.
This API is emitted whenever the TO clause exists (but not the WHILE clause) and also it has one of the database-related clauses or at least one on-phrase is present.
repeatWhile [ label : ]
REPEAT [ FOR record [ , record ] ... ]
[ preselect-phrase ]
WHILE expression
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ] :
...
END.
Similar to repeatTo, this API is emitted whenever the WHILE clause exists (but not the TO clause) and also it has one of the database-related clauses or at least one on-phrase is present.
repeatToWhile [ label : ]
REPEAT [ FOR record [ , record ] ... ]
[ preselect-phrase ]
[ query-tuning-phrase ]
variable = expression1 TO expression2 [ BY k ]
WHILE expression
[ TRANSACTION ]
[ on-endkey-phrase ]
[ on-error-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ] :
...
END.
This API is used when both the TO and WHILE clauses are both present and also it has one of the database-related clauses or at least one on-phrase is present.

For each of the above APIs, BlockManager defines several versions which take different parameters (depending on its configuration):

  • for BlockManager.repeat the following versions are defined:
repeat(String label, Block block)
repeat(String label, OnPhrase[] on, Block block)
repeat(String[] enclosing, String label, Block block)
repeat(String[] enclosing, String label, OnPhrase[] on, Block block)
repeat(TransactionType lvl, String label, Block block)
repeat(TransactionType lvl, String label, OnPhrase[] on, Block block)
repeat(TransactionType lvl, String[] enclosing, String label, Block block)
repeat(TransactionType lvl, String[] enclosing, String label, OnPhrase[] on, Block block)

where:

  • lvl is the transaction level to be honored. If missing, it defaults to sub-transaction.
  • enclosing is the list of non-managed block names that enclose the current block.
  • label is the block name of the current block.
  • on is the list of user-defined on phrases.
  • block is the code to be executed.
  • for BlockManager.repeatTo the following versions are defined:
repeatTo(String label, ToClause to, Block block)
repeatTo(String label, ToClause to, OnPhrase[] on, Block block)
repeatTo(String[] enclosing, String label, ToClause to, Block block)
repeatTo(String[] enclosing, String label, ToClause to, OnPhrase[] on, Block block)
repeatTo(TransactionType lvl, String label, ToClause to, Block block)
repeatTo(TransactionType lvl, String label, ToClause to, OnPhrase[] on, Block block)
repeatTo(TransactionType lvl, String[] enclosing, String label, ToClause to, Block block)
repeatTo(TransactionType lvl, String[] enclosing, String label, ToClause to,
         OnPhrase[] on, Block block)

where the new parameters are:

  • to is the clause that defines a loop condition. The loop control variable is initialized on the first pass through and on subsequent passes it is incremented by a given factor. Before the loop body is entered, the loop control variable is compared to the termination value (which is dynamically resolved at each loop iteration). When the termination condition is met, the loop ends.
  • for BlockManager.repeatWhile the following versions are defined:
repeatWhile(String label, LogicalExpression expr, Block block)
repeatWhile(String label, LogicalExpression expr, OnPhrase[] on, Block block)
repeatWhile(String[] enclosing, String label, LogicalExpression expr, Block block)
repeatWhile(String[] enclosing, String label, LogicalExpression expr, OnPhrase[] on, Block block)
repeatWhile(TransactionType lvl, String label, LogicalExpression expr, Block block)
repeatWhile(TransactionType lvl, String label, LogicalExpression expr, OnPhrase[] on, Block block)
repeatWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
            Block block)
repeatWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
            OnPhrase[] on, Block block)

where the new parameters are:

  • expr is the expression associated with the WHILE clause that defines a loop condition. This expression is evaluated every time the loop body is about to be entered. The loop body will be entered unless the expression evaluates to false.
  • for BlockManager.repeatToWhile the following versions are defined:
repeatToWhile(String label, ToClause to, LogicalExpression expr, Block block)
repeatToWhile(String label, ToClause to, LogicalExpression expr, OnPhrase[] on, Block block)
repeatToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr, Block block)
repeatToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr,
              OnPhrase[] on, Block block)
repeatToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr, Block block)
repeatToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr,
              OnPhrase[] on, Block block)
repeatToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
              LogicalExpression expr, Block block)
repeatToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
              LogicalExpression expr, OnPhrase[] on, Block block)

where the expr and the to parameters have the same meaning as for the repeatWhile and the repeatTo APIs.

Details about how the TO and WHILE clauses get converted can be found in the Control Flow chapter of this book. The next examples will show how the REPEAT block gets converted.

Example 4:

B0: repeat:
   message "infinite loop".
end.

Converted code:

repeat("b0", new Block()
{
   public void body()
   {
      message("infinite loop");
   }
});

Details:

In its simplest form and without a LEAVE statement to exit the block, the REPEAT block will loop infinitely. Notice how the init() method is not emitted, as the block has no special state to initialize.

Example 5:

B1: repeat for book:
   message book.book-title.
end.

Converted code:

repeat("b1", new Block()
{
   public void init()
   {
      RecordBuffer.openScope(book);
   }

   public void body()
   {
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

When record scoping is added to the block, the init() method is emitted and contains the buffer scope initialization code.

Example 6:

B2: repeat for book preselect each book:
   get next book.
   message book.book-title.
end.

Converted code:

repeat("b2", new Block()
{
   PreselectQuery query0 = null;

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

   public void body()
   {
      query0.next().
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

This example shows how the REPEAT block gets converted when it iterates over the records in a query - in this case, the query is emitted as an instance field for the anonymous Block implementation and the init() method contains the code to initialize the query.

FOR Block

The FOR statement in 4GL comes in several versions: FOR, FOR FIRST, FOR LAST and FOR EACH. From these, the first three convert to the same BlockManager API (the forBlock method), while the FOR EACH statement will convert to one of the BlockManager.forEach* APIs, depending on its configuration.

The full syntax of the FOR statement is:

[ label: ]
FOR
[ EACH | FIRST | LAST ] record-phrase
[ , [ EACH | FIRST | LAST ] record-phrase ] ...
[ query-tuning-phrase ]
[ BREAK ]
[ BY expression [ DESCENDING ] COLLATE ( string , strength [ , collation ] ) [ DESCENDING ] ] ...
[ variable = expression1 TO expression2 [ BY k ] ]
[ WHILE expression ]
[ TRANSACTION ]
[ on-error-phrase ]
[ on-endkey-phrase ]
[ on-quit-phrase ]
[ on-stop-phrase ]
[ frame-phrase ]
...
END.

where the query-tuning-phrase is not supported.

Considering this statement is used for iterating over the records returned by a query, this section will focus only on how each version of the statement gets converted in a block-related manner. For details about how the record-phrase gets converted, please see the Queries chapter of this book. Also, for details about how the TO and WHILE looping clauses get converted, please see the FOR Block section from the Control Flow chapter of this book.

GES: rewrite this next content. This was copied from the BlockManager javadoc. DO, REPEAT and other blocks should have their javadoc content copied/merged too.

Execute a FOR FIRST/LAST WHILE block, FOR FIRST/LAST TO block or FOR FIRST/LAST TO WHILE block with the given configuration and with default properties. Any TO or WHILE clause is treated as an additional condition that must be met before the block body is executed. Note that the presence of a TO or WHILE does not make this a loop. If neither the TO clause or WHILE expression is specified, this is a simple block. If both are specified then both of the conditions are tested with a logical AND. By default, the ON ENDKEY UNDO, LEAVE and ON ERROR UNDO, RETRY properties are supported.

The record retrieval that is associated with FOR blocks or loops must be implemented directly inside the code of the block itself or if there is a TO or WHILE clause then the query processing should be located in the {@link Block#enter} method. This means that the caller is responsible for all query processing. The record retrieval must be done on the first line of code in the block or in the enter method to duplicate the Progress behavior. Note that this is required to handle all error cases and 4GL conditions properly.

The evaluation of the TO or WHILE block termination conditions is done AFTER the record retrieval. This causes any found record to remain available if the block is never entered because of the TO or WHILE conditions.

The TO clause that defines the block condition. The block control variable is initialized before entering the block. Before the block body is entered, the control variable is compared to the termination value (which is dynamically resolved at that moment). if the termination condition is met, the block is never entered.

The WHILE clause that defines a block condition. This expression is evaluated before the block body is about to be entered. The block body will be entered unless the expression evaluates to false.

Execute a FOR EACH, FOR EACH WHILE block, FOR EACH TO block or FOR EACH TO WHILE block with the given configuration and with default properties. Any TO or WHILE clause is treated as an additional condition that must be met before the loop body is executed. If both are specified then both of the conditions are tested with a logical AND. By default, the ON ENDKEY UNDO, LEAVE and ON ERROR UNDO, RETRY properties are supported.

The TO clause that defines a loop condition. The loop control variable is initialized on the first pass through and on subsequent passes it is incremented by a given factor. Before the loop body is entered, the loop control variable is compared to the termination value (which is dynamically resolved at each loop iteration). When the termination condition is met, the loop ends.

The WHILE clause that defines a loop condition. This expression is evaluated every time the loop body is about to be entered. The loop body will be entered unless the expression evaluates to false.

In all cases, the FOR block will get converted to a structure similar to this one:

<forBlock>([ <transaction type>, ] <label>, [ /* other parameters */, ] new Block()
{
   /* declared fields*/

   public void init()
   {
      /* state initialization code */
   }

   public void enter()
   {
      /* query iteration code */
   }

   public void body()
   {
      /* converted 4GL code */
   }
});

where:

  • <forBlock> is one of the forBlock, forEach, forEachTo, forEachWhile or forEachToWhile APIs in BlockManager.
  • the <transaction type> is an optional parameter which will explicitly set the transaction type (please see the Transactions chapter of this book for when it gets emitted). It defaults to sub-transaction.
  • the <label> is the block's label (for details about how it gets converted, please see the Control Flow chapter of this book).
  • depending on its configuration, other parameters will be emitted in the converted code between the label and the Block implementation.
  • the enter() method provides a "callback" to execute user-defined logic at the top of the block body but before the block body is executed. This is guaranteed to be called AFTER the transaction manager scope opens but BEFORE the block body is entered. It will be called once per iteration of the block or once if the block is not iterating. It WILL be called on retry. It is emitted only in cases when the FOR statement has set the TO or the WHILE clause (or both).
  • the init() method contains the initialization code for this block: variables, buffers, queries, streams, etc.
  • the body() method contains the converted 4GL code for this block.

Similar to how the DO and REPEAT blocks get converted, each API version for the FOR statement comes in 4 flavors, to satisfy any combination of its looping clauses: without the TO or WHILE clause, with only the TO clause, with only the WHILE clause and with both the TO and WHILE clauses.

This being a query block, the converted code does not depend on the query type; instead, all query-related fields will be emitted as instance fields for the anonymous class which extends the Block class and the query initialization code will be emitted in the init() method. The code which retrieves the next record is emitted either as the first line of code in the body() method or in the enter() method, when the TO or WHILE (or both) clause is set.

As with the DO or REPEAT blocks, please see the Control Flow chapter of this book for details about how the TO and WHILE clauses get converted.

FOR, FOR FIRST and FOR LAST Statements

These three statements convert to the same BlockManager API - the forBlock method. In this case, as all of them can return at most one record (FOR will return the unique record satisfying the condition (or none), FOR FIRST will return the first record and FOR LAST will return the last record), the conversion rules separate these kind of blocks from the FOR EACH block, which works with queries which can return more than one record..

Depending on its configuration, in this case the FOR block will convert to one of the following APIs:

  • when neither TO or WHILE are used:
forBlock(String label, Block block)
forBlock(String label, OnPhrase[] on, Block block)
forBlock(String[] enclosing, String label, Block block)
forBlock(String[] enclosing, String label, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String label, Block block)
forBlock(TransactionType lvl, String label, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, OnPhrase[] on, Block block)

where:

  • lvl is the transaction level to be honored. If missing, it defaults to sub-transaction.
  • enclosing is the list of non-managed block names that enclose the current block.
  • label is the block name of the current block.
  • on is the list of user-defined on phrases.
  • block is the code to be executed.
  • when the TO clause is used:
forBlock(String label, ToClause to, Block block)
forBlock(String label, ToClause to, OnPhrase[] on, Block block)
forBlock(String[] enclosing, String label, ToClause to, Block block)
forBlock(String[] enclosing, String label, ToClause to, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String label, ToClause to, Block block)
forBlock(TransactionType lvl, String label, ToClause to, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, ToClause to, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, ToClause to,
         OnPhrase[] on, Block block)

where the new parameters are:

  • to is the clause that defines a loop condition. The loop control variable is initialized on the first pass through and on subsequent passes it is incremented by a given factor. Before the loop body is entered, the loop control variable is compared to the termination value (which is dynamically resolved at each loop iteration). When the termination condition is met, the loop ends.
  • when the WHILE clause is used:
forBlock(String label, LogicalExpression expr, Block block)
forBlock(String label, LogicalExpression expr, OnPhrase[] on, Block block)
forBlock(String[] enclosing, String label, LogicalExpression expr, Block block)
forBlock(String[] enclosing, String label, LogicalExpression expr, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String label, LogicalExpression expr, Block block)
forBlock(TransactionType lvl, String label, LogicalExpression expr, OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
         OnPhrase[] on, Block block)

where the new parameters are:

  • expr is the expression associated with the WHILE clause that defines a loop condition. This expression is evaluated every time the loop body is about to be entered. The loop body will be entered unless the expression evaluates to false.
  • when both the TO and WHILE clauses are used:
forBlock(String label, ToClause to, LogicalExpression expr, Block block)
forBlock(String label, ToClause to, LogicalExpression expr, OnPhrase[] on, Block block)
forBlock(String[] enclosing, String label, ToClause to, LogicalExpression expr, Block block)
forBlock(String[] enclosing, String label, ToClause to, LogicalExpression expr,
         OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String label, ToClause to, LogicalExpression expr, Block block)
forBlock(TransactionType lvl, String label, ToClause to, LogicalExpression expr,
         OnPhrase[] on, Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, ToClause to, LogicalExpression expr,
         Block block)
forBlock(TransactionType lvl, String[] enclosing, String label, ToClause to, LogicalExpression expr,
         OnPhrase[] on, Block block)

where the expr and the to parameters have the same meaning as for the forBlockWhile and the forBlockTo APIs.

Example 7:

B0: for book where recid(book) = 1:
   message book.book-title.
end.

Converted code:

forBlock("b0", new Block()
{
   RandomAccessQuery query0 = null;

   public void init()
   {
      RecordBuffer.openScope(book);
      query0 = new RandomAccessQuery(book, "book.bookId = 1", null, "book.bookId asc");
   }

   public void body()
   {
      query0.unique();
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

This example converts the FOR statement to a query which retrieves an unique result (if found). The conversion rules convert it to a forBlock API call and the Block implementation will have the query and record initialization code emitted in its init() method.

Example 8:

B1: for first book:
   message book.book-title.
end.

Converted code:

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

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

   public void body()
   {
      query1.first();
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

This example shows that the FOR FIRST (as with FOR LAST too) gets converted to the same API call, forBlock. As the difference is only in the type of query being executed, the converted code is structured the same as in the previous example.

Example 9:

B2: for last book i = 1 to 10:
   message book.book-title.
end.

Converted code:

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

forBlockTo("b2", toClause0, new Block()
{
   RandomAccessQuery query2 = null;

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

   public void enter()
   {
      query2.last();
   }

   public void body()
   {
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

When either the TO or WHILE clause is present, it is needed to emit the query navigation code in a distinct method - the enter() method; this is because, to duplicate the 4GL behavior, the next record needs to be retrieved before the TO or WHILE clauses are evaluated (as they might contain reference to some field in the iterated buffer).

FOR EACH Statement

The FOR EACH statement can iterate over the records from one or more buffers. Depending on its configuration, this block will convert to one of the following APIs:

  • when neither TO or WHILE are used:
forEach(String label, Block block)
forEach(String label, OnPhrase[] on, Block block)
forEach(String[] enclosing, String label, Block block)
forEach(String[] enclosing, String label, OnPhrase[] on, Block block)
forEach(TransactionType lvl, String label, Block block)
forEach(TransactionType lvl, String label, OnPhrase[] on, Block block)
forEach(TransactionType lvl, String[] enclosing, String label, Block block)
forEach(TransactionType lvl, String[] enclosing, String label, OnPhrase[] on, Block block)

where:

  • lvl is the transaction level to be honored. If missing, it defaults to sub-transaction.
  • enclosing is the list of non-managed block names that enclose the current block.
  • label is the block name of the current block.
  • on is the list of user-defined on phrases.
  • block is the code to be executed.
  • when the TO clause is used:
forEachTo(String label, ToClause to, Block block)
forEachTo(String label, ToClause to, OnPhrase[] on, Block block)
forEachTo(String[] enclosing, String label, ToClause to, Block block)
forEachTo(String[] enclosing, String label, ToClause to, OnPhrase[] on, Block block)
forEachTo(TransactionType lvl, String label, ToClause to, Block block)
forEachTo(TransactionType lvl, String label, ToClause to, OnPhrase[] on, Block block)
forEachTo(TransactionType lvl, String[] enclosing, String label, ToClause to, Block block)
forEachTo(TransactionType lvl, String[] enclosing, String label, ToClause to,
          OnPhrase[] on, Block block)

where the new parameters are:

  • to is the clause that defines a loop condition. The loop control variable is initialized on the first pass through and on subsequent passes it is incremented by a given factor. Before the loop body is entered, the loop control variable is compared to the termination value (which is dynamically resolved at each loop iteration). When the termination condition is met, the loop ends.
  • when the WHILE clause is used:
forEachWhile(String label, LogicalExpression expr, Block block)
forEachWhile(String label, LogicalExpression expr, OnPhrase[] on, Block block)
forEachWhile(String[] enclosing, String label, LogicalExpression expr, Block block)
forEachWhile(String[] enclosing, String label, LogicalExpression expr, OnPhrase[] on, Block block)
forEachWhile(TransactionType lvl, String label, LogicalExpression expr, Block block)
forEachWhile(TransactionType lvl, String label, LogicalExpression expr, OnPhrase[] on, Block block)
forEachWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
             Block block)
forEachWhile(TransactionType lvl, String[] enclosing, String label, LogicalExpression expr,
             OnPhrase[] on, Block block)

where the new parameters are:

  • expr is the expression associated with the WHILE clause that defines a loop condition. This expression is evaluated every time the loop body is about to be entered. The loop body will be entered unless the expression evaluates to false.
  • when both the TO and WHILE clauses are used:
forEachToWhile(String label, ToClause to, LogicalExpression expr, Block block)
forEachToWhile(String label, ToClause to, LogicalExpression expr, OnPhrase[] on, Block block)
forEachToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr, Block block)
forEachToWhile(String[] enclosing, String label, ToClause to, LogicalExpression expr,
               OnPhrase[] on, Block block)
forEachToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr, Block block)
forEachToWhile(TransactionType lvl, String label, ToClause to, LogicalExpression expr,
               OnPhrase[] on, Block block)
forEachToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
               LogicalExpression expr, Block block)
forEachToWhile(TransactionType lvl, String[] enclosing, String label, ToClause to,
               LogicalExpression expr, OnPhrase[] on, Block block)

where the expr and the to parameters have the same meaning as for the forEachWhile and the forEachTo APIs.

Example 10:

B3: for each book:
   message book.book-title.
end.

Converted code:

forEach("b3", new Block()
{
   AdaptiveQuery query3 = null;

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

   public void body()
   {
      query3.next();
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

Here, the FOR EACH statement iterates over all records from the book table. Considering there is no TO or WHILE clause, it gets converted to a forEach API call. Notice how the query navigation code - query3.next() - is emitted at the beginning of the body() method.

Example 11:

B4: for each book while book.book-title begins "FWD":
   message book.book-title.
end.

Converted code:

LogicalExpression whileClause0 = new LogicalExpression()
{
   public logical execute()
   {
      return begins(book.getBookTitle(), "FWD");
   }
};

forEachWhile("b4", whileClause0, new Block()
{
   AdaptiveQuery query4 = null;

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

   public void enter()
   {
      query4.next();
   }

   public void body()
   {
      message((character) new FieldReference(book, "bookTitle").getValue());
   }
});

Details:

In this case, the FOR EACH query has a reference to the book.book-title field (part of the iterated buffer) in its WHILE clause. This is one of the reasons why the query navigation code must be emitted in the enter() method, which is executed before the WHILE expression gets evaluated - so that the buffer can be positioned on the next record prior to expression evaluation.

Conditions

A condition is an unusual event that may occur during the course of processing. The Progress programmer must be able to specify how each procedure responds to the range of possible conditions. Progress 4GL provides a mechanism to specify such behavior at the level of each block.

Progress defines blocks as having very specific "block properties" which define behavior associated with a block. These properties can be explicitly defined or are implicit. The beginning and end of each block corresponds to the beginning and end of a "scope". These scopes can be nested and this structure allows a Progress programmer to control the granularity to which a block's properties are applied.

In Progress, all blocks have some form of implicit and/or explicit condition processing, properties and transaction support. One of the most central behaviors Progress provides is UNDO. This is similar to the concept of a rollback of a transaction and allows one to abort partial changes to variables (and other non-database resources) when a failure or problem is encountered. This is provided by default to reduce the amount of manual work by the programmer.

The behavior of which scope gets undone in transaction processing rollbacks is defined by block properties. In addition, these block properties also define how the block responds to each type of condition AFTER the UNDO occurs. More specifically, the way that the application changes its flow of control in response to a condition is something that is defined by block properties (implicit or explicitly overridden where possible).

The following conditions are possible:

Condition Default Action in Response
ERROR If a transaction is active, UNDO the closest enclosing block that has the ERROR property (this will also be a transaction or sub-transaction).

If a transaction is not active, the UNDO will be a no operation.

Then RETRY the same block (or loop) that has the ERROR property.
ENDKEY If a transaction is active, UNDO the closest enclosing block that has the ENDKEY property (this will also be a transaction or sub-transaction).

If a transaction is not active, the UNDO will be a no operation.

Only blocks with the ENDKEY property will have an interactions counter. The interactions counter is a simple count of the number of input-blocking language statements have executed in the block. Any nested block that does not have an interactions counter of its own will have its interactions counted in the nearest enclosing block's interactions counter. This counter is used to determine the behavior of the END-ERROR event.

Then LEAVE the same block (or loop) that has the ENDKEY property.
STOP If a transaction is active, UNDO the block that defines the transaction (as opposed to a sub-transaction).
If a transaction is not active, the UNDO will be a no operation.
Then RETRY the "startup procedure". This means that the entire top-level procedure is run again from the top.
Note that if this is generated based on a database disconnect, a special form of processing will occur where the default STOP behavior cannot be explicitly overridden until an exit to a scope in which no access is made to the database that was disconnected. At this point the normal STOP processing is re-imposed. See the Honoring STOP Conditions section of this chapter for more details.
QUIT If a transaction is active, commit the block that defines the transaction (as opposed to a sub-transaction).
If a transaction is not active, the commit will be a no operation.
Exit to the operating system.
END-ERROR Technically, this really isn't a separate condition, but rather a key in the UI which generates 2 different conditions based on context. An END condition is generated if the key press occurs during the first input-blocking language statement in the block. An ERROR condition is generated if the key press occurs during a subsequent input-blocking language statement in the block.
The exception to this processing occurs for blocks that do not have the ENDKEY property (this can happen for some DO blocks and is always true for EDITING blocks). In such a case, the interactions counter is not maintained for the current block but rather is maintained at the level of the nearest enclosing block which has the ENDKEY property.
This means that the normal conversion of END-ERROR to ERROR is thus modified in such blocks. In the EDITING case especially, it is likely that an ERROR will be generated (because the EDITING block itself counts as an interaction and thus with a larger interactions counter it is more likely that an ERROR will be generated). What is especially different here is that the ERROR will be propagated to the enclosing block that has the ENDKEY property! So even if the DO or EDITING block has the ERROR property in this case, if they also do not have the ENDKEY property and the interactions counter (for the enclosing ENDKEY block) is < 2 then the ERROR will NOT be processed by the current block.
See above for ENDKEY and ERROR respectively.

Some language statements and all assignments that can generate the ERROR condition can be executed using the NO-ERROR keyword, which disables the generation of this condition. When a failure occurs, instead of generating ERROR, the ERROR-STATUS system handle will have its state updated.

The possible actions in response to a condition are:

Action Description
UNDO Rolls back all database, variable, temp-table and work-table modifications since the beginning of the target block. This target block must have opened a transaction or it must be enclosed within a block that opened a transaction AND must itself be a sub-transaction. In any other case, the UNDO is a no operation.
There is no way of disabling UNDO. Any condition that is raised will always trigger an UNDO (with the exception of the implicit behavior for QUIT which is to commit instead of UNDO). In addition, one of the actions below will also be executed. If not explicitly specified, the implicit action is always a RETRY.
LEAVE Breaks (exits) out of the target block. If the target block is a top-level block, the LEAVE will be converted to a RETURN.
RETRY The target block (non-loop or loop) is executed from the top with the same state as when it last executed.

Since a retry of a block in which the state never changes from iteration to iteration would yield an infinite loop, Progress provides a feature to detect when an infinite loop could occur. This is called Infinite Loop Protection (ILP). RETRY will be converted to a LEAVE, NEXT or RETURN depending on the block type to which the RETRY is targeted. Please see the Infinite Loop Protection section of this chapter for details about how this is done.
NEXT The next iteration of the loop is executed. If the associated block is not a loop, then this is automatically treated as a LEAVE or a RETURN (see below).
Infinite Loop Protection (ILP) protects the NEXT action (when triggered from an ON phrase or from an UNDO statement). In certain cases, a NEXT will be converted to a LEAVE or RETURN by the infinite loop protection logic depending on the block type to which the RETRY is targeted - see the Infinite Loop Protection section of this chapter for details.
RETURN The current procedure, function or trigger returns to the caller. There are 5 forms:
     1. Raise an ERROR condition in the caller, using RETURN ERROR. In a function, this can be specified but the error is ignored (not raised).
     2. Return from a procedure or trigger block and allow normal processing to continue, using RETURN. If this executed from a procedure, then the RETURN-VALUE global character variable is set to the empty string &quot;" which can be read in the caller.
     3. Return the value of one of the basic data types when returning from a function, using RETURN <expression>.
     4. Set the RETURN-VALUE global character variable which can be read in the caller when returning from a procedure, using RETURN <character_expression>.
     5. Return and disable normal event processing when returning from an event trigger block, using RETURN NO-APPLY. This can also be specified in other block types, but the NO-APPLY will be ignored.

In all actions (except RETURN), the block which is the target of the action can be implicitly determined (if there is no label) or it can be explicitly specified using a user-defined label for the block.

One cannot UNDO a specified label which is outside of the scope referenced in the associated action such as LEAVE or RETRY. In other words, the target of the action will determine the flow of control for the program. Any UNDO operation must be scoped inside (more deeply nested than) or equal to the block which is the target for the other action otherwise a compiler error will result.

Blocks have properties that match a condition name. Thus there are ERROR, ENDKEY, STOP and QUIT properties. The existence of a property (implicitly or explicitly) in a given block means that that block provides a predictable sequence of actions in response to the condition (matching that property) being raised. To explicitly add a property to a block, one uses the ON ERROR, ON ENDKEY, ON STOP or ON QUIT clauses in the block header. For each of these explicit overrides, one may also specify UNDO, secondary_action. UNDO is required and cannot be avoided. The secondary action is one of those listed in the previous table. If no secondary action is specified, then RETRY is the action executed by default.

The secondary action of an ON phrase can have an explicit label defining its target block. If no label is specified but there is a label defined for the UNDO portion of the ON phrase, then that UNDO label will also define the target block for the secondary action. If neither portion of the ON phrase defines a label, then the target block will be determined implicitly. The logic for this is very complicated. Please see the Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section of this chapter for details.

The implicit behavior of each block type is different in regards to condition handling. Since all condition processing is handled based on whether the condition has a matching property for a given block, if a block doesn't have a property and that condition is raised, that block will be bypassed and the processing will occur in an enclosing block. This means that if (for example) a block has the ERROR property, then in the case where an ERROR condition is raised, if that block is the innermost block which has the ERROR property, then that is the level at which implicit UNDO and following implicit action occurs. The implicit action that occurs is defined differently for each condition. This default action ONLY OCCURS if the block implicitly has the matching property (e.g. the ENDKEY property when the ENDKEY condition is raised).

Please note that the UNDO action is special since it always occurs first (if it occurs at all), before the secondary action. UNDO cannot be disabled however it only occurs when a transaction is active in the current or a containing scope. Some blocks always have a given set of properties (implicit or explicit) and some blocks can only have explicit properties (i.e. DO). If any property is not associated with a block, then no processing of that condition will occur in that scope. This means that the stack unwinds past that block without stopping to do any processing. Only those properties that are implicitly or explicitly associated with a scope will ever be processed in that scope.

Each property is independent. For example, if a block has an ERROR property, that has no bearing on how it processes the ENDKEY.

The following are the implicit properties that are inherently provided with each block type:

Block Type ERROR ENDKEY STOP QUIT
FOR Y Y N* N*
REPEAT Y Y N* N*
DO N* (except if the TRANSACTION keyword is present, then there is an implicit ERROR property) N N N
procedure Y Y Y* (only for the "startup procedure") N*
trigger Y Y N* N*
editing Y (for UPDATE except when the ERROR condition is generated by the END-ERROR key and the interactions counter is < 2) / N (for PROMPT-FOR and SET) N N N
function Y (always LEAVES the function with an unknown value return) Y (always LEAVES the function with an unknown value return) N N

(*) All of these entries have been found to be different than documented in the official 4GL documentation.

The DO block is the only block with no implicit behavior (except in the case where the TRANSACTION keyword or ON phrases are present). All DO, REPEAT and FOR blocks can have the same set of explicit properties. If explicitly set, this definition overrides the default action.

The DO block will have sub-transaction support if they contain any ON phrase. The condition does not matter. So an ON QUIT UNDO, RETRY is enough to force the DO block to have the sub-transaction property.

Please note that the function block is quite special. In user defined functions, no user-input blocking statement can be executed. This means that PROMPT-FOR/SET/UPDATE/INSERT/CHOOSE/WAIT-FOR/PAUSE and READKEY are all illegal, there are very limited choices of editing database fields within a function. This combined with the fact that functions don't have the transaction property by default means that functions usually are sub-transactions. However, when a direct assignment (e.g. field = expression) occurs, this will cause the function block to obtain TRANSACTION level. There is no way to make a function block a "NO" transaction (lacking the sub-transaction or transaction properties). The ERROR and END conditions both default to LEAVE and the return value will be the unknown value in these cases. This cannot be overridden. Because of this behavior and the fact that one cannot specify an ON phrase with any of the 4 conditions, it is only possible to get a RETRY at the function block level by using the UNDO, RETRY language statement when infinite loop protection is disabled (e.g. with the RETRY function).

Honoring STOP Conditions

Stop conditions can be generated by certain runtime errors such as a database disconnection or failures in stream processing. Any runtime code running on a thread which is servicing converted Progress business logic will translate any InterruptedException into a StopConditionException. Classes which provide such a service:

  • Stream (handles the general cases for all streams)
  • ProcessStream (handles a case that is specific to reading from a process' standard I/O)
  • InMemoryLockManager

There are unusual cases in the runtime where processing is not occurring on a thread which is servicing converted Progress business logic. For example, if a secondary thread is used to cleanup file system resource (e.g. close pipes or files) or if a secondary thread is used to copy data between two pipes. Note that in these cases, there is no counterpart or proper mapping into a Progress semantic since such cases don't exist in the Progress runtime. Generally, such cases silently exit or fail. Classes which provide such behavior:

  • ProcessOps (both the Cleaner inner class and interruptions that occur during a child process' execution results in Progress not seeing such interruptions)
  • StreamConnector (secondary copying thread)

In addition, the STOP language statement in Progress allows an explicit stop condition to be raised. In this case the business logic will explicitly throw a StopConditionException.

Stop condition processing is complicated by the Progress feature that the user may press the CTRL-C key combination at any time and this immediately interrupts the execution of the Progress program (which is single threaded). In the FWD environment this is complicated by the split of UI processing into a separate process accessed via the network. The UI client will monitor for the CTRL-C key combination and this will cause asynchronous processing of the interruption to occur. In particular, if processing is currently occurring on the client side, the interruption will be converted to an interruption of that running thread which will be converted to a StopConditionException in the UI client. This will then unwind the stack normally until the location in the business logic that handles (catches) the stop condition is reached. If the current thread is processing on the server (such that the client cannot honor the UI since it is not actively running), then the server will be asynchronously notified and this will trigger a call to Thread.interrupt(). If the currently processing thread on the server (for that user's context) is blocked, an InterruptedException would be generated and this would be caught by the runtime as noted above and translated into a StopConditionException. This is a key point: J2SE only generates an InterruptedException in specific cases where a thread blocks such as Object.wait() or Thread.sleep(). If the thread is not blocked, then J2SE will not generate an InterruptedException, instead it just sets the interrupt status of the thread. The TransactionManager checks the interrupted status of the current thread at block start, stop and when a block iterates. Any previous interruption will be honored by throwing a StopConditionException. Please see the Transactions chapter of this book for more details on TransactionManager.

Infinite Loop Protection

Infinite loop protection (ILP) is a hidden feature of the block processing that is designed to modify the block behavior as specified by the 4GL programmer in order to avoid an infinite loop. The idea is that an action that is specified by a 4GL programmer (RETRY or NEXT) which could perpetuate an infinite loop, will be silently converted (by the runtime) to a different action (LEAVE, NEXT or RETURN) in order to cause a change in the loop state that will avoid an infinite loop. This silent conversion is done based on runtime state which is affected by the flow of control of the program. So blocks and statements that are conditionally executed will change the state of the runtime and thus modify the conversions that occur. For this reason, the behavior cannot be duplicated by static code modifications. In particular, the following behavior is regulated:

1. RETRY

  • ON condition UNDO [ label1 ] [ , RETRY [ label2 ] ]
    • This is an ON phrase for any of the 4 conditions, which has a secondary action of RETRY
  • UNDO, RETRY
    • This is an UNDO language statement with a secondary action of RETRY.
  • ILP modifies RETRY in all block types.

2. NEXT

  • ON condition UNDO [ label1 ] [, NEXT [ label2 ] ]
    • This is an ON phrase for any of the 4 conditions, which has a secondary action of NEXT
  • UNDO, NEXT
    • This is an UNDO language statement with a secondary action of NEXT.
  • ILP does NOT modify NEXT in blocks that are loops which inherently change the program state in each iteration, as the DO TO, REPEAT TO, and all forms of FOR EACH. In such cases, the program's data generally changes in each iteration (FOR EACH technically can be an infinite loop if you edit the index being used to navigate the records, but this is a rare use case). This means that a NEXT is "safe" in such cases so ILP does not apply.

3. The block to which the RETRY or NEXT is targeted (explicitly using a label or implicitly calculated by the 4GL runtime/compiler) determines the kind of conversion that occurs. See the Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY section for details on determining the target block for an action.

This conversion will occur unless ILP has been disabled for the targeted block. ILP becomes disabled based on runtime state which is maintained implicitly by particular language statements and by the block processing infrastructure of the runtime.

If ILP is NOT disabled, the following conversions occur:

Block Type Top Level Block/Loop Conversion
FOR EACH (all forms - even when there is a WHILE or TO/BY present) no loop RETRY - converted to NEXT
NEXT - no conversion, allowed to execute
DO TO/BY no loop RETRY - converted to NEXT
NEXT - no conversion, allowed to execute
REPEAT TO/BY no loop RETRY - converted to NEXT
NEXT - no conversion, allowed to execute
FOR FIRST/LAST (all forms - even when there is a WHILE or TO/BY present) no block RETRY - converted to LEAVE
NEXT - converted to LEAVE
DO WHILE no loop RETRY - converted to LEAVE
NEXT - converted to LEAVE
REPEAT WHILE no loop RETRY - converted to LEAVE
NEXT - converted to LEAVE
REPEAT no loop RETRY - converted to LEAVE
NEXT - converted to LEAVE
DO no block RETRY - converted to LEAVE
NEXT - converted to LEAVE
external procedure yes block RETRY - converted to RETURN
NEXT - converted to RETURN
internal procedure yes block RETRY - converted to RETURN
NEXT - converted to RETURN
function yes block RETRY - converted to RETURN
NEXT - converted to RETURN
trigger yes block RETRY - converted to RETURN
NEXT - converted to RETURN
editing no loop Infinite loop protection is always disabled in editing blocks (since they can only be called from UPDATE/SET/PROMPT-FOR as part of a user interaction), so no conversions ever occur in targeted editing blocks.

There are 2 cases where this ILP is disabled:

  1. A language statement which blocks for user input is executed before the point in the block where the condition is raised. At the moment that such a statement executes, the infinite loop protection is disabled. Since editing blocks are inherently processed as part of user input, infinite loop protection is meaningless for such loops.
  2. The RETRY built-in function is executed before the point in the block where the condition is raised. At the moment that function executes, the infinite loop protection is disabled.

Until one of these 2 cases is executed, any condition that generates a RETRY will result in a NEXT, LEAVE or RETURN. Only when the infinite loop protection is disabled will RETRY result in a RETRY! Likewise@ NEXT may be allowed, but depending on the block type it may be converted to a LEAVE or RETURN.

All block types have retry support. In other words, it is possible to retry any block including non-looping blocks and the top-level blocks (like a built-in function or internal procedure). But whether the RETRY will be allowed or not is determined by the ILP processing at runtime. It is important to note that the NEXT language statement is NOT affected by ILP. ILP only affects ON phrases and the UNDO language statement.

A note in the UNDO language statement states that if "nothing changes during a RETRY of a block" it will convert the RETRY into a NEXT or LEAVE. As far as can be determined using application code, the 4GL runtime does not actually monitor the state of variables or other resources to detect if "nothing changes". It looks like the disabling of ILP is taken as a proxy for whether things have changed in the current iteration.

The Progress READKEY documentation has a specific note regarding a special implementation of infinite loop protection. It states that a count is used to convert UNDO, RETRY into UNDO, NEXT as well as the conversion of UNDO, NEXT into UNDO, LEAVE. While testing has not found any special counter behavior for READKEY, there is a "stutter quirk" (a counter based difference in ILP behavior) that has been found associated with ON ENDKEY UNDO, RETRY when it is used on certain block types.

The following code will cause a kind of "stuttering" where RETRY will sometimes be allowed to occur due to the state of a counter. This stutter quirk only comes into play when an ENDKEY condition is raised and is handled by the ON ENDKEY UNDO, RETRY phrase. Consider this code:

def var i as int no-undo.
do i = 1 to 5 on endkey undo, retry:
  message i.
  apply "endkey".
end.

The results:

1
2
3
3
4
4
5
5

The first 2 retries are converted to a NEXT but the 3rd RETRY is allowed to go forward as a RETRY (no conversion occurs). Then the 4th RETRY is converted to a NEXT but the 5th RETRY is a RETRY. This only occurs with the execution of the actions associated with an ON phrase and ONLY with ENDKEY. If any other condition is targeted (e.g. ERROR), this will not occur. Likewise, if any other action is specified (NEXT instead of RETRY), then this behavior does not occur.

Finally, this stutter mode is only seen in ON ENDKEY UNDO, RETRY phrases for the following block types:

  • DO TO
  • REPEAT TO
  • FOR EACH(all forms)

Determining the Target and Meaning of UNDO, LEAVE, NEXT and RETRY

Each of these actions can explicitly reference a specific labeled block (e.g. LEAVE inner) or will implicitly reference a block. In each of these cases, the action always operates in regards to a specific block. In the implicit cases (one in which no block label was specified in the 4GL source code), the 4GL compiler/runtime must determine the block which is referenced. There are complex rules by which the UNDO, LEAVE, NEXT and RETRY actions are "bound" to a specific block as its target. These actions do not always operate on the current block or even on an explicitly defined block target. This target block resolution is complicated by the block structure in which the ON phrase or language statements are contained. In particular, the target can reference enclosing blocks implicitly or explicitly. The block properties of the enclosing blocks will also affect any implicit target calculation.

There are 3 ways that these actions can be invoked:

  • A language statement encountered in the normal flow of control (LEAVE [ label ] and@ NEXT [ label ] only).
  • As an other_action defined in an UNDO language statement (UNDO [ label1 ], [ other_action [ label2 ] ]), encountered in the normal flow of control.
  • As an other_action defined in an ON phrase (ON <condition> UNDO [ label1 ], [ other_action [ label2 ] ]), in response to a condition being raised inside the block.

The UNDO language statement and ON phrases have the ability to specify up to 2 labels (one for the UNDO and one for the other action).

There are only 4 types of blocks in Progress which can be labeled: DO, REPEAT, FOR and EDITING. Thus, only these block types can be explicitly targeted by the UNDO, LEAVE, NEXT or RETRY actions. But when implicit block targets are calculated by Progress, it is possible for a non-labeled block to be the target of an action. The non-labeled block types are special top-level blocks (external procedure, internal procedure, trigger and user defined function). None of those non-labeled block types are iterating blocks. This means that a NEXT which targets one of these blocks must be translated into something else (LEAVE in Progress terms). In Progress, LEAVE on a top-level block is the same as a RETURN statement (with no modifiers or returned data). If LEAVE or NEXT do implicitly target a top-level block, then the 4GL behavior silently converts them into a RETURN. RETRY on a top-level block will be converted to a RETURN if infinite loop protection is NOT disabled.

All blocks in Progress implicitly have the ERROR property (see the Conditions section of this chapter for details) except for the DO and EDITING (PROMPT-FOR or SET forms) blocks. The DO only has the ERROR property when it has an ON ERROR phrase or if it has the TRANSACTION keyword.

The ERROR property that exists for DO blocks and for the UPDATE form of EDITING blocks (and for the special "hidden" block in non-EDITING UPDATE blocks) is special in that it is not in effect in the following case:

  1. The block does not also have the ENDKEY property (EDITING blocks never have the ENDKEY property and DO blocks only have it if it is explicitly specified in an ON ENDKEY phrase); AND
  2. The ERROR condition was raised by the END-ERROR key (or an APPLY of that key); AND
  3. The current interactions counter is less than 2

This is runtime state that will modify how the ERROR property is processed when the ERROR condition is generated by the END-ERROR key. This means that an ERROR condition generated by END-ERROR key in an UPDATE block (EDITING or not) is always forwarded on to the containing block, even though all other ERROR conditions are handled as an UNDO, RETRY.

Most importantly, for the purposes of resolving unlabeled actions to blocks with the ERROR property, EDITING blocks (even UPDATE EDITING blocks) are treated as if they do not have the ERROR property.

Considering this, the following are the possible implicit block target resolution algorithms:

Code Description
NEAR-ERR The nearest enclosing block with the ERROR property.

Example 1:
outer:

repeat:

  inner:

  do on error undo, retry:

    <statement>

  end.

end.


The nearest enclosing block (to the <statement>) with the ERROR properly is inner.

Example 2:
outer:

repeat:

  inner:

  do:

    <statement>

  end.

end.

The nearest enclosing block (to the <statement>) with the ERROR property is outer.
CURRENT The "current" block. This is the block which directly encloses the UNDO, NEXT or LEAVE language statement. Or in the case of an ON phrase, it is the block upon which the ON phrase is specified.
outer:

repeat:

 inner:

 do on error undo, RETRY:

   <statement>

 end.

end.

In this example, for both the <statement> and the RETRY, the current block is inner.
AS-UNDO The other action will be targeted at the same block as the associated UNDO is targeted. If UNDO is targeted (implicitly or explicitly) to inner then the other action will be targeted to inner.

Assuming that the directly containing block is labeled "inner" and that block is contained inside a block labeled "outer", the following are possible formations of the UNDO language statement and ON phrases:

Feature UNDO Target Other Action Other Action Target Static Conversion Possible Compiler Error
UNDO. NEAR-ERR RETRY (implicit) AS-UNDO   n/a
UNDO inner. inner RETRY (implicit) AS-UNDO   If inner does not have the ERROR property.
UNDO outer. outer RETRY (implicit) AS-UNDO   If outer does not have the ERROR property.
UNDO, RETRY. NEAR-ERR RETRY AS-UNDO    
UNDO, LEAVE. NEAR-ERR LEAVE AS-UNDO *  
UNDO, NEXT. NEAR-ERR NEXT AS-UNDO *  
UNDO, RETRY inner. NEAR-ERR RETRY inner   If inner does not have the ERROR property, then UNDO would choose a different block than inner. Since RETRY is targeted at inner, this is a problem. UNDO and RETRY must always be targeted at the same block.
UNDO, LEAVE inner. NEAR-ERR LEAVE inner   If inner does not have the ERROR property, then UNDO would choose a different block (a more enclosing one) than inner. Since LEAVE must be targeted at a block that is the same as the UNDO target or which encloses the UNDO target, this is a problem.
UNDO, NEXT inner. NEAR-ERR NEXT inner   If inner does not have the ERROR property, then UNDO would choose a different block (a more enclosing one) than inner. Since NEXT must be targeted at a block that is the same as the UNDO target or which encloses the UNDO target, this is a problem.
In addition, if inner is not an iterating block the compiler fails.
UNDO, RETRY outer. NEAR-ERR RETRY outer   This is only possible if an inner block (one that more directly encloses the statement) does not have the ERROR property. If inner does have such properties, then it would be implicitly chosen as the UNDO target and that would mismatch with the outer block as the action target, causing the compiler to error.
UNDO, LEAVE outer. NEAR-ERR LEAVE outer   The outer block or one of its nested blocks (which still encloses this statement) must have the ERROR property, otherwise the LEAVE will be targeted inside the UNDO target, which is not allowed.
UNDO, NEXT outer. NEAR-ERR NEXT outer   The outer block or one of its nested blocks (which still encloses this statement) must have the ERROR property, otherwise the NEXT will be targeted inside the UNDO target, which is not allowed.

In addition, if outer is not an iterating block the compiler fails.
UNDO inner, RETRY. inner RETRY AS-UNDO   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, LEAVE. inner LEAVE AS-UNDO * inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, NEXT. inner NEXT AS-UNDO * inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, RETRY inner. inner RETRY inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, LEAVE inner. inner LEAVE inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, NEXT inner. inner NEXT inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO inner, RETRY outer. n/a n/a n/a   Invalid syntax, compiler will always error because RETRY and UNDO must target the same block.
UNDO inner, LEAVE outer. inner LEAVE outer   inner must have ANY property (not just ERROR) otherwise there is a compiler error. (outer has no property dependencies)
UNDO inner, NEXT outer. inner NEXT outer   inner must have ANY property (not just ERROR) otherwise there is a compiler error. (outer has no property dependencies)
In addition, if outer is not an iterating block the compiler fails.
UNDO outer, RETRY. outer RETRY AS-UNDO   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO outer, LEAVE. outer LEAVE AS-UNDO * outer must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO outer, NEXT. outer NEXT AS-UNDO * outer must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO outer, RETRY inner. n/a n/a n/a   Invalid syntax, compiler will always error because RETRY and UNDO must target the same block.
UNDO outer, LEAVE inner. n/a n/a n/a   Invalid syntax, compiler will always error because LEAVE must target the same or a more enclosing block as UNDO.
UNDO outer, NEXT inner. n/a n/a n/a   Invalid syntax, compiler will always error because NEXT must target the same or a more enclosing block as UNDO.
UNDO outer, RETRY outer. outer RETRY outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO outer, LEAVE outer. outer LEAVE outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
UNDO outer, NEXT outer. outer NEXT outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
In addition, if outer is not an iterating block the compiler fails.
ON <condition> UNDO. CURRENT RETRY (implicit) AS-UNDO   CURRENT must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner. inner RETRY (implicit) AS-UNDO   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer. outer RETRY (implicit) AS-UNDO   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO, RETRY. CURRENT RETRY AS-UNDO    
ON <condition> UNDO, LEAVE. CURRENT LEAVE AS-UNDO *  
ON <condition> UNDO, NEXT. CURRENT NEXT AS-UNDO * No error even if the implicit target block is not an iterating block.
ON <condition> UNDO, RETRY inner. CURRENT RETRY inner   CURRENT must be the same block as inner (RETRY and UNDO must always be at the targeted to the same block).
ON <condition> UNDO, LEAVE inner. CURRENT LEAVE inner   CURRENT must be the same block as inner or enclosed by inner. The LEAVE target block needs no properties in particular.
ON <condition> UNDO, NEXT inner. CURRENT NEXT inner   CURRENT must be the same block as inner or enclosed by inner. The NEXT target block needs no properties in particular.
However, if inner is not an iterating block the compiler fails.
ON <condition> UNDO, RETRY outer. n/a n/a n/a   Invalid syntax. The UNDO will target the current block which is more deeply nested than the RETRY target, which causes the compiler to fail.
ON <condition> UNDO, LEAVE outer. CURRENT LEAVE outer   The LEAVE target block needs no properties in particular.
ON <condition> UNDO, NEXT outer. CURRENT NEXT outer   The NEXT target block needs no properties in particular.
If outer is not an iterating block the compiler fails.
ON <condition> UNDO inner, RETRY. inner RETRY AS-UNDO   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, LEAVE. inner LEAVE AS-UNDO * inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, NEXT. inner NEXT AS-UNDO * inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, RETRY inner. inner RETRY inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, LEAVE inner. inner LEAVE inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, NEXT inner. inner NEXT inner   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
If inner is not an iterating block the compiler fails.
ON <condition> UNDO inner, RETRY outer. n/a n/a n/a   Invalid syntax. The UNDO will target the inner block which is more deeply nested than the RETRY target, which causes the compiler to fail.
ON <condition> UNDO inner, LEAVE outer. inner LEAVE outer   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO inner, NEXT outer. inner NEXT outer   inner must have ANY property (not just ERROR) otherwise there is a compiler error.
If outer is not an iterating block the compiler fails.
ON <condition> UNDO outer, RETRY. outer RETRY AS-UNDO   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer, LEAVE. outer LEAVE AS-UNDO * outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer, NEXT. outer NEXT AS-UNDO * outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer, RETRY inner. n/a n/a n/a   Invalid syntax. The UNDO and RETRY must target the same block, otherwise the compiler fails.
ON <condition> UNDO outer, LEAVE inner. n/a n/a n/a   Invalid syntax. LEAVE must target the same or a more enclosing block as UNDO is targeting.
ON <condition> UNDO outer, NEXT inner. n/a n/a n/a   Invalid syntax. NEXT must target the same or a more enclosing block as UNDO is targeting.
ON <condition> UNDO outer, RETRY outer. outer RETRY outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer, LEAVE outer. outer LEAVE outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
ON <condition> UNDO outer, NEXT outer. outer NEXT outer   outer must have ANY property (not just ERROR) otherwise there is a compiler error.
If outer is not an iterating block the compiler fails.

In the above table <condition> can be ERROR, ENDKEY, STOP or QUIT. The particular condition in question does not matter for the purposes of the behavior documented above.

For details on the implicit targeting of LEAVE and NEXT statements, please see the LEAVE Statement and NEXT Statement sections (respectively) of the Control Flow chapter.

The "Static Conversion Possible" column denotes the difference between runtime conversion of actions (infinite loop protection) versus static (compile time) conversion of actions. A static conversion is one that will not change based on runtime state, but instead is based on data known at the time the source code is compiled.

At runtime, a RETRY action will be converted to a NEXT, LEAVE or RETURN (depending on target block type) if infinite loop protection is NOT disabled. See the Infinite Loop Protection section above for details.

If the action is NEXT and was caused by an ON phrase or UNDO statement AND the target block is a non-iterating inner block (it is not a top-level block), then the NEXT will be converted to a LEAVE.

If the action is NEXT or LEAVE and the target block matched is a top level block (external procedure, internal procedure, function or trigger), then the action will be converted to a RETURN.


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