Project

General

Profile

Record Buffer Definition and Scoping

The concept of a record buffer is central to data access in the Progress 4GL. A record buffer acts as the bridge between the Progress database and application business logic. A record buffer is created implicitly by the Progress runtime environment whenever business logic accesses an existing database record or creates a new one. A record buffer also can be created explicitly using certain language statements. Each record buffer can hold a single record at a time, or it can be empty. A record buffer is a holding area for a record newly created by business logic before it is persisted in a database, or for a record retrieved from a database, so that it can be read, updated, or deleted by application logic.

Buffers are referenced via a name. That name may have been explicitly defined in the current external procedure, internal procedure or function using one of the following language constructs:

DEFINE BUFFER {buffer} FOR {record}

DEFINE PARAMETER BUFFER {buffer} FOR {record}

FUNCTION ... (BUFFER {buffer} FOR {record}, ...)

If not explicitly defined, then the buffer name must be implicitly defined based on a reference to a table name (persistent table, temp-table or work-table) inside a language statement or built-in function.

In the 4GL, record buffers operate within various record scopes. A record scope (a.k.a, buffer scope) corresponds with some block of enclosing code, which varies depending upon the ways in which the buffer is accessed by the program. The scope determines what happens to records which are stored in the buffer, and when certain events occur. For instance, a buffer's scope determines when a newly created record is inserted into the database or a modified record is saved (for create and update operations, respectively). It determines when a new record is read from the database into the buffer (for read operations), and how undo operations are conducted. Scopes can be nested, further complicating the behavior.

Progress manages buffer scopes implicitly, based upon many factors. Seemingly innocuous changes in a program can lead to unusual and often unintended changes in a record scope, resulting in unexpected variations in buffer behavior. This implicit handling makes record scoping one of the more challenging aspects of Progress programming. As we will see below, the rules determining record scopes are quite complicated, and are not well documented elsewhere.

FWD Record Buffer Implementation Classes

The concept of a record buffer is maintained in the FWD runtime environment. The class com.goldencode.p2j.persist.RecordBuffer implements buffers associated with persistent tables and com.goldencode.p2j.persist.TemporaryBuffer implements those associated with temporary tables. These classes enable the basic CRUD (Create, Read, Update, Delete) services, and implement numerous legacy, database-related, built-in functions (for a detailed explanation of how 4GL database functions map to their FWD equivalents, please refer to the Database Field References chapter of this book).

The use of record buffers and particularly their scopes is more explicit in Java source code converted by FWD than in the original Progress code. All buffers are defined explicitly via Data Model Objects (DMOs), and their scopes are opened explicitly. Scopes are closed again automatically at the end of the block of code for which they are opened. This ensures that the source code is self-documenting with respect to record buffers and their corresponding scopes.

Instances of RecordBuffer and its subclass TemporaryBuffer are neither created nor accessed directly by converted application code. Rather, DMO instances are defined/created in Java code, and the FWD runtime creates an associated instance of RecordBuffer (for a persistent table buffer) or of TemporaryBuffer (for a temporary table buffer). The buffer instance acts as a worker object behind the scenes to provide persistence services to application logic through the associated DMO. These services are accessed either by calling instance methods of the target DMO directly (to get or set individual record fields, for example), or by invoking static methods of RecordBuffer/TemporaryBuffer, passing the target DMO as a parameter to such methods (to create a new record or to test whether a record currently exists in the buffer, for instance). Although the actual worker object (RecordBuffer or TemporaryBuffer) is hidden behind the DMO, the explicit creation of the DMO makes it obvious when a buffer is created, and all scopes are opened explicitly via the RecordBuffer.openScope static method.

Note that although the RecordBuffer/TemporaryBuffer instance and the DMO it backs are technically separate objects, they are treated as interchangeable by various FWD APIs. Thus, many APIs which call for a “buffer” will accept a generic parameter. The FWD conversion will emit a DMO reference into converted code for these parameters.

There are various ways to define a record buffer in FWD. The mechanism used in any given case depends upon the 4GL code being converted. The Progress language constructs which cause buffers to be defined and the mechanisms used to define them in FWD are discussed in the following sections. The syntax diagrams for the Progress statements below are provided for convenience and reference only for the purpose of discussing the output of the FWD conversion. For additional details about the Progress language itself, please refer to the original Progress documentation.

DEFINE BUFFER

With some exceptions, most references to a table or to a field in a 4GL program will result in the implicit creation of a record buffer. To create a named buffer explicitly, the DEFINE BUFFER language statement is used. The FWD conversion supports various permutations of the DEFINE BUFFER statement.

The basic syntax for this statement is as follows:

DEFINE BUFFER
   [ [ NEW ] SHARED ] [ PRIVATE | PROTECTED ] BUFFER <buffer>
   FOR [ TEMP-TABLE ] <table>
   [ PRESELECT ]
   [ LABEL ] <label>
   [ NAMESPACE-URI ] <namespace> [ NAMESPACE-PREFIX ] <prefix>

where:

<buffer> is the name of the buffer being defined

<table> is the table for which the buffer is being defined

<label> is a label used for the buffer in error messages

<namespace> and <prefix> refer to an optional namespace associated with the buffer

While the FWD parser parses all of the possible options associated with the DEFINE BUFFER statement, the full conversion does not support every option. Specifically, the PRIVATE and PROTECTED options, which have to do with Progress' object oriented programming support, currently are ignored, as are the PRESELECT, LABEL, and namespace-related options.

Define a Buffer for a Persistent Table

In its simplest form, DEFINE BUFFER is used to define a buffer for a persistent table. For example, consider a testdb database with a customer table and the following statement.

Example 1

4GL code:

DEFINE BUFFER x-customer FOR customer.

Converted code:

Customer.Buf xCustomer = RecordBuffer.define(Customer.Buf.class, "testdb", "xCustomer", "x-customer");

Details:

The above conversion assumes default name conversion rules are in use. The xCustomer variable appears as an instance member of the Java class representing the converted, external procedure.

The Java code declares and initializes a variable xCustomer of type Customer.Buf. Customer is the DMO interface which represents records from the customer table in the testdb database. Customer.Buf is an inner interface declared within the generated Customer DMO interface, which extends the Customer DMO interface and the com.goldencode.p2j.persist.Buffer interface. A label of "xCustomer" is used to keep track of the DMO within the runtime.

Note the use of the RecordBuffer static method:

public static <T> T define(Class<T> dmoBufIface,
                           String   database,
                           String   variable,
                           String   legacyName)

This method accepts:

  • An interface type, which represents the combined methods of the DMO interface and of the com.goldencode.p2j.persist.Buffer interface. A proxy will be returned which implements this combined interface.
  • the logical name or alias of the associated database.
  • The name of the variable which represents this DMO. This parameter must be unique within the context of the containing class (i.e., the Java class which corresponds to the converted, external procedure). It uniquely identifies the DMO within the currently executing program for purposes of scope management, and internal housekeeping functions.
  • The legacy name of the buffer in the original 4GL code. This may be needed for error reporting and to support legacy functionality related to the buffer.

The RecordBuffer.define method returns a DMO proxy which implements the specified dmoBufIface interface (in this example, Customer.Buf) and which is linked to a new instance of the RecordBuffer class. The RecordBuffer instance is not exposed to application logic. Instead, business logic uses this proxy to invoke methods of both the Customer DMO interface (i.e., getter/setter methods for properties of the Customer DMO) and the com.goldencode.p2j.persist.Buffer interface. The latter interface declares methods which represent 4GL handle-based methods which can be invoked on a BUFFER handle. This approach allows business logic to access a database record's data and to invoke BUFFER methods on the same Data Model Object, using natural, object-oriented syntax.

Define a Buffer for a Temp-Table

When the DEFINE BUFFER statement is used with temporary tables, the converted outcome differs.

Example 2

Consider the following temp-table definition:

DEFINE TEMP-TABLE tmp-customer
   FIELD cust-num AS INT
   FIELD name AS CHAR.

Now consider the following DEFINE BUFFER statement which - as in Example 1 above - is in its simplest form (i.e., no options):

DEFINE BUFFER x-customer FOR tmp-customer.

...or its functional equivalent:

DEFINE BUFFER x-customer FOR TEMP-TABLE tmp-customer.

Note that the TEMP-TABLE keyword above is syntactic sugar, which is not considered by the conversion rules.

TODO: ECF review
Converted code:

TempRecord1 xCustomer = TemporaryBuffer.define(TempRecord1.class, "xCustomer", false);
* * * Conversion is actually the below, so narrative needs examination. It looks like they 2nd uses the first. * * *
TmpCustomer_1_1.Buf tmpCustomer = TemporaryBuffer.define(TmpCustomer_1_1.Buf.class, "tmpCustomer", "tmp-customer", false);
TmpCustomer_1_1.Buf xCustomer = TemporaryBuffer.define(tmpCustomer, "xCustomer", "x-customer");

TODO: ECF review
Details:

FWD converts both of these 4GL statements into the same Java code. Even though the DEFINE BUFFER statement here is nearly identical to Example 1, the fact that tmp-customer represents a temp-table rather than a persistent table triggers a different converted result.

An instance variable of xCustomer of type TempRecord1 is declared and initialized. TempRecord1 is the DMO interface representing records of the converted tmp-customer temp-table 1 . A label of "xCustomer" is used by the runtime to manage this DMO and its associated buffer.

In this case, we use the following, static method of TemporaryBuffer to define the DMO and to create a backing buffer:

public static <T extends Temporary> T define(Class<T> dmoIface,
                                             String   variable,
                                             boolean  global)

This method accepts the type (class) of the DMO to be created, the name of the variable which represents the DMO (again, this must be unique within the context of the enclosing program), and a boolean flag indicating whether the temporary table backing the DMO is global (i.e., survives beyond the end of the external procedure in which it is defined).

The global parameter is always false for a converted DEFINE BUFFER statement. This parameter exists because this TemporaryBuffer.define method also is used by the FWD conversion when defining the implicit, default buffer associated with a DEFINE TEMP-TABLE statement. The latter statement has an option to define a temporary table as global.

In this case, since the backing table is a temp-table, the buffer object for the created DMO is an instance of the TemporaryBuffer class rather than the RecordBuffer class.

1 This example assumes x-customer was the first temp-table encountered during conversion. The naming convention for temp-table DMO types uses the template TempRecordN, where the value of N is determined by a counter managed by the conversion logic. Each new, unique, temp-table definition encountered causes this counter to be incremented. See the Data Model Objects chapter for additional information.

Define a Shared Buffer

See Shared Buffers.

DEFINE TEMP-TABLE (WORK-TABLE, WORKFILE)

Defining a temp-table (or work-table/workfile) in Progress code implicitly defines a default buffer with the same name as the temp-table. The FWD conversion makes these buffer definitions explicit, using the TemporaryBuffer.define method.

For the purpose of this discussion, the portions of the DEFINE [TEMP-TABLE | WORK-TABLE | WORKFILE] statements which define the structure of temp-tables are ignored. This structural aspect of temp-table conversion is handled during parsing and schema conversion; please refer to the Schema Conversion chapter of this document for those details.

The beginning portion of the syntax for the DEFINE TEMP-TABLE Progress language statement is relevant to buffer creation:

DEFINE [ [ NEW [ GLOBAL ] ] SHARED ] TEMP-TABLE <temptable> [ NO-UNDO ]
...

where <temptable> is the name of the temp-table being defined, as well as the name of the default buffer which will be defined.

Example 3

4GL Code:

DEFINE TEMP-TABLE tmp-customer
...

Converted code:

TmpCustomer_1_1.Buf tmpCustomer = TemporaryBuffer.define(TmpCustomer_1_1.Buf.class, "tmpCustomer", "tmp-customer", false);

Details:

This (partial) 4GL temp-table definition creates an implicit, default record buffer named tmp-customer.

The FWD conversion, in addition to defining a DMO interface and an implementation class in Java, and a Hibernate .hbm.xml mapping document, will declare and initialize a DMO as a member instance variable of the converted class. The tmpCustomer DMO is backed by an internally linked record buffer object.

The naming in this example again assumes default name conversion rules, and further assumes this is the first temp-table encountered during conversion.

For conversion of shared temp-tables, see Shared Temp-Tables section.

Record Buffers as Parameters

The 4GL procedures and functions can define a record buffer as parameter, beside variables. Depending on how a buffer is defined as a parameter, the associated parameter in the called procedure or function might be initialized to hold a reference to the passed record buffer, allowing the function or procedure to have access to the record currently loaded in the buffer.

When defining buffer parameters using the DEFINE PARAMETER BUFFER bufname FOR table (for procedures) and BUFFER bufname FOR table (for functions) statements, the resulting buffer will be:

  1. explicitly defined.
  2. assigned based on a parameter rather than causing a new memory allocation.
  3. scoped to the procedure or function to which they apply. This means that there will not be multiple scopes inside this block (for that buffer).

Buffers as Procedure Parameters

For procedures, buffer parameters can be defined using either the DEFINE PARAMETER BUFFER statement (which can define a parameter for a temporary or database table, always in input-output mode) or the DEFINE PARAMETER TABLE statement (which can be used to define parameters only for temporary tables).

The syntax of the DEFINE PARAMETER BUFFER statement is:

DEFINE PARAMETER BUFFER <buffername> FOR [TEMP-TABLE] <table> [PRESELECT].

This statement is used to create an alias for an existing buffer, which will be used within the procedure. When the procedure (internal or external) is executed, all changes to the buffer defined as parameter will reflect in the buffer instance which was passed to that parameter, if the procedure commits the transaction. For this statement, it is required for the passed buffer instance and the buffer parameter to be of the same type (i.e. they reference the same temporary or database table).

The syntax of the DEFINE PARAMETER TABLE statement is:

DEFINE <mode> PARAMETER TABLE FOR <temp_table_name> [table_options].

and is used to associated a temporary table with another one; it is not required for the passed buffer reference and the defined buffer parameter to be for the same temporary table - 4GL will match and use only the fields in the table which have the type and index. The mode can have one of the INPUT, OUTPUT or INPUT-OUTPUT values; if missing, it defaults to INPUT mode. The only supported value for table_options is APPEND. Depending on the mode, during initialization the destination table will have its records deleted (on input or input-output mode) or all created and changed records copied back to the table referenced by the passed buffer (on output or input-output mode).

Example 4

4GL code:

...
procedure testProc1:
   define parameter buffer x-book-1 for book.
   ...
end.
...
run testProc1(buffer book).
...

Converted code:

Book.Buf book = RecordBuffer.define(Book.Buf.class, "p2j_test", "book", "book");
...
public void testProc1(final Book.Buf _xBook1)
{
   Book.Buf xBook1 = RecordBuffer.defineAlias(Book.Buf.class, _xBook1, "xBook1", "x-book-1");

   internalProcedure(new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook1);
   }));
}
...
RecordBuffer.openScope(book);
ControlFlowOps.invokeWithMode("testProc1", "B", book);
...

TODO: ECF review
Details:

This example adds to the procedure the buffer parameter xBook1, which maps to the database table book. In the converted code, beside emitting the procedure's buffer parameter, another buffer is emitted as an instance field for the converted external procedure. The instance field will be assigned to the buffer instance passed as parameter, when the internal procedure is executed; this is needed because the buffer parameter can end up being used in 4GL code which converts to inner classes, where the procedure's parameters are not visible; by “promoting” the parameter as an instance field, we expose the buffer instance to any other code which might need it.

The testProc1(book) call will invoke the testProc1 procedure, which needs a buffer parameter; the converted code will emit the converted buffer name, which in this case is book. This must be for the same database table as the procedure's parameter.

Example 5

4GL code:

define temp-table tt-book like book.
...
procedure testProc2:
   define parameter buffer t-book-2 for temp-table tt-book.
   ...
end.
...
run testProc2(buffer tt-book).
...

Converted code:

...
TempRecord1 ttBook = TemporaryBuffer.define(TempRecord1.class, "ttBook", false);
TempRecord1 tBook2;
...
public void testProc2(final TempRecord1 tBook2)
{
  internalProcedure(new Block()
  {
      public void body()
      {
         RecordBuffer.openScope(tBook2);
         TestProc.this.tBook2 = tBook2;
         ...
      }
  });
}
...
testProc2(ttBook);
...

TODO: ECF review
Now is:
   TtBook_1_1.Buf ttBook = TemporaryBuffer.define(TtBook_1_1.Buf.class, "ttBook", "tt-book", false);
...
   public void testProc2(final TtBook_1.Buf _tBook2)
   {
      TtBook_1_1.Buf tBook2 = RecordBuffer.defineAlias(TtBook_1_1.Buf.class, _tBook2, "tBook2", "t-book-2");

      internalProcedure(new Block((Body) () -> 
      {
         RecordBuffer.openScope(tBook2);
      }));
   }
...
   RecordBuffer.openScope(ttBook);
   ControlFlowOps.invokeWithMode("testProc2", "B", ttBook);
...

TODO: ECF review
Details:

When a buffer for temporary table is passed as a parameter, the converted code is similar to the case when a buffer for a database table is used - the buffer instance is saved in a instance field for the converted external procedure.

Example 6

4GL code:

define temp-table tt-book like book.
...
procedure testProc3:
   define temp-table tt-book-3 like book.

   define input parameter table for tt-book-3.
   ...
end.
...
run testProc3(input table tt-book).
...

Converted code:

...
TempRecord1 ttBook = TemporaryBuffer.define(TempRecord1.class, "ttBook", false);
TempRecord3 ttBook3 = TemporaryBuffer.define(TempRecord3.class, "ttBook3", false);
...
public void testProc3(final Temporary ttBook3Buf2)
{
  internalProcedure(new Block()
  {
      public void body()
      {
         TemporaryBuffer.associate(ttBook3Buf2, ttBook3, true, false);
         ...
      }
  });
}
...
testProc3(ttBook);
...

TODO: ECF review
Now is:
   TtBook_1_1.Buf ttBook = TemporaryBuffer.define(TtBook_1_1.Buf.class, "ttBook", "tt-book", false);
   TtBook3_1_1.Buf ttBook3 = TemporaryBuffer.define(TtBook3_1_1.Buf.class, "ttBook3", "tt-book-3", false);
   public void testProc3(final TableParameter ttBook3Buf2)
   {
      internalProcedure(new Block((Body) () -> 
      {
         TemporaryBuffer.associate(ttBook3Buf2, ttBook3, true, false);
      }));
   }
...
   RecordBuffer.openScope(ttBook, ttBook3);
   ControlFlowOps.invokeWithMode("testProc3", "I", new TableParameter(ttBook));
...

TODO: ECF review
Details:

When using the DEFINE PARAMETER TABLE statement to define a parameter, any buffer instance associated with a temporary table can be passed for that parameter. Thus, the converted Java method needs to have the type of these kind of parameters (ttBook3Buf2 int this case) set to the generic Temporary type, which in FWD is the root interface for all temporary DMOs; this allows any temporary buffer to be passed as a buffer parameter. The buffer name (tt-book-3 in this case) needs to be explicitly defined before the DEFINE PARAMETER TABLE statement occurs. When using this statement, if none of the INPUT, OUTPUT or INPUT-OUTPUT options are specified, then it is assumed this is an INPUT parameter.

The ttBook3 buffer, which acts as a placeholder for the defined procedure parameter, is not the argument set to the converted Java method; instead, as in the DEFINE PARAMETER BUFFER case, an instance field (ttBook3) is used to define the temporary buffer for that temporary table. The reason is the same as for the DEFINE PARAMETER BUFFER statement - the parameter specified as a buffer can end up being used in inner classes in the converted code, which don't have access to the converted Java method's arguments.

The TemporaryBuffer.associate([src-buffer], [dst-buffer], [input], [output] {, [append] }) method is used to initiate the actual buffer which acts as the parameter, ttBook3; the arguments of this method are set depending on the options set to the@ DEFINE PARAMETER TABLE.The src-buffer stands for the buffer sent as a parameter ( ttBook3Buf2 in this case) and the dst-buffer references the buffer which acts as the procedure's parameter( ttBook3 ). The association between the arguments of this method and the statement's options and all the possible ways this method can be emitted in the converted code is shown in the table bellow. Note that the table-name buffer (the parameter) will always be emitted in place of the dst-buffer in the converted call.

4GL Code Converted Code
define input parameter
table for [table-name].
TemporaryBuffer.associate([src-buffer], [dst-buffer],
true, false);
define input-output parameter
table for [table-name].
TemporaryBuffer.associate([src-buffer], [dst-buffer],
true, true);
define output parameter
table for [table-name].
TemporaryBuffer.associate([src-buffer], [dst-buffer],
false, true);
define input parameter
table for [table-name] append.
TemporaryBuffer.associate([src-buffer], [dst-buffer],
true, false, true);
define input-output parameter
table for [table-name] append.
TemporaryBuffer.associate([src-buffer], [dst-buffer],
true, true, true);

Example 7

...
define temp-table tt-book like book.
...
procedure testProc4:
   define temp-table tt-book-3 like book.

   define input-output parameter table for tt-book-4 append.
   ...
end.
...
run testProc4(input-output table tt-book).
...

Converted code:

...
TtBook_1_1.Buf ttBook = TemporaryBuffer.define(TtBook_1_1.Buf.class, "ttBook", "tt-book", false);
TtBook4_1_1.Buf ttBook4 = TemporaryBuffer.define(TtBook4_1_1.Buf.class, "ttBook4", "tt-book-4", false);
...
   public void testProc4(final TableParameter ttBook4Buf2)
   {
      internalProcedure(new Block((Body) () -> 
      {
         TemporaryBuffer.associate(ttBook4Buf2, ttBook4, true, true, true);
      }));
   }
...
RecordBuffer.openScope(ttBook, ttBook4);
ControlFlowOps.invokeWithMode("testProc4", "U", new TableParameter(ttBook));
...

TODO: ECF review
Details:

The APPEND clause can be specified only for OUTPUT and INPUT-OUTPUT parameters. When it is set for a temporary buffer parameter, the T emporaryBuffer.associate method call will set its third boolean parameter to true. In this case, as it is an INPUT-OUTPUT parameter with an APPEND clause, all the boolean parameters for the TemporaryBuffer.associate method are set to true.

Example 8

4GL code:

...
define parameter buffer x-book-a for book.
...

Converted code:

...
BookBuf xBookA;
...
public void execute(final Book.Buf _xBookA)
{
   TestProc.this.xBookA = RecordBuffer.defineAlias(Book.Buf.class, _xBookA, "xBookA", "x-book-a");

   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBookA);
   }));
}
...

TODO: ECF review
Details:

Using DEFINE PARAMETER BUFFER with an external procedure results in a converted code similar to when defining a buffer parameter for an internal procedure: the buffer received as parameter (xBookA) will be set to an instance field for the converted external procedure (TestProc.xBookA).

Example 9

4GL code:

...
define temp-table tt-book like book.
define parameter buffer t-book-a for temp-table tt-book.
...

Converted code:

...
TtBook_1_1.Buf ttBook = TemporaryBuffer.define(TtBook_1_1.Buf.class, "ttBook", "tt-book", false);
TtBook_1_1.Buf tBookA;
...
public void execute(final TtBook_1.Buf _tBookA)
{
   TestProc.this.tBookA = RecordBuffer.defineAlias(TtBook_1_1.Buf.class, _tBookA, "tBookA", "t-book-a");

   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(ttBook, tBookA);
   }));
}
...

TODO: ECF review
Details:

This examples uses a buffer for a temporary table instead of a database table, but there are no differences in how the parameter for the external procedure is used: the parameter is received using the tBookA argument for the execute method and it is saved in the TestProc.tBookA instance field.

Example 10

4GL code:

...
define temp-table tt-book like book.
define input parameter table for tt-book.
...

Converted code:

...
TtBook_1_1.Buf ttBook = TemporaryBuffer.define(TtBook_1_1.Buf.class, "ttBook", "tt-book", false);
TableParameter ttBookBuf2;
...
public void execute(final TableParameter ttBookBuf2)
{
   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(ttBook);
      TemporaryBuffer.associate(ttBookBuf2, ttBook, true, false);
   }));
}
...

TODO: ECF review
Details:

Defining a parameter for the external procedure using the DEFINE PARAMETER TABLE results in a converted code similar to the case when this statement is used to define a parameter for an internal procedure. A temporary buffer of undefined type (ttBookBuf1) is used to receive the buffer parameter when invoking the external procedure and the TemporaryBuffer.associate method call will be used to initiate the actual temporary buffer parameter (ttBook). The TemporaryBuffer.associate is emitted using the same rules as for the DEFINE PARAMETER BUFFER statement.

h4(#buffers-as-function-parameters){font-style: normal}. Buffers as Function Parameters

For functions, the parameters are specified using special clauses in their parameter list: the BUFFER [buf-name] FOR [table-name] clause is used to specify a parameter for a database table and works in the same way as the DEFINE PARAMETER BUFFER statement works with a procedure; the TABLE FOR [table-name] is used to specify a parameter for a temporary table and is converted following the same rules as the DEFINE PARAMETER TABLE statement, when used with a procedure. Note that the BUFFER FOR clause can't be used in the BUFFER [buf-name] FOR TEMP-TABLE [table-name] form, and can be used only for specifying buffer parameters for database tables.

Example 11

4GL code:

...
function testFun1 returns char (buffer x-book for book):
  ...
  return “foo”.
End.
...
message testFun1(buffer book).
...

Converted code:

...
Book.Buf book = RecordBuffer.define(Book.Buf.class, "p2j_test", "book", "book");
...
public character testFun1(final Book.Buf _xBook)
{
   Book.Buf xBook = RecordBuffer.defineAlias(Book.Buf.class, _xBook, "xBook", "x-book");

   return function(this, "testFun1", character.class, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      returnNormal();
   }));
}
...
RecordBuffer.openScope(book);
message(testFun1(book));
...

TODO: ECF review
Details:

Although the function parameters are defined using different constructs, the converted parameter looks the same as when is set to a procedure: for the BUFFER ... FOR case, an “external procedure” instance field is created which will be set to the passed buffer reference. The reason is the same: the buffer parameter may end up being used in inner classes called by the function, which don't have access to the function's arguments.

Example 12

4GL code:

...
def temp-table tt-book-2 like book.
...
function testFun2 returns char (input-output table for tt-book-2 append):
  ...
  return "bar".
end.
...
message testFun2(input-output table tt-book-2).
...

Converted code:

...
TtBook2_1_1.Buf ttBook2 = TemporaryBuffer.define(TtBook2_1_1.Buf.class, "ttBook2", "tt-book-2", false);
...
public character testFun2(final TableParameter ttBook2Buf2)
{
   return function(this, "testFun2", character.class, new Block((Init) () -> 
   {
      TemporaryBuffer.associate(ttBook2Buf2, ttBook2, true, true, true);
   }, 
   (Body) () -> 
   {
      returnNormal("bar");
   }));
}
...
RecordBuffer.openScope(ttBook2);
message(testFun2(new TableParameter(ttBook2)));
...

TODO: ECF review
Details:

When a buffer for a temporary table is defined as parameter to a function, the converted code follows the same rules as for a procedure with a parameter defined using the DEFINE PARAMETER TABLE statement. This includes the TemporaryBuffer.associate method call - it is emitted to associate the received buffer instance with the buffer defined as parameter and initiate the defined parameter accordingly, depending on the buffer mode (INPUT, OUTPUT or INPUT-OUTPUT) and the APPEND state.

Record Scopes

A record scope is the beginning and ending of a buffer (associated with the table or "record" in question) that can be used to store real rows of data. This scope determines many things such as when data is written to the database or when a new record is fetched. However, from the parser's perspective, the only reason why record scope matters is because it affects unqualified field name resolution.

Progress databases are very likely to have a large number of field names that are duplicated between tables. For this reason, these field names are not unique across the database and thus in normal circumstances, they can only be unambiguously referenced when they are qualified. Wherever there are long lists of field names, it is convenient to allow the more liberal usage of unqualified names. This is possible whenever there are one or more record scopes that are active. In this case, the list of unique unqualified fields can be expanded based on the list of those tables in scope at the time. If multiple tables are in scope, then the set of all field names that are unique across that list of tables in scope will be available for searching even if those names would normally be unavailable due to conflicts with other tables. As long as the conflicts are only with tables that are not in scope, those unqualified names can be used. If however, the same field name is used in 2 or more of the tables that are in scope, that name still needs to be qualified on lookup.

Buffers can only be scoped to an enclosing block which has the record scoping property. All block types (external procedures, internal procedures, triggers, functions, repeat and for) except DO have the record scoping property by default. Even a DO obtains record scoping when either the FOR or PRESELECT qualifier is added (these are mutually exclusive).

Internal procedures, triggers and functions additionally have namespace scoping properties such that an explicit definition of a buffer name in one of these blocks will hide the same explicit buffer name in the containing external procedure. However, if no "local" definition exists for an explicit buffer name, references in these contained blocks will cause the original definition in the external procedure to be scoped to the external procedure level itself. Please note that any explicit buffer definitions inside an internal procedure, trigger or function cannot be used outside of that block. The external procedure is different in this respect (this is the same behavior as Progress provides for variables).

If a buffer is not explicitly created, then it is created implicitly via a direct table name reference in the source code. For example, if a table named customer exists in the currently connected database, then any of the following language constructs that reference customer (e.g. CREATE customer) cause a buffer of the same name (customer) to be created. This can be confusing since some language constructs use this same name to reference the schema definition of that table, some use the name to reference the table and some use the name as a buffer. This polymorphic usage is considered an "implicit" buffer.

Whether or not a language construct creates a buffer is determined by the type of the reference. The following maps language constructs to the type of record reference:

Language Construct Type Reference Type
any field reference in an expression lvalue free reference
AMBIGUOUS record built-in function free reference
AVAILABLE record built-in function free reference
ASSIGN field = expression statement free reference
ASSIGN record statement free reference
BUFFER-COMPARE buffer TO buffer statement free reference
BUFFER-COPY buffer TO buffer statement free reference
CAN-FIND( record ... ) built-in function no reference
CAN-FIND( ... OF record ) built-in function free reference
CREATE record statement free reference
CURRENT-CHANGED record built-in function free reference
DEFINE BROWSE browse_name QUERY query_name DISPLAY record WITH ... statement free reference
DEFINE BUFFER buffer-name FOR record statement no reference
DEFINE FRAME frame-name record statement no reference
DEFINE PARAMETER BUFFER buffername FOR record statement free reference
DEFINE PARAMETER TABLE FOR temp-table-name statement no reference
DEFINE type PARAMETER TABLE-HANDLE FOR temp-table-handle statement unknown
DEFINE TEMP-TABLE temp-table-name LIKE record statement no reference
DEFINE QUERY <queryname> FOR <tablename> statement free reference
DELETE record statement free reference
DISPLAY record statement free reference
DO FOR record statement strong
DO PRESELECT record statement weak
EMPTY TEMP-TABLE record statement free reference
EXPORT record statement free reference
FIND ... record statement free reference
FOR EACH record statement weak
FOR FIRST record statement weak
FOR LAST record statement weak
FORM record statement no reference
FUNCTION func_name RETURNS type (BUFFER buffername FOR record, ...). statement free reference
FUNCTION func_name RETURNS type (TABLE FOR temp_table_name, ...). statement no reference
IMPORT record statement free reference
INSERT record statement free reference
LOCKED record built-in function free reference
NEW record built-in function free reference
ON dbevent OF record statement free reference
OPEN QUERY query ... record ... statement free reference
PROMPT-FOR record statement free reference
RAW-TRANSFER buffer TO buffer statement free reference
RECID record built-in function free reference
RELEASE record statement free reference
REPEAT FOR record statement strong
REPEAT PRESELECT record statement weak
ROWID record built-in function free reference
RUN procedure (BUFFER record, ...) statement free reference
SET record statement free reference
UPDATE record statement free reference
VALIDATE record statement free reference

There are 4 types of record references (to database tables, temp-tables or work-tables) which may cause a buffer to be created. If this reference is implicit, the resulting buffer is named the same as the table to which it refers. It is important to note that even when a reference does create a buffer (allocate memory) only a small subset of the above statements actually fill it with a record (all buffers only hold a single record at a time). Other statements (e.g. FIND, FOR or GET) may handle both the creation of the buffer and the implicit loading of that buffer with the first record.

The following sections describe each of the reference types. 3 of the 4 types can cause the creation of a new buffer. Multiple references to the same buffer are sometimes combined into a single buffer creation and at other times more than 1 buffer can be created. The following describes the rules by which the number of buffers are determined. For each buffer that is created, there is also a "record scope" that corresponds to this buffer. This scope determines the topmost block in which the buffer is available.

In the following rules, the use of "implicit" should not be confused with whether or not a buffer name has been explicitly or implicitly referenced. In many cases, Progress will implicitly create a larger scope for a buffer such that multiple references share the same buffer. This is the sense in which the word implicit should be interpreted. Most importantly, the scoping rules (both the explicit rules and the implicit expansion rules) are applied in the same exact manner for both explicitly defined buffers as for implicitly defined buffers.

No Reference

Some name references can occur in many language statements, even before these scope creating statements. For example, a DEFINE TEMP-TABLE <temp-table-name> LIKE <table_name> statement is such a reference that can occur anywhere in a procedure. No buffer is being referenced here, instead it is the schema dictionary (the data structure of the record) that is being referenced.

Strong Reference

Only two language statements (both are kinds of block definitions) can be a strong reference (DO FOR and REPEAT FOR). A new buffer is created and that buffer is scoped to the block being defined. Strong scopes are explicitly defined by the 4GL programmer and the Progress environment never implicitly changes the scope to be larger (as can happen with the next two reference types). A strong reference doesn't ever implicitly expand beyond the block that defines it.

Other reference types can be nested inside a strong scope and the scope of these references is always raised to the strong scope. Thus every buffer reference within a strong scoped block is always scoped to the same buffer.

Additionally, no free references to the same buffer are allowed outside the block defining a strong scope (the code will fail to compile). Weak references to the same buffer can be placed outside or inside a strong scope.

Weak Reference

Only three language statements (all of which are kinds of block definitions) can be a weak reference (FOR EACH, DO PRESELECT and REPEAT PRESELECT). Depending on other references (or the lack thereof) in a file, a weak reference may create a new buffer and scope that buffer to the block in which the buffer was referenced.

If a weak reference to a buffer is nested inside a block to which the buffer is strongly scoped, then this reference does not create a new buffer and is simply a reference to that same strongly scoped buffer.

If a weak reference is not nested inside a strong scope, it will create a new buffer and scope that buffer to the block definition which made the weak reference, BUT this will only happen if no unbound free references to the same buffer exist (see Free Reference below for details).

With the existence of one or more unbound free references, it is possible that the weak reference will have its scope implicitly expanded to that of a containing scope. For this to occur, the scope of the buffer will be expanded to the nearest block that can enclose the multiple references to this same buffer. The problem is that there are complex rules to determine which references (weak and free) are combined into a single buffer (and its associated scope) and which weak references are left alone.

Free Reference

Anything that is not a "no reference", strong reference or weak reference is a free reference.

If the free reference is contained inside a block to which the referenced buffer is strongly scoped, then this is simply a reference to that same buffer and no buffer creation or scope change will occur.

If the free reference is contained inside a block to which the referenced buffer is weakly scoped, then this is simply a reference to that same buffer and no buffer creation or scope change will occur. Please note that the Progress documentation incorrectly states that "a weak-scope block cannot contain any free references to the same buffer". More specifically, one cannot nest certain free references to a given buffer that is scoped to a FOR EACH block (which is the most common type of weak reference). For example, one cannot use a FIND inside a FOR EACH where both are operating on the same buffer. However, one should note that field access is a form of free reference and this is perfectly valid (as one would expect) inside a FOR EACH.

Contrary to the Progress documentation, if the weak reference is created by a DO PRESELECT or REPEAT PRESELECT, then it is required that one use one or more free references in order to read records since neither of these blocks otherwise provide record reading. In this case, even FIND statements work within such a block.

If the free reference is not nested inside a block to which the same buffer is already scoped, then this free reference may cause the scope of other free references and weak references to be implicitly expanded to a higher containing block.

In two cases (FOR FIRST and FOR LAST), a free reference is part of a block definition. In such a case, if there is no need to implicitly expand the scope for this buffer, it will be scoped to the block definition in which the free reference was made.

Much more common is for a free reference to be a standalone language statement which is not part of a block definition (e.g. any form of FIND). In such cases, whether a buffer is created or not and where that buffer is scoped are decided via a complex series of rules.

It is important to note that in the absence of free references, there will never be any implicit expansion of record scopes. Thus the free reference is the cause of implicit scope expansion.

Implicit Scope Expansion Rules

The following are the rules for scope expansion (Progress does not document these rules, but they have been determined by experience). These rules all assume that all references refer to the same implicit buffer name. The previous rules for scoping are not repeated here as these rules only reflect the logic for implicitly expanding scope. To perform the analysis described here, one must traverse the Progress source AST that the parser creates with "forward" meaning down and/or right from the current node and "backward" meaning up and/or left of the current node. To understand some of the terms in the following rules, one must think of the tree as a list. In other words, one must "flatten" (degenerate) the tree into a simple list (based on the depth first approach of traversing down the tree before traversing right). Using this conceptual list form of the tree, one can see that the "closest prior" (the most recently encountered node that matches some criteria) or "next" (the closest downstream node that matches some criteria) node can be thought of as a simple backward or forward "search" respectively. This is critical to understand these rules since much of the logic is not defined by the nesting of blocks (as would be expected) but is rather defined by the "simple" ordering of the references.

  1. No scope is ever expanded without the presence of more than 1 reference to the same buffer where at least one of the references is a free reference.
  2. Any free or weak reference that is nested inside a weak or strong scope per the above rules, is not considered in this analysis. Such references are effectively ignored.
  3. Once (due to a match with one of the following rules) an implicit scope has been assigned (expanded) to a block, this is considered an active scope. Since at least one of the references that triggered the expansion must have been a free reference, this reference is now considered bound and no longer affects any further implicit expansion decisions.
  4. The block to which a scope expansion has occurred will be called an expanded scope in subsequent rules.
  5. Weak and free references that are positioned later than statements triggering the scope expansion but which are nested inside an active scope will use that active scope and will not affect or cause any further expansions. Such free references are considered bound free references.
  6. When an unbound free reference (any free reference that is not defined as bound as noted above) is encountered, an implicit scope expansion will occur. Unbound free references are greedy since they will "reuse" or "borrow" the buffer defined elsewhere if at all possible. A backward "search" is made for the first match to any of the following (to which to "bind" the free reference):
    • The most recent prior weak reference (at the current or a higher nesting level).
    • The most recent prior expanded scope .
    • If a nested block is encountered, the first (in order of occurrence, no matter its sub-nesting dept) weak reference of that block.
  7. Whichever of these backward references is found first is the reference or scope that is used to determine the enclosing block that will be the target for the expansion. If no prior weak references or prior expanded scopes exist to which to bind, then the next weak reference after the unbound free reference will be the bind target. By definition, an expanded scope can only be found by searching backward since the source tree is processed top to bottom/left to right and it is this current processing which creates expanded scopes.
  8. Once a bind target is found, the nearest block that encloses both references (or the one free reference and the expanded scope) and which has the record scoping property will form the target of an expanded scope. This means that at a minimum, the two references (or the one free reference and the expanded scope) will both use a common buffer that is associated with the target block.
  9. A special case exists when there is 1 or more unbound free references and no weak references or expanded scopes to the same buffer. This means that the only references to this buffer are free references and there are no buffers to "borrow". Instead a buffer is created and scoped based only on the free reference(s). The result is that all of these free references will be combined into a single expanded scope at the nearest block that encloses all free references and which has the record scoping property.
  10. An expanded scope's block target can be very close to the free reference (the currently enclosing block) or it can be located at any enclosing block up to and including the external procedure block itself. The target is calculated as described in rule 8 above. No matter which block is the target it is possible that there are prior weak references and expanded scopes that are contained within the scope of the target block. The following sub-rules must be used to determine which of these weak references or expanded scopes will have their scope raised or rolled up to the new enclosing target block and which of them will retain their own independent scope. If conditions cause some of the references or expanded scopes to remain separate, then the result is a nested island which uses a separate buffer referencing the same table by the same name.
    • (a) This processing must be done in a backwards walk of the tree with the results heavily based on block nesting.
    • (b) Only weak scopes and expanded scopes will be encountered in such a backward walk, because the forward walk to this point has by its nature, already bound all free references into expanded scopes.
    • (c) The rollup process starts with the first prior weak reference or expanded scope which was not the bind target (see rules 6 and 7 above) and which is directly enclosed by the target block. In other words, the starting point must be a weak reference or expanded scope that is a direct child node of the target block.
    • (d) The rollup process ends at (and including) the closest prior expanded scope or when there are no more weak references or expanded scopes to rollup . This means that only 1 prior expanded scope will ever have its scope raised in a given rollup process. The other (counter-intuitive) result of this is that any direct children of the target block that are weak references or expanded scopes but which precede the closest prior expanded scope, are never rolled up. In other words, they become islands of unique scope nested inside a larger scope but remaining separate. The closest prior expanded scope thus forms a kind of firewall before which all previous scopes (within the same enclosing target block) are forever fixed. In other words, once this firewall has been created (just another name for an expanded scope), certain prior scopes can never be rolled up.
    • (e) All weak references that are directly enclosed by the target block and are located between the start and end points of the rollup process, will have their scope raised to the target block. They are essentially "merged" with that scope. Weak references that are more deeply nested (i.e. they are not direct child nodes of the target block) are not merged, with the single exception noted in the next sub-rule (10f).
    • (f) Besides weak references and expanded scopes that exist as direct child nodes of the target block, this rollup process can cause a counter-intuitive scope merger for weak references that are more deeply nested. This only happens if there is a block nested directly inside the target block AND there are no expanded scopes (at any level) in that nested block. In this case (where the nested block has only weak references), the first (in order of occurrence) weak reference in that nested block (even if that weak reference is deeply nested inside that nested block) will have its scope raised while all other following weak references in the nested block will remain islands. This is really a side effect of or related processing for how nested blocks are processed in rule 6 above.
    • (g) This rollup of the first weak reference in a nested block does not stop the rollup process. This means that multiple directly nested blocks in a target scope will each have their first weak reference's scope raised (assuming that each nested block contains no expanded scope). If one of these blocks contains an expanded scope, then the sub-rule (10d) above takes precedence and the first weak reference DOES NOT have its scope raised.

Implicit Scope Expansion Examples

The following are examples to show both the simple/expected behavior as well as each of the more obscure (counter-intuitive) cases. In each example the following explains how to interpret the text:

  1. Ignore the fact that these examples are completely useless (mostly just infinite loops), they are illustrative only.
  2. Only the customer buffer is being used. All scoping decisions are relative to this buffer and decisions are always independent between buffers with different names (even if they refer to the same table).
  3. Any text that is left in the color black has no record scope assigned at all. That means that the buffer(s) shown are invalid at those locations.
  4. Non-black colors are used to show scopes. Since scopes are associated with an entire block, the whole block will be colored the same except in the case where there are nested islands of different scope. In such cases the islands will be distinguished using multiple colors.

Example 12

4GL code:

message "hello".

find first customer.

Converted code:

externalProcedure(TestProc.this, new Block((Body) () -> 
{
   RecordBuffer.openScope(customer);
   message("hello");
   new FindQuery(customer, (String) null, null, "customer.recid asc").first();
}));

Details:

This is the most simple case. There is 1 free reference and the buffer created is associated with the external procedure.

Example 13

4GL code:

message "hello".

repeat:
   find first customer.
end.

Converted code:

externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      new FindQuery(customer, ... "customer.recid asc").first();
   }));
}));

Details:

Another simple case with a single free reference. The scope is assigned at the nearest enclosing block that holds all references (and which has the record scoping property). In this case, the scope is associated with the repeat block instead of the external procedure.

Example 14

4GL code:

repeat:
   repeat:
      repeat:
         repeat:
            repeat:
               find first customer.
            end.

            repeat:
               repeat:
                  find first customer.
               end.
            end.
         end.
      end.
   end.
end.

Converted code:

externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
         repeat("loopLabel2", new Block((Body) () -> 
         {
            repeat("loopLabel3", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               repeat("loopLabel4", new Block((Body) () -> 
               {
                  new FindQuery(customer, ... "customer.recid asc").first();
               }));

               repeat("loopLabel5", new Block((Body) () -> 
               {
                  repeat("loopLabel6", new Block((Body) () -> 
                  {
                     new FindQuery(customer, ... "customer.recid asc").first();
                  }));
               }));
            }));
         }));
      }));
   }));
}));

Details:

In the absence of weak references, 2 or more free references will all combine into a single scope at the nearest enclosing block that holds all references (and which has the record scoping property).

Note that the level of nesting has no impact on this result.

Example 15

4GL code:

do for customer:
   for each customer:
   end.

   find first customer.
end.

for each customer:
end.

Converted code:

externalProcedure(TestProc.this, new Block((Body) () -> 
{
   doBlock("blockLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel0", new Block((Init) () -> 
      {
         query0.initialize(customer, (... "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      new FindQuery(customer, ... "customer.recid asc").first();
   }));

   AdaptiveQuery query1 = new AdaptiveQuery();

   forEach("loopLabel1", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query1.initialize(customer, (... "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query1.next();
   }));
}));

Details:

Here, the DO FOR block establishes a strong scope. All customer references within this block are all scoped to the block. In this example, the free reference is not considered unbound because it is within the strong scope. Also note that no free references external to the same buffer are allowed outside this strong scope.

The weak reference within the strong scope is part of that scope, but the weak reference outside of the strong scope has its own buffer/record scope.

No implicit scoping will occur, everything is fixed and explicit.

Example 16

4GL code:

message "hello".

for each customer:
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");
... 
   forEach("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query0.initialize(customer, (..., "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query0.next();
   }));
}));

Details:

This weak reference is the simple case where the buffer created is associated with the FOR block.

No implicit scoping changes will occur.

Example 17

4GL code:

message "hello".

for each customer:
end.

for each customer:
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");     
...      
   forEach("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query0.initialize(customer, ((String) null), null, "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query0.next();
   }));     
...  
   forEach("loopLabel1", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query1.initialize(customer, (... "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query1.next();
   }));
}));

Details:

These 2 weak references each have their own scope where the buffer created is associated with each FOR block.

No implicit scoping changes will occur.

Example 18

4GL code:

message "hello".

do preselect each customer:
   find first customer.
   find next customer.
end.

for each customer:
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");
...        
   doBlock("blockLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query0.initialize(customer, (... "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query0.first();
      query0.next();
   }));
...
   forEach("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
      query1.initialize(customer, (... "customer.recid asc");
   }, 
   (Body) () -> 
   {
      query1.next();
   }));
}));

Details:

This is an example of two weak scopes that are sequential. Please note that while it is not possible to have a free reference inside a FOR EACH block, the PRESELECT blocks must by their nature use free references to read records.

Example 19

4GL code:

message "hello".

repeat:
   for each customer:
   end.

   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
...           
      forEach("loopLabel1", new Block((Init) () -> 
      {
         query0.initialize(customer, (... "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      new FindQuery(customer, ... "customer.recid asc").first();
   }));
}));

Details:

This is the most simple type of implicit scope expansion. The default scope for the weak reference is raised to the enclosing REPEAT block due to the presence of the FIND free reference.

The REPEAT block in this case would be considered an expanded scope in the text above.

Example 20

4GL code:

message "hello".

repeat:
   repeat:
      for each customer:
      end.
   end.
end.

find first customer.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
...
         forEach("loopLabel2", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query0.initialize(customer, (... "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query0.next();
         }));
      }));
   }));
}));

Details:

This implicit scope expansion shows that the default scope for the weak reference is raised to the external procedure due to the presence and location of the FIND free reference. An important note is that the nesting level of the FOR EACH weak reference makes no difference with this behavior. Likewise, whether or not the FIND was deeply nested has no impact. The decision to implicitly expand scope is based on the presence of an unbound free reference and the order in which the buffer references occur.

Example 21

4GL code:

message "hello".

repeat:
   find first customer.
   repeat:
      repeat:
         for each customer:
         end.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      new FindQuery(customer, ... "customer.recid asc").first();

      repeat("loopLabel1", new Block((Body) () -> 
      {
         repeat("loopLabel2", new Block((Body) () -> 
         {
...                 
            forEach("loopLabel3", new Block((Init) () -> 
            {
               query0.initialize(customer, (... "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query0.next();
            }));
         }));
      }));
   }));
}));

Details:

This implicit scope expansion shows that the default scope for the weak reference is raised to the REPEAT block even though it follows the FIND free reference. In this case, there is no prior weak reference or expanded scope to which to bind.

Example 22

4GL code:

repeat:
   repeat:
      repeat:
         for each customer.
         end.
         repeat:
            repeat:
               find first customer.
            end.
            repeat:
               repeat:
                  find first customer.
               end.
            end.
         end.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
         repeat("loopLabel2", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
         }, 
         (Body) () -> 
         {
...
            forEach("loopLabel3", new Block((Init) () -> 
            {
               query0.initialize(customer, (... "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query0.next();
            }));

            repeat("loopLabel4", new Block((Body) () -> 
            {
               repeat("loopLabel5", new Block((Body) () -> 
               {
                  new FindQuery(customer, ... "customer.recid asc").first();
               }));

               repeat("loopLabel6", new Block((Body) () -> 
               {
                  repeat("loopLabel7", new Block((Body) () -> 
                  {
                     new FindQuery(customer, ... "customer.recid asc").first();
                  }));
               }));
            }));
         }));
      }));
   }));
}));

Details:

In this case when the first free reference is encountered there is a prior weak reference AND it is at a different (higher in this case) nesting level so that changes the target block chosen.

It is important to note that following free references that are enclosed in this active scope (see rules 3 and 5 above) are automatically scoped to this same level and do not have any further impact.

Example 23

4GL code:

repeat:
   repeat:
      repeat:
         repeat:
            repeat:
               find first customer.
            end.
            repeat:
               repeat:
                  find first customer.
               end.
            end.
         end.
      end.
      for each customer.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");\
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
         repeat("loopLabel2", new Block((Body) () -> 
         {
            repeat("loopLabel3", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               repeat("loopLabel4", new Block((Body) () -> 
               {
                  new FindQuery(customer, ... "customer.recid asc").first();
               }));

               repeat("loopLabel5", new Block((Body) () -> 
               {
                  repeat("loopLabel6", new Block((Body) () -> 
                  {
                     new FindQuery(customer, ... "customer.recid asc").first();
                  }));
               }));
            }));
         }));
      }));
   }));
}));

Details:

As the tree is walked, when an unbound free reference is encountered (the first FIND), a backward search will be made. In this case, no weak references or expanded scopes are found. So the free reference will bind to the next weak reference encountered. This expands the scope to the 2nd level REPEAT block.

Example 24

4GL code:

repeat:
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
...             
         forEach("loopLabel2", new Block((Init) () -> 
         {
            query0.initialize(customer, (... "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query0.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel3", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
...              
         forEach("loopLabel4", new Block((Init) () -> 
         {
            query1.initialize(customer, (... "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, ... "customer.recid asc").first();
      }));
   }));
}));

Details:

An example of 2 expanded scopes that never combine. This separation is due to the nesting levels of the scopes.

Example 25

4GL code:

repeat:
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   repeat:
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
         find first customer.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query0 = new AdaptiveQuery();

         forEach("loopLabel2", new Block((Init) () -> 
         {
            query0.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query0.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel4", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         AdaptiveQuery query2 = new AdaptiveQuery();

         forEach("loopLabel5", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query2.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query2.next();
         }));

         repeat("loopLabel6", new Block((Body) () -> 
         {
            repeat("loopLabel7", new Block((Body) () -> 
            {
...                    
               forEach("loopLabel8", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query3.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query3.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

A more complex example that shows that nesting may stop two expanded scopes from combining at a higher level, but that no matter how deeply nested the original unbound free reference and the weak reference to which it binds, the rollup process stops at the end of the expanded scope. If no subsequent free references occur outside of the expanded scope, such that it is rolled up further, this condition holds.

Example 26

4GL code:

repeat:
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   repeat:
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
         find first customer.
      end.
   end.
end.
find first customer.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query0 = new AdaptiveQuery();

         forEach("loopLabel2", new Block((Init) () -> 
         {
            query0.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query0.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel4", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         AdaptiveQuery query2 = new AdaptiveQuery();

         forEach("loopLabel5", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query2.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query2.next();
         }));

         repeat("loopLabel6", new Block((Body) () -> 
         {
            repeat("loopLabel7", new Block((Body) () -> 
            {
               AdaptiveQuery query3 = new AdaptiveQuery();

               forEach("loopLabel8", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query3.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query3.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

A more complex example that shows that nesting may stop two expanded scopes from combining at a higher level, even when there are subsequent free references that occur outside of the parent block of both expanded scope*s, such that the last one (forming a *firewall) it is rolled up further while the first one is left separate.

Example 27

4GL code:

message "hello".

repeat:
  repeat:
      for each customer:
      end.
      find first customer.
   end.

   repeat:
      find first customer.
   end.

   repeat:
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
         find first customer.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
         AdaptiveQuery query0 = new AdaptiveQuery();

         forEach("loopLabel2", new Block((Init) () -> 
         {
            query0.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query0.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel4", new Block((Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel5", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         AdaptiveQuery query2 = new AdaptiveQuery();

         forEach("loopLabel6", new Block((Init) () -> 
         {
            query2.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query2.next();
         }));

         repeat("loopLabel7", new Block((Body) () -> 
         {
            repeat("loopLabel8", new Block((Body) () -> 
            {
               AdaptiveQuery query3 = new AdaptiveQuery();

               forEach("loopLabel9", new Block((Init) () -> 
               {
                  query3.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query3.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

The introduction of an intermediate free reference between what would have been 2 separate expanded scopes causes the first expanded scope to be raised up (as the bind target) and the entire contents of the 4th REPEAT block automatically have their scope raised because they are inside of an active scope.

Example 28

4GL code:

repeat:
   repeat:
      repeat:
         repeat:
            repeat:
               find first customer.
            end.
            repeat:
               repeat:
                  find first customer.
               end.
            end.
         end.
      end.
      for each customer.
      end.
      for each customer.
      end.
      for each customer.
      end.
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      repeat("loopLabel1", new Block((Body) () -> 
      {
         repeat("loopLabel2", new Block((Body) () -> 
         {
            repeat("loopLabel3", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               repeat("loopLabel4", new Block((Body) () -> 
               {
                  new FindQuery(customer, (String) null, null, "customer.recid asc").first();
               }));

               repeat("loopLabel5", new Block((Body) () -> 
               {
                  repeat("loopLabel6", new Block((Body) () -> 
                  {
                     new FindQuery(customer, (String) null, null, "customer.recid asc").first();
                  }));
               }));
            }));
         }));
      }));
   }));
}));

Details:

Another example of active scope processing. The number or order of subsequent enclosed references does not make any difference.

Example 29

4GL code:

message "hello".

repeat:
   for each customer:
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         for each customer:
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
      end.
   end.
   for each customer:
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel3", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      repeat("loopLabel4", new Block((Body) () -> 
      {
         repeat("loopLabel5", new Block((Body) () -> 
         {
            AdaptiveQuery query3 = new AdaptiveQuery();

            forEach("loopLabel6", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query3.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query3.next();
            }));
         }));
      }));
   }));
}));

Details:

This demonstrates the effect that nesting has (and doesn't have) on implicit scope expansion. All of the weak references in the top level REPEAT block never have their scope expanded because there is no unbound free reference at a nesting level that can affect them. However, the nested weak reference inside the 3rd level REPEAT block is the bind target for the single FIND free reference. The 2nd level REPEAT block is the nearest enclosing scope that can contain both weak and free references AND which has record scoping properties (see the next example). So the resulting expanded scope is never expanded again due to no additional unbound free references.

It is important to note that the relative nesting of the weak and free references within the 2nd level REPEAT block only affects the target block to which the scope is expanded. The relative nesting of the weak and free references does NOT affect the choice of which weak reference is bound to the free reference! Rather, this is primarily a sequence issue (it is almost always determined by the order or occurrence). The only exception is noted in rules 6a and 10f above.

Example 30

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   for each customer:
   end.
   for each customer:
   end.
   do:
      repeat:
         for each customer:
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
      end.
   end.
   for each customer:
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel3", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      blockLabel0:
      {
         repeat("loopLabel4", new Block((Body) () -> 
         {
            AdaptiveQuery query3 = new AdaptiveQuery();

            forEach("loopLabel5", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query3.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query3.next();
            }));
         }));
      }
   }));
}));

Details:

The only difference between this example and the previous one is that the 2nd level REPEAT block has been changed to a DO block. Since the DO block has no record scoping property by default, this means that the nearest enclosing block which has this property is the top level REPEAT block. Once the scope expands to this level (due to the FIND free reference), the previous weak references that are direct children of the target REPEAT block have their scope expanded too. Once that expanded scope is active, the subsequent weak references (the last one in the file) is automatically expanded too.

Example 31

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         for each customer:
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
      end.
   end.
   for each customer:
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel3", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      repeat("loopLabel4", new Block((Body) () -> 
      {
         repeat("loopLabel5", new Block((Body) () -> 
         {
            AdaptiveQuery query3 = new AdaptiveQuery();

            forEach("loopLabel6", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query3.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query3.next();
            }));
         }));
      }));
   }));
}));

Details:

This is an example of the exception in rule 10f above. 2 expansions occur, in series. The first is due to the deeply nested FIND free reference. This expands scope to the 2nd level REPEAT block and binds to the 4th instance of FOR EACH. Then when the 2nd FIND free reference is encountered, it binds to the 5th FOR EACH, raises scope to the top-level REPEAT block and then searches backward to expand the scope of prior weak or expanded scopes. In this case it stops its search when it encounters the first expanded scope (this is an example of the firewall described in rule 10d above). This means that the previous weak scopes each remain separate and independent (islands) but enclosed within a larger scope of the same buffer name.

Example 32

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         for each customer:
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
      end.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      repeat("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel4", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      AdaptiveQuery query3 = new AdaptiveQuery();

      forEach("loopLabel5", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query3.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query3.next();
      }));

      repeat("loopLabel6", new Block((Body) () -> 
      {
         repeat("loopLabel7", new Block((Body) () -> 
         {
            AdaptiveQuery query4 = new AdaptiveQuery();

            forEach("loopLabel8", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query4.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query4.next();
            }));
         }));
      }));
   }));
}));

Details:

This demonstrates that a free reference can bind to the closest prior expanded scope. That expanded scope also performs the firewall function, causing all prior weak references and expanded scopes at that level to remain scoped as they are (no expansion of their scope to a higher level).

Example 33

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   repeat:
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
         repeat:
            repeat:
               find first customer.
            end.
         end.
      end.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      repeat("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel4", new Block((Body) () -> 
      {
         AdaptiveQuery query2 = new AdaptiveQuery();

         forEach("loopLabel5", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query2.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query2.next();
         }));

         AdaptiveQuery query3 = new AdaptiveQuery();

         forEach("loopLabel6", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query3.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query3.next();
         }));

         repeat("loopLabel7", new Block((Body) () -> 
         {
            repeat("loopLabel8", new Block((Body) () -> 
            {
               AdaptiveQuery query4 = new AdaptiveQuery();

               forEach("loopLabel9", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query4.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query4.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

This demonstrates that the closest prior expanded scope can be inside a nested block. That expanded scope still performs the firewall function, causing all prior weak references and expanded scopes at that level to remain scoped as they are (no expansion of their scope to a higher level).

In the backwards search, the prior expanded scope is found before the weak reference (the 4th FOR EACH), which is why the prior expanded scope is the bind target for the last FIND free reference and why the nested FOR EACH weak references are their own independent scopes.

Example 34

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   repeat:
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
         repeat:
            repeat:
               find first customer.
            end.
         end.
      end.
      for each customer:
      end.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      repeat("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      repeat("loopLabel4", new Block((Body) () -> 
      {
         AdaptiveQuery query2 = new AdaptiveQuery();

         forEach("loopLabel5", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query2.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query2.next();
         }));

         AdaptiveQuery query3 = new AdaptiveQuery();

         forEach("loopLabel6", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query3.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query3.next();
         }));

         repeat("loopLabel7", new Block((Body) () -> 
         {
            repeat("loopLabel8", new Block((Body) () -> 
            {
               AdaptiveQuery query4 = new AdaptiveQuery();

               forEach("loopLabel9", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query4.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query4.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

The closest prior weak reference in this case is the bind target for the final free reference. However we can still see that the rollup process forces the closest prior expanded scope to be raised and that expanded scope performs the firewall function. Thus all prior weak references and expanded scopes in the nested 2nd level REPEAT block AND at the 1st level REPEAT block level to remain scoped as they are (no expansion of their scope to a higher level).

In the backwards search, the nested prior expanded scope is found as the first bind target. This is because a nested weak reference (except for the rule 10f above) cannot be a bind target.

Example 35

4GL code:

repeat:
   for each customer:
   end.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      for each customer:
      end.
      for each customer:
      end.
      for each customer:
      end.
      repeat:
         repeat:
            for each customer:
            end.
         end.
      end.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      repeat("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
      }, 
      (Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel4", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      AdaptiveQuery query3 = new AdaptiveQuery();

      forEach("loopLabel5", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query3.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query3.next();
      }));

      repeat("loopLabel6", new Block((Body) () -> 
      {
         AdaptiveQuery query4 = new AdaptiveQuery();

         forEach("loopLabel7", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query4.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query4.next();
         }));

         AdaptiveQuery query5 = new AdaptiveQuery();

         forEach("loopLabel8", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query5.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query5.next();
         }));

         AdaptiveQuery query6 = new AdaptiveQuery();

         forEach("loopLabel9", new Block((Init) () -> 
         {
            RecordBuffer.openScope(customer);
            query6.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query6.next();
         }));

         repeat("loopLabel10", new Block((Body) () -> 
         {
            repeat("loopLabel11", new Block((Body) () -> 
            {
               AdaptiveQuery query7 = new AdaptiveQuery();

               forEach("loopLabel12", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query7.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query7.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

This shows that the last free reference binds to the first weak reference in a nested block (the 3rd REPEAT block, see rule 6c above). But also note that the all prior weak references and the closest prior expanded scope also get rolled up. That expanded scope forms a firewall which leaves the 1st FOR EACH as an island.

All weak references in the nested block (the 3rd REPEAT block) after the first one, are not _rolled up.

_The key points here are:

  1. The bind target is selected from 3 possible choices per rule 6 above (prior weak references at the same nesting level, the presence at any nesting level of an expanded scope or the first weak reference in a nested block). The first one encountered is chosen.
  2. Once the bind target is found, the rollup process extends from the point of the bind target until all prior direct child weak references (or first weak references in a nested block) are rolled up OR until the closest prior expanded scope is rolled up (the firewall).

Example 36

4GL code:

repeat:
   for each customer.
   end.
   for each customer.
   end.
   repeat:
      repeat:
         for each customer.
         end.
         repeat:
            for each customer.
            end.
            find first customer.
         end.
         repeat:
            for each customer.
            end.
            find first customer.
         end.
         repeat:
            for each customer.
            end.
            find first customer.
         end.
         for each customer.
         end.
      end.
      for each customer.
      end.
   end.
   for each customer.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         repeat("loopLabel4", new Block((Body) () -> 
         {
            AdaptiveQuery query2 = new AdaptiveQuery();

            forEach("loopLabel5", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query2.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query2.next();
            }));

            repeat("loopLabel6", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               AdaptiveQuery query3 = new AdaptiveQuery();

               forEach("loopLabel7", new Block((Init) () -> 
               {
                  query3.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query3.next();
               }));

               new FindQuery(customer, (String) null, null, "customer.recid asc").first();
            }));

            repeat("loopLabel8", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               AdaptiveQuery query4 = new AdaptiveQuery();

               forEach("loopLabel9", new Block((Init) () -> 
               {
                  query4.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query4.next();
               }));

               new FindQuery(customer, (String) null, null, "customer.recid asc").first();
            }));

            repeat("loopLabel10", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
            }, 
            (Body) () -> 
            {
               AdaptiveQuery query5 = new AdaptiveQuery();

               forEach("loopLabel11", new Block((Init) () -> 
               {
                  query5.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query5.next();
               }));

               new FindQuery(customer, (String) null, null, "customer.recid asc").first();
            }));

            AdaptiveQuery query6 = new AdaptiveQuery();

            forEach("loopLabel12", new Block((Init) () -> 
            {
               RecordBuffer.openScope(customer);
               query6.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query6.next();
            }));
         }));
      }));
   }));
}));

Details:

Another example of how the rollup process stops processing when it encounters the closest prior expanded scope, even if this is deeply nested in sub-blocks.

Example 37

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   find first customer.
   repeat:
      for each customer:
      end.
      find first customer.
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         for each customer:
         end.
      end.
      repeat:
         repeat:
            find first customer.
         end.
      end.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      new FindQuery(customer, (String) null, null, "customer.recid asc").first();

      repeat("loopLabel2", new Block((Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));

         new FindQuery(customer, (String) null, null, "customer.recid asc").first();
      }));

      AdaptiveQuery query2 = new AdaptiveQuery();

      forEach("loopLabel4", new Block((Init) () -> 
      {
         query2.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query2.next();
      }));

      AdaptiveQuery query3 = new AdaptiveQuery();

      forEach("loopLabel5", new Block((Init) () -> 
      {
         query3.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query3.next();
      }));

      repeat("loopLabel6", new Block((Body) () -> 
      {
         repeat("loopLabel7", new Block((Body) () -> 
         {
            AdaptiveQuery query4 = new AdaptiveQuery();

            forEach("loopLabel8", new Block((Init) () -> 
            {
               query4.initialize(customer, ((String) null), null, "customer.recid asc");
            }, 
            (Body) () -> 
            {
               query4.next();
            }));
         }));
      }));
   }));
}));

Details:

The introduction of a free reference early (the first FIND on line 8) in a program which causes the scope to be expanded early to the top-level REPEAT. All subsequent references within this top-level REPEAT block will scope to this level no matter the order or sub-nesting depth. This is an example of the effect of an active scope as described by rules 3 and 5 above.

Note that without this early free reference, the firewall effect will apply, as seen in the previous example.

Example 38

4GL code:

message "hello".
repeat:
   for each customer:
   end.
   find first customer.
   repeat:
      for each customer:
      end.
   end.
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         for each customer:
         end.
      end.
      for each customer:
      end.
   end.
   for each customer:
   end.
   for each customer:
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   message("hello");

   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      new FindQuery(customer, (String) null, null, "customer.recid asc").first();

      repeat("loopLabel2", new Block((Body) () -> 
      {
         AdaptiveQuery query1 = new AdaptiveQuery();

         forEach("loopLabel3", new Block((Init) () -> 
         {
            query1.initialize(customer, ((String) null), null, "customer.recid asc");
         }, 
         (Body) () -> 
         {
            query1.next();
         }));
      }));
   }));
}));

Details:

Another example of the active scope effect. Note that in this case only a single free reference exists in the entire program, but the result is the same. Also note that the sub-nesting level has no impact on whether the contained weak references (or expanded scopes if any had existed in this example) would have their scope expanded.

Example 39

4GL code:

repeat:
   for each customer.
   end.
   for each customer.
   end.
   repeat:
      repeat:
         repeat:
            for each customer.
            end.
         end.
         for each customer.
         end.
         repeat:
            for each customer.
            end.
         end.
         repeat:
            for each customer.
            end.
         end.
         for each customer.
         end.
      end.
      for each customer.
      end.
   end.
   for each customer.
   end.
   repeat:
      for each customer.
      end.
      for each customer.
      end.
   end.
   for each customer.
   end.
   find first customer.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         RecordBuffer.openScope(customer);
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         repeat("loopLabel4", new Block((Body) () -> 
         {
            repeat("loopLabel5", new Block((Body) () -> 
            {
               AdaptiveQuery query2 = new AdaptiveQuery();

               forEach("loopLabel6", new Block((Init) () -> 
               {
                  RecordBuffer.openScope(customer);
                  query2.initialize(customer, ((String) null), null, "customer.recid asc");
               }, 
               (Body) () -> 
               {
                  query2.next();
               }));
            }));
         }));
      }));
   }));
}));

Details:

This is an important example of the rollup process. There is only a single free reference (the FIND statement at the end) and it binds to the FOR EACH just prior to it. The rollup process occurs from there, and since there is no prior expanded scope to act as a firewall, the rollup continues all the way up to the top of the enclosing top-level REPEAT. Both direct child weak references and the first occurring weak references in 2 nested blocks are rolled up. Note that due to nesting 6 weak scopes do NOT get rolled up.

Example 40

4GL code:

repeat:
   for each customer:
   end.
   for each customer:
   end.
   repeat:
      repeat:
         repeat:
            find first customer.
         end.
      end.
      repeat:
         for each customer:
         end.
      end.
   end.
   for each customer:
   end.
end.

Converted code:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");
...
externalProcedure(TestProc.this, new Block((Body) () -> 
{
   repeat("loopLabel0", new Block((Init) () -> 
   {
      RecordBuffer.openScope(customer);
   }, 
   (Body) () -> 
   {
      AdaptiveQuery query0 = new AdaptiveQuery();

      forEach("loopLabel1", new Block((Init) () -> 
      {
         query0.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query0.next();
      }));

      AdaptiveQuery query1 = new AdaptiveQuery();

      forEach("loopLabel2", new Block((Init) () -> 
      {
         query1.initialize(customer, ((String) null), null, "customer.recid asc");
      }, 
      (Body) () -> 
      {
         query1.next();
      }));

      repeat("loopLabel3", new Block((Body) () -> 
      {
         repeat("loopLabel4", new Block((Body) () -> 
         {
            repeat("loopLabel5", new Block((Body) () -> 
            {
               new FindQuery(customer, (String) null, null, "customer.recid asc").first();
            }));
         }));
      }));
   }));
}));

Details:

This helps demonstrate the sequential nature of the bind target decision. The 1st free reference is encountered deeply nested inside the 4th REPEAT block. It searches backward and binds to the 2nd FOR EACH (just inside the top-level REPEAT block). The rollup process then raises the scope for the 1st FOR EACH to the same top-level REPEAT block and ends. Now the last 2 weak references are both contained inside an active scope, so they automatically get their scope raised too.

If the FIND free reference and the 3rd FOR EACH were transposed in sequence, the result would have been 4 separate scopes, rather than 1 single scope. Note that nesting in this case has no affect, only the sequence mattered.

Nesting Scopes

In Progress, there are various restrictions when nesting two or more buffer scopes. This depends on the type of the nested scopes and whether the same buffer is used in multiple, nested scopes. When nesting two scopes referring to the same table, the restrictions are related to the type of the scopes: 2 weak scopes or 2 strong scopes can't be nested and also a strong scope cannot be tested inside a weak scope for the same buffer. On the other hand, it is possible to nest a weak scope inside a strong scope where both are referencing the same table. Note that if a weak scope is nested inside a strong scope, that weak scope cannot be expanded by a free reference - the strong scope binds to the specified block and cannot (by definition) ever be expanded.

Record scopes create a schema namespace issue because while a record is in scope, unqualified field names resolve which would normally be ambiguous. In these cases, the parser needs to disambiguate the unqualified field name and make sure the field will be associated with the correct buffer. For this, the subset of unique field names (that are unique across those tables actively in scope) are added to the namespace and any common (non-unique across the active scopes) names are removed from the namespace, each time a new nested scope is encountered. As tables are added and removed from the active scope list, each such change can both add and remove from the namespace, and adds to the complexity of the field name lookup algorithm. This is complicated by the fact that the algorithm to decide upon the block to which a given buffer is scoped cannot be implemented in a single-pass parser.

The primary difference between these scopes seems to be in how the scopes get implicitly expanded based on other database references in containing blocks. Normally a scope is limited to the nearest block in which the scope was created. Strong scopes can be removed from namespace consideration as soon as the this block ends. Weak scopes and free references can both be expanded to the containing block depending on a complex set of conditions. Note that within the naturally scoped block, normally ambiguous unqualified field names are made unambiguous. The problem is that the parser must properly detect when the scope is implicitly expanded and know when to remove the scope. The parser cannot simply leave all weak and free reference scopes in scope permanently. This is because in a complex procedure, as scopes are removed, some names are removed but some names may also be added back! This is due to the fact that adding a scope while other scopes are still active will remove any field names that are in common between all active scopes. So, removing that scope, re-adds those removed names to the namespace. Since the namespace can expand when a scope is removed, it is critical to detect this exact point and implement this removal otherwise the parser will fail to lookup names that should not be ambiguous.

The approach has been to use the SchemaDictionary namespace to keep track of scopes. At the start of every for, repeat or do block a scope is added and at the end, this scope is removed. This stack of scopes handles the nesting of names properly. Then the parser makes a very big simplifying assumption: every record (table, buffer, work-table or temp-table) reference is automatically "promoted" to the current scope (the top of the stack of scopes). This allows any tables in the current scope to take precedence for unqualified field lookups and then tables in the next scope down take precedence and so on until the name is resolved (remember: valid Progress code will ALWAYS have only unambiguous names so it is only a matter of where one finds it). The problem in this simplistic approach is that Progress may not always promote as we do. Thus in our implementation, there may be promoted tables that wouldn't otherwise be there. This may cause some names that would have been unambiguous (with a more limited set of tables in scope) to now be ambiguous. We don't have an example of such a situation but if it does occur, the implementation may have to get much more smart about when to and when not to promote.

It is very important to note that there is only ever 1 buffer (memory allocation) in each given procedure (internal or external), function or trigger, even though there may be multiple buffer scopes. The scopes affect record-release but the same memory is used for storage of records read in all contained scopes and Progress doesn't restore the state of the buffer when a nested scope is complete. This means that when a nested scope is added, the record that had been there is released at the end of that scope. Once that record is released, there is no record in the buffer. If there is downstream processing within the outer scope that expects the record to be there, it will find the record missing.

Unless a record is explicitly defined in an internal procedure, function or trigger, the buffer creation will occur at the external procedure level and this same buffer will be in use the entire scope of the external procedure. In the case where an internal procedure, function or trigger does have an explicit buffer defined, this results in namespace hiding and an additional (local) memory allocation. Buffer names can be the same with a buffer in the external procedure, but any explicit definition will create a local instance of that buffer. Nested scopes that operate on that buffer will have no affect on the buffer of the same name in an enclosing external procedure.

Multiple scopes are possible with explicitly defined buffers, just as they are with implicit buffers, even though there is only ever 1 memory allocation per procedure, trigger or function. The exception is for shared buffers that are imported (e.g. DEFINE SHARED BUFFER b FOR customer) rather than allocated locally. In this case, there is only ever a single scope for this buffer and that scope is set to the external procedure. Access to the shared buffer is imported once and is accessible for the lifetime of the procedure. In the cases of NEW SHARED and NEW GLOBAL SHARED buffers, these do cause a memory allocation and they can have multiple scopes just as any other explicit buffer or as the implicit buffers.

Following examples demonstrate how an explicit buffer defined in an internal procedure, function or trigger is scoped only to that internal procedure, function or trigger and they can't be used outside of that block.

Example 41

procedure testProc1:
  define buffer x-book for book.
  find first x-book.
  return x-book.book-title.
end.

Converted code:

public void testProc1()
{
   Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");

   internalProcedure(new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
      returnNormal(xBook.getBookTitle());
   }));
}

Details:

The explicit buffer x-book is defined in the testProc1 internal procedure. As there is no other buffer defined with the same name in the external procedure or other function or internal procedure, the converted buffer name uses only the name as defined in the legacy 4GL code.

Example 42

define buffer x-book for book.
find first x-book.
message x-book.book-title.
...
procedure testProc2:
  define buffer x-book for book.
  find first x-book.
  return x-book.book-title.
end.

Converted code:

Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");

public void execute()
{
   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
      message((character) new FieldReference(xBook, "bookTitle").getValue());
   }));
}

public void testProc2()
{
   Book.Buf xBookBuf2 = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBookBuf2", "x-book");

   internalProcedure(new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBookBuf2);
      new FindQuery(xBookBuf2, (String) null, null, "xBookBuf2.bookId asc").first();
      returnNormal(xBookBuf2.getBookTitle());
   }));
}

Details:

In this case, although the x-book is used as a buffer name in both the external procedure and the internal procedure, they are actually two completely different buffers. In this case, to disambiguate between the two buffer names, the buffer defined in the internal procedure is added a counter suffix, resulting in the xBookBuf2 name.

Example 43

function testFun1 returns char ():
  define buffer x-book for book.
  find first x-book.
  return x-book.book-title.
end.

Converted code:

public character testFun1()
{
   Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");

   return function(this, "testFun1", character.class, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
      returnNormal(xBook.getBookTitle());
   }));
}

Details:

This example demonstrates how a buffer defined in a function gets converted to a distinct buffer, which can be accessed only from that function.

Example 44

define buffer x-book for book.
find first x-book.
message x-book.book-title.

function testFun2 returns char ():
  define buffer x-book for book.
  find first x-book.
  return x-book.book-title.
end.

Converted code:

Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");

public void execute()
{
   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
      message((character) new FieldReference(xBook, "bookTitle").getValue());
   }));
}

public character testFun2()
{
   Book.Buf xBookBuf2 = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBookBuf2", "x-book");

   return function(this, "testFun2", character.class, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBookBuf2);
      new FindQuery(xBookBuf2, (String) null, null, "xBookBuf2.bookId asc").first();
      returnNormal(xBookBuf2.getBookTitle());
   }));
}

Details:

Similar to the internal function case, when the same name is used to define a buffer in the external procedure and a function, the conversion rules will disambiguate between the two cases by converting each buffer definition to an unique name.

Example 45

on leave anywhere
do:
   define buffer x-book for book.
   find first x-book.
   message x-book.book-title.
end.

Converted code:

externalProcedure(TestProc.this, new Block((Body) () -> 
{
   registerTrigger(new EventList("leave", true), TestProc.this, () -> new Trigger()
   {
      Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");

      public void body()
      {
         RecordBuffer.openScope(xBook);
         new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
         message((character) new FieldReference(xBook, "bookTitle").getValue());
      }
   });
}));

Details:

If a buffer is explicitly defined within a trigger, then the buffer is isolated and can be used only in that trigger, similar to the internal procedure and function cases.

Example 46

define buffer x-book for book.
find first x-book.
message x-book.book-title.

on leave anywhere
do:
   define buffer x-book for book.
   find first x-book.
   message x-book.book-title.
end.

Converted code:

Book.Buf xBook = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBook", "x-book");
...
public void execute()
{
   externalProcedure(TestProc.this, new Block((Body) () -> 
   {
      RecordBuffer.openScope(xBook);
      new FindQuery(xBook, (String) null, null, "xBook.bookId asc").first();
      message((character) new FieldReference(xBook, "bookTitle").getValue());
      registerTrigger(new EventList("leave", true), TestProc.this, () -> new Trigger()
      {
         Book.Buf xBookBuf2 = RecordBuffer.define(Book.Buf.class, "p2j_test", "xBookBuf2", "x-book");

         public void body()
         {
            RecordBuffer.openScope(xBookBuf2);
            new FindQuery(xBookBuf2, (String) null, null, "xBookBuf2.bookId asc").first();
            message((character) new FieldReference(xBookBuf2, "bookTitle").getValue());
         }
      });
   }));
}

Details:

Using the same name to define two buffers, one in the external procedure and one in a trigger block, it results in two completely different buffers. To disambiguate, the conversion rules will make sure the buffer definitions are converted to unique names.


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