Data Types¶
- Data Types
All data type support is provided at the Progress v10.2B level. DATETIME
, DATETIME-TZ
, INT64
, CLASS
, and LONGCHAR
are added to supported list.
This chapter describes the data types that are available for use in business logic. There are certain data types (BLOB
, CLOB
) that are only available as a field in a database (or a temp-table). Such types are not documented in this chapter. Please see Part 3 or Part 5 for details.
In all cases, WIDGET-HANDLE
is treated as a synonym for HANDLE
. There is no separate processing or differentiation for WIDGET-HANDLE
in Progress 4GL (as far as is known to the authors) or in FWD.
Constants¶
Most constants are the literal forms of the primitive data types. For example, the text 14
in a 4GL program would be lexicographically determined to be an integer literal, which is a constant (an unchanging value). There are some special cases, where the Progress 4GL provides symbolic constants or constants that are not forms of specific data types.
Progress 4GL transparently treats the literal forms of primitive data types as instances of those respective types. Not all primitive data types have literal forms in Progress. In particular, the following types have no literal forms: CLASS
, COM-HANDLE
, HANDLE
, LONGCHAR
, MEMPTR
, RAW
, RECID,
ROWID
and WIDGET-HANDLE
.
The following describes how each literal is emitted, but for full details on the behavior of each data type, please see the Variables section of this chapter.
CHARACTER¶
All known forms of string literals are supported in FWD. Both the '
and "
forms are handed, including all the possible escaped quotes and other escaped characters.
Use of international characters is supported as long as they are properly interpreted by the default locale of the JVM in which FWD runs. This default locale (e.g. set through the LANG
environment variable on Linux) must be defined for the JVM in which the FWD conversion process runs as well as the JVM in which the runtime environment runs.
As in Progress 4GL, the preprocessor is responsible for certain transformations. Among other changes, preprocessor argument/definition references are expanded, unescaped newlines are removed and escaped null characters modify the rest of the string. For full details, please see the String Literal section of the Preprocessor chapter in Part 1 of this book. The transformed result is written to the cache file, which is the fully preprocessed version of the source file that is read as input by the Progress 4GL parsing logic of FWD.
The subsequent FWD conversion converts a Progress 4GL compatible string to the equivalent Java source string, by stripping off enclosing quotes (including any string options) and converting the contents (including rewriting all escape sequences). Note that the output format is designed to be written to a Java source file, which requires a different approach than writing a string that can be used at runtime.
Escaped characters (using either the tilde ~
or the backslash \
character) and special formations are converted to native representations and any characters that need to be escaped in Java are so escaped. In particular, almost all usage of the backslash is compatible between Progress and Java, so only those constructs that must be different will be handled differently.
The following conversions will be made:
Input Characters | Output Characters | Description |
---|---|---|
~b | \\b | backspace |
~t | \\t | horizontal tab |
~n | \\n | linefeed |
~f | \\f | formfeed |
~r | \\r | carriage return |
~" | \\" | double quote |
~' | \\' | single quote |
~\ | \\ | backslash |
~~ | ~ | escaped tilde converts to a single tilde character |
~{ | { | left curly brace |
~E | \\033 | ASCII escape character |
~nnn | \\nnn | octal escape sequence (nnn is a 3 digit octal number) |
'' | \\' | single quote (only when inside a string that was enclosed by single quotes) |
"" | \\" | double quote (only when inside a string that was enclosed by double quotes) |
\~ | ~ | escaped tilde converts to a single tilde character |
\\ | \\ | backslash |
\{ | { | left curly brace |
\E | \\033 | ASCII escape character |
Any other escape sequence that is unrecognized will result in the tilde or backslash being dropped and the following character output into the string unchanged which is the same behavior as Progress 4GL.
The final result when output will be enclosed inside a pair of double quotes. Note that a Progress 4GL string literal is always emitted as a Java string literal, even if the result is only a single character. There is no difference in the 4GL between a single character and a string, and FWD maintains this logic for the converted code.
The decision to emit the Java string literal for Progress 4GL string literals is one that makes a direct and simple conversion. However, this has the implication that any 4GL replacement interface that could be called in Progress 4GL with either a character or a string literal, must have method signatures that allow for both String
and com.goldencode.p2j.util.character
types.
The simple example of the character constant. The following 4GL code:
def var chVar as char initial "The text constant".
Will be converted to Java as:
@LegacySignature(type = Type.VARIABLE, name = "chVar") character chVar = UndoableFactory.character(); [...] chVar.assign("The text constant");
DATE¶
There is no date literal in Java. A new instance of the com.goldencode.p2j.util.date
is created instead. This means that the use of a date literal will generate the date.fromLiteral(...)
with no name and no way to modify the value of that instance. It is not stored as a member of the class nor is it stored as a local variable.
All known forms of the date literal are supported in FWD. For details, please see the Date Literals section of the Lexer chapter in Part 1 of this book.
In the most common form of a date literal, the literal always has a value that is not unknown. Assuming that the code passes 4GL compilation, the value must be a valid date literal. The lexed text for the literal will be converted to a Java string literal (enclosed directly in double quotes) and that string will be passed to the date static method.
This Progress 4GL code:
if my-date-var > 12-31-1999 then
will result in this Java code:
if (_isGreaterThan(myDateVar, date.fromLiteral("12-31-1999"))
The only exception to this conversion is in the special case of the TODAY
"function" inside a variable initializer. Variable initializers are documented as always needing to be constants. But there is an undocumented feature that allows TODAY
to be used as a "constant". There is a special date static method used to obtain a date instance with today's date: date.today()
. The default constructor will quickly build an unknown object.
This Progress 4GL code:
DEFINE VARIABLE my-date-var1 AS DATE INIT TODAY. DEFINE VARIABLE my-date-var2 AS DATE.
will result in this Java code:
@LegacySignature(type = Type.VARIABLE, name = "my-date-var1") date myDateVar1 = UndoableFactory.date(date.today()); @LegacySignature(type = Type.VARIABLE, name = "my-date-var2") date myDateVar2 = UndoableFactory.date();
DATETIME and DATETIME-TZ¶
Like DATE
datatype, there are no DATETIME
and DATETIME-TZ
literals in java language. Each occurrence of such literals will be converted in calls to static methods datetime.fromLiteral()
and datetimetz.fromLiteral()
.
All known forms of the datetime literal are supported in FWD. For details, please see the Date Literals section of the Lexer chapter in Part 1 of this book.
In the most common form of a datetime literal, the literal always has a value that is not unknown. Assuming that the code passes 4GL compilation, the value must be a valid datetime literal. The lexed text for the literal will be converted to a Java string literal (enclosed directly in double quotes) and that string will be passed to the date constructor.
This Progress 4GL code:
if (dt-var1 > 2013-07-01T12:32:45.678 or dtz-var1 < 2013-07-01T12:34:56.789+01:00) then
will result in this Java code:
if (_or(isGreaterThan(dtVar1, datetime.fromLiteral("2013-07-01T12:32:45.678")) (isLessThan(dtzVar1, datetimetz.fromLiteral("2013-07-01T12:32:45.678+01:00"))
The only exception to this conversion is in the special case of the NOW
"function" inside a variable initializer. Variable initializers are documented as always needing to be constants. But there is an undocumented feature that allows NOW
to be used as a "constant". There is a special datetime static method used to obtain a datetime instance with current timestamp: datetime.now()
. The default constructors will quickly build unknown objects.
This Progress 4GL code:
DEFINE VARIABLE my-datetime-var1 AS DATETIME INIT NOW. DEFINE VARIABLE my-datetime-var2 AS DATETIME. DEFINE VARIABLE my-datetime-tz-var1 AS DATETIME-TZ INIT NOW. DEFINE VARIABLE my-datetime-tz-var2 AS DATETIME-TZ.
will result in this Java code:
@LegacySignature(type = Type.VARIABLE, name = "my-datetime-var1") datetime myDatetimeVar1 = UndoableFactory.datetime(datetime.now()); @LegacySignature(type = Type.VARIABLE, name = "my-datetime-var2") datetime myDatetimeVar2 = UndoableFactory.datetime(); @LegacySignature(type = Type.VARIABLE, name = "my-datetime-tz-var1") datetimetz myDatetimeTzVar1 = UndoableFactory.datetimetz(datetimetz.now()); @LegacySignature(type = Type.VARIABLE, name = "my-datetime-tz-var2") datetimetz myDatetimeTzVar2 = UndoableFactory.datetimetz();
DECIMAL¶
Until recently, the Java double
literal was used as the replacement for the Progress 4GL decimal literal. This had two important limitations (see below). Right now, FWD converts decimal literals using decimal.fromLiteral(...)
(like date types). Even if it former version was increasing the readability of code and it was closer to how Java would be hand-coded, the adopted solution provides a correct Java implementation of the Progress 4GL considering the computation precision.
The text representation of a Progress 4GL decimal literal is now wrapped by a decimal.fromLiteral(...)
call. This means that wherever a 4GL literal occurs, the corresponding location in the converted Java program will have such wrapping. Given this input in 4GL:
sales-price gt 932.33
This output would be generated in Java:
isGreaterThan(salesPrice, decimal.fromLiteral("932.33"))
The only exception is when the Progress 4GL decimal literal has no decimal point. This might happen in any case where a decimal literal is expected, but the programmer encoded it as an integer literal. The result is still a decimal literal, but it has no decimal point. In this case, the resulting Java literal is not generated through a string representation of the legacy value, but using a Java native integer literal. For example:
DEFINE VARIABLE sales-price AS DECIMAL INIT 950.
This would be generated in Java:
@LegacySignature(type = Type.VARIABLE, name = "sales-price") decimal salesPrice = UndoableFactory.decimal(new decimal(950));
Considering that the former solution was based on the native double type in Java, the following explanation is kept for reference. It also explains the limitations of using the double Java data type.
The double
is a data type based on the 64-bit IEEE 754 floating point type. It is a binary representation of a decimal number. It stores an approximation of a real number inside a 64-bit data structure. It has 3 "fields" within the 64-bits of memory dedicated to storing its value.
The first field is a 1-bit value for the sign.
The second field is a 52-bit binary value for the significand (often also called the mantissa). Actually, the value is always stored with an assumed 1 in the most significant bit, so although that bit is never stored, the real precision is 53-bits. The significand stores a number that will be multiplied by an exponent to get the numeric value being represented. It stores all the digits (both to the left and to the right of the decimal point) as a binary number whose magnitude will be determined by the exponent (see field 3).
The third field is an 11-bit base-2 exponent. This means that the exponents from -1022 through positive 1023 can be represented. This is a binary (base-2) exponent. This means that the multiplier (which determines the magnitude of the significand) is 2 to the power of the exponent. For example, if the exponent is 2, the multiplier is 2 2 , or 4. If the exponent is -4, then the multiplier 2 -4 or 1/16.
The actual number being represented is calculated from the 3 fields as follows:
value = sign * significand * 2 exponent
This approach can only accurately store two kinds of numbers: integers (whole numbers) that fit within its range and numbers whose fractional portion (the multiplier) has a denominator that is a power of 2. Any other number is stored as an approximation. For example, representing the fraction 1/10 in base-2 results in a repeating fraction. This is the same conceptually as 1/3 in base-10, which is the repeating number .333333...
This means that there are "gaps" in the numbers that can be accurately represented. These gaps can lead to errors in math. Some errors that occur in comparisons of floating point numbers can be corrected using an "epsilon" value (the smallest possible difference in values for the floating point format) such that numbers that have slightly different binary representations but which actually refer to the same number, can be detected. But epsilon is only useful in comparison operations.
If a number cannot be properly represented, then arithmetic operations may lead to incorrect results. Although there is no direct usage of double
math in the FWD runtime or in the converted code, a problem occurs when a value that is appears in the source code as one value but which cannot be properly represented as a binary value in memory, can cause a subtle problem wherever that double
literal is used. Ultimately, the double value will be converted into a com.goldencode.p2j.decimal
(which uses the reliable BigDecimal
internally instead of the double
) when math is done, but that conversion will occur using a binary floating point value that may differ from the original text of the source code. These problems will be rare (such problems don't exist for simple numbers such as 1 / 10), but they are possible.
The first limitation is based on the number total number of significant digits that must be stored. The Java primitive double
can only reliably store 15 to 17 decimal digits. It varies based on the specific number being encoded. Since a Progress 4GL decimal can store up to 50 significant digits, there is a mismatch.
The second limitation is inherent in the floating point nature of the Java double
. A deviation can occur where the inexact representation of Java's double
causes two (different but close) constants in the source program to be determined as the same number when they really aren't. This can only occur with numbers that exceed the maximum number of significant digits. For example, in Java, the following expression will evaluate to true
:
343753.04000000004D == 34375304000000006D
In Progress 4GL, these constants can be distinguished. In particular, Progress 4GL will honor up to 11 digits to the right of the decimal point. In Java, as long as the value is within the 15 to 17 digit limit, the double
will work. As an alternative, instances of the decimal
class can be instantiated from a string literal. At this time, FWD does this for all decimal literals.
The decision to emit the Java double literal for Progress 4GL decimal literals is one that makes a direct and simple conversion. However, this has the implication that any 4GL replacement interface that could be called in Progress 4GL with either a decimal or a double literal, must have method signatures that allow for both double
and com.goldencode.p2j.util.decimal
types.
INTEGER¶
Progress 4GL integer literals are signed 32-bit values that range which has a minimum value of -2147483648 and a maximum value of 2147483647. The text representation of such literal is identical in both Progress 4GL and Java. The Java int
literal is an exact match. This means that wherever a 4GL literal occurs, the corresponding location in the converted Java program will have the same text. The common usage case:
if i < 6
This output would be generated in Java:
if (_isLessThan(i, 6))
The decision to emit the Java int
literal for Progress 4GL integer literals is one that makes a direct and simple conversion. However, this has the implication that any 4GL replacement interface that could be called in Progress 4GL with either a integer
or a int
literal, must have method signatures that allow for both int
and com.goldencode.p2j.util.integer
types.
INT64¶
Progress 4GL int64 literals are signed 64-bit values that range which has a minimum value of -9223372036854775808 and a maximum value of 9223372036854775807 which are big enough not to fit in interger's 32-bit space. The text representation of such literal is identical in both Progress 4GL and Java. The Java long
literal is an exact match. This means that wherever a 4GL literal occurs, the corresponding location in the converted Java program will have the same text. The common usage case:
if i < 6000000000
This output would be generated in Java (note the long L
suffix):
if (_isLessThan(i, 6000000000L))
The decision to emit the Java long
literal for Progress 4GL int64 literals is one that makes a direct and simple conversion. However, this has the implication that any 4GL replacement interface that could be called in Progress 4GL with either a int64
or a long
literal, must have method signatures that allow for both long
and com.goldencode.p2j.util.int64
types.
On the other hand, because com.goldencode.p2j.util.int64
is the base class for com.goldencode.p2j.util.integer
and in java int
is automatically widened to long
the the matrix of integer-typed signatures of the methods can be greatly simplified.
LOGICAL¶
Progress 4GL logical literals have the following 4 possible conversions:
Progress 4GL logical Literal | Java boolean Literal |
---|---|
false |
false |
no |
false |
true |
true |
yes |
true |
No abbreviations are possible in the Progress 4GL forms, though the Progress 4GL literals can be uppercase or mixed case (they are case-insensitive). The FWD lexer handles any text input differences based on case. The result is converted to the Java boolean
literal, which is an exact match. This means that wherever a 4GL literal occurs, the corresponding location in the converted Java program will have true
or false
depending on the 4GL input.
The decision to emit the Java boolean
literals for Progress 4GL logical
literals is one that makes a direct and simple conversion. However, this has the implication that any 4GL replacement interface that could be called in Progress 4GL with either a logical
or a boolean
literal, must have method signatures that allow for both boolean
and com.goldencode.p2j.util.logical
types.
Unknown Value¶
In Progress 4GL, there is a special literal dedicated to representing the unknown value: the ?
character. At first glance, it seems that using the Java null
literal is a close match. In both cases, the type of the literal is unspecified but can be determined by context. A problem occurs with how Java treats null
. De-referencing null
and passing null
to many APIs will cause a NullPointerException
. This changes the flow of control and is very different from how Progress 4GL handles unknown value. In Progress 4GL, use of unknown value will silently propagate through expressions with no ill effects and no control flow changes. This means that to use null
as a replacement for unknown value, null
checks would have to be placed throughout the code and many parts of the converted code would need to be re-factored to handle all the possible cases. This would result in a significant increase in the complexity of the converted code, with an accompanying decrease in readability. For this reason, Java class instances are being used internalize the state of being set to the unknown value.
In addition, certain use cases have been converted without a direct replacement for the unknown value literal.
The following is the table of possible conversions when ?
is the right operand (rvalue) to the assignment operator. The lvalue is the left operand of the assignment operator, the resource which is being assigned to.
lvalue Type | lvalue Extent | lvalue is Element or Array | Java Result |
---|---|---|---|
variable | 1 (not an array) | n/a | variable.setUnknown() |
variable | > 1 (array) | Element (this is a reference to a specific element of the array, like variable[4] ) |
Emits as a call to a worker routine in the com.goldencode.p2j.util.ArrayAssigner class.import static com.goldencode.p2j.util.ArrayAssigner.*; ... assignSingleUnknown(variable, index) |
variable | > 1 (array) | Array (this is a reference to the entire array like variable ) |
Emits as a call to a worker routine in the com.goldencode.p2j.util.ArrayAssigner class.import static com.goldencode.p2j.util.ArrayAssigner.*; ... assignUnknown(variable) |
database field | n/a | n/a | A new instance of the correct com.goldencode.p2j.util.BaseDataType subclass will be created with the internal state set to be unknown value. The instance will be passed as a parameter to the setter method for that field (in the buffer being accessed).If the buffer being used is named customer-order and the field is an integer value named order-num, then the following would be the result: customerOrder.setOrderNum(new integer()); |
The following lists how a type-specific unknown value is created:
Type | Java Result |
---|---|
character | new character() |
com-handle | new comhandle()@ |
date | new date() |
datetime | new datetime() |
datetime-tz | new datetimetz() |
decimal | new decimal() |
handle | new handle() |
integer | new integer() |
int64 | new int64() |
logical | new logical() |
longchar | new longchar() . |
memptr | new memptr() |
raw | new raw() |
recid | new recid() |
rowid | rowid.instantiateUnknownRowid() |
The result of the above instantiation will be an instance of that type which has the value set as unknown.
Most non-assignment cases use the type-specific instance approach (e.g. new character()
). These cases include (but are not limited to) explicit RETURN
statements in a user-defined function, parameters to a user-defined function, parameters to a built-in 4GL function and variable initializers.
When used as one operand (left or right) of the EQ (or =) equals operator or NE (or <>) not equal operator, the following conversion occurs:
Operator | Java Result |
---|---|
variable EQ ? or ? EQ variable |
Emits as a call to a worker routine in the com.goldencode.p2j.util.CompareOps class.import static com.goldencode.p2j.util.CompareOps.*; ... isUnknown(variable) In some situations, if the expression is being directly used for Java control flow purposes (e.g. if () ) the CompareOps._isUnknown() is used instead of CompareOps.isUnknown() . The _isUnknown() returns a boolean instead of a logical . |
variable NE ? or ? NE variable |
Emits as a call to a worker routine in the com.goldencode.p2j.util.CompareOps class.import static com.goldencode.p2j.util.CompareOps.*; ... notUnknown(variable) In some situations, if the expression is being directly used for Java control flow purposes (e.g. if () ) the CompareOps._notUnknown() is used instead of CompareOps.notUnknown() . The _notUnknown() returns a boolean instead of a logical . |
In Progress it isn't possible to compare two instances of the unknown value literal, so that case is not handled and does not need to be handled.
The common example usage in 4GL:
if chVar ne ? then chVar = ?. if intVar ne ? then intVar = ?. if decVar ne ? then decVar = ?.
converting to Java as:
if (_notUnknown(chVar)) { chVar.setUnknown(); } if (_notUnknown(intVar)) { intVar.setUnknown(); } if (_notUnknown(decVar)) { decVar.setUnknown(); }
Any other usage not documented above converts to an instance of com.goldencode.p2j.util.unknown
. For example, this is used for comparison operators (except for EQ
and NE
) since the operator does not need to know the type in order to determine the logical result of a comparison with unknown value. This unknown
type is only used in cases where the data type is not necessary.
Compiler Constants¶
The Progress 4GL compiler provides a small set of poorly documented symbolic constants. When these text symbols are present in a 4GL program, the corresponding value is substituted in as a constant by the compiler.
Progress 4GL Constant | 4GL Data Type | Value | Java Result |
---|---|---|---|
WINDOW-DELAYED-MINIMIZE | integer | 4 | CommonWindow.WINDOW_STATE_DELAYED_MINIMIZE |
WINDOW-MAXIMIZED | integer | 1 | CommonWindow.WINDOW_STATE_MAXIMIZED |
WINDOW-MINIMIZED | integer | 2 | CommonWindow.WINDOW_STATE_MINIMIZED |
WINDOW-NORMAL | integer | 3 | CommonWindow.WINDOW_STATE_NORMAL |
Unquoted Character Literals in Form Item¶
The 4GL FORM
language statement is a user-interface feature which allows the static definition of a frame in a 4GL source program. In a FORM
statement, the individual frame elements are known as form items. Form items can include a wide variety of expressions, the simplest of which is a character literal (a quoted string). When strings are form items, they are included as hard coded portions of the resulting frame.
The Progress 4GL compiler has a quirk in its lexing/parsing which allows an unquoted character literal in a form item. Character literals elsewhere in a 4GL program must always be quoted (enclosed in matched pairs of single or double quotes). Such text would not normally be allowed, but it is silently honored in a form item.
There are two possible compositions: ------
and ++++++
. The idea seems to be that these would be allowed as hard coded separators, probably for column displays (DOWN
frames). Any form item that consists of one or more contiguous hyphen characters, without any enclosing quotes and without any intervening whitespace, will be treated as if there were enclosing quotes (the text is treated as a string literal). The first non-hyphen character ends the literal. This means that the following are valid: -
, --
, ---
, ----
, -----
and so on. Likewise, any form item that consists of one or more contiguous plus characters, without any enclosing quotes and without any intervening whitespace, will be treated as if there were enclosing quotes (the text is treated as a string literal). The first non-plus character ends the literal. This means that the following are valid: +
, ++
, +++
, ++++
, +++++
and so on. The two characters cannot be intermixed, which means that -+-+-+
is not valid.
FWD handles this inside the lexer, by properly matching such contexts and silently treating it as a string literal. It will be emitted into the resulting Java source code as a string literal (see the CHARACTER section above).
LOGICAL Type Initializers Encoded as Strings¶
The Progress 4GL provides the ability to customize a format string for each variable. The logical type format string is in the form of a string literal that includes a text value that represents true and a text value that represents false, each separated by a /
character. For example, the default format string is "yes/no"
which equates yes
with true
and no
with false
. But a user-defined format string can be specified. The value "over/under" would equate over
with true
and under
with false
.
As a general rule, logical literals cannot be specified using values customized by a user-defined format string. This means that the following is invalid:
DEFINE VARIABLE flag AS LOGICAL FORMAT "valid/invalid". /* this WILL NOT work */ flag = valid.
valid
is not recognized by the Progress compiler as a logical literal, so the above code will fail.
Instead, these format strings are used in user-interface processing to accept logical values input as text that matches one of the customized values. This means that a user that types the text valid
into a FILL-IN
widget that is defined for a logical variable which has a format string of "valid/invalid"
, will successfully set that logical value to true
. In other words, format strings are used for display purposes (both to format the content of a widget and to properly parse the input for a widget), but cannot be used to change the known set of logical literals that can be hard coded into 4GL source code.
As with many things in Progress 4GL, there is an exception to this rule. It is possible to define a variable initializer as a string literal even though the variable's type is logical. In this case, the 4GL environment uses the format string to interpret the initializer string and determine the proper value for initialization of the logical variable.
DEFINE VARIABLE flag1 AS LOGICAL INIT "invalid" FORMAT "valid/invalid". DEFINE VARIABLE flag2 AS LOGICAL INIT "over" FORMAT "over/under".
The order of the FORMAT
and INIT
clauses does not matter.
Progress will match against the format string with case-insensitive text. Uppercase, lowercase or mixed case is all equivalent. As is true with many Progress 4GL features, it gets even more complicated. Progress will match exactly or it will also match any unambiguous abbreviated value. Given a format string of the form 'XXX/YYY'
then x
, X
, xx
, XX
, xxx
, XXX
all should resolve to true
. Likewise, y
, Y
, yy
, YY
, yyy
, and YYY
will resolve to false
.
If no format string is available, then the default is made to be 'yes/no'
and 'true/false'
. Otherwise the specified format is used.
This is not allowed outside of a variable initializer. For example, this code doesn't work:
DEFINE VARIABLE flag AS LOGICAL FORMAT "valid/invalid". /* this WILL NOT work */ flag = "valid".
Since strings cannot be assigned to logical types, the above code will fail.
FWD fully honors these behavior. Given this:
DEFINE VARIABLE flag AS LOGICAL INIT "over" FORMAT "over/under".
the result will be this in Java:
logical flag = new logical(true);
The FWD conversion process reads the content of the string literal and matches it using the format string. But this match is always converted to one of the standard Java boolean
literals true
or false
.
Other Non-CHARACTER Type Initializers Encoded as Strings¶
For CHARACTER
and LONGCHAR
types, the initializer constant must always be a string literal. Each other data type accepts the equivalent literal for its respective type. An integer variable can accept a numeric literal (either a whole number or a decimal number) as an initializer. Progress also allows these other data types to optionally accept a string literal as an initializer constant. The content of the string literal must match the syntax rules for literals of that type, but otherwise such strings work just as a hard coded literal would work. This allows code such as the following to work:
DEFINE VARIABLE num AS INTEGER INIT "14".
The above results in this Java code:
@LegacySignature(type = Type.VARIABLE, name = "num") integer num = UndoableFactory.integer((long) 14);
The string will be converted to a result matches the type-specific constant form as if it was coded in the 4GL without the enclosing quotes.
Logical types have special format string aware behavior (see the section above entitled Logical Type Initializers Encoded as Strings).
DATE, DATETIME and DATETIME-TZ Initializer Special Cases¶
Variable initializers are constants in the source code which provide a hard-coded initial value for the corresponding variable. The only exception to the rule that initializers must be constants, is for date and time related variables. Progress 4GL allows a variable's initializer to be set as the built-in function TODAY
or the built-in function NOW
.
FWD fully supports the use of TODAY
as a DATE
variable initializer. The following 4GL:
DEFINE VARIABLE some-day AS DATE INIT TODAY.
will result in the following Java code:
@LegacySignature(type = Type.VARIABLE, name = "some-day") date someDay = UndoableFactory.date(date.today());
The default constructor for the com.goldencode.p2j.util.date
class creates an instance that is set to the current day at the moment the variable is initialized. This is the exact equivalent of the TODAY
built-in function.
The same holds for NOW
built-in function of DATETME
and DATETIME-TZ
types.
Variables¶
Design Decisions¶
In hand-written Java code, the use of primitives has many benefits. Performance of calculations and readability of the code are the two most important advantages. For example, in an arithmetic expression, using primitives allows the data to be operated on directly, reducing method calls, temporary variable and making the expression simpler and easier to read.
But there are still reasons that hand-coded Java must sometimes use the Java wrapper classes such as java.lang.Integer
instead of primitives like int
. An example is the storage of a number in a map or other collection. Primitives aren't treated as the equivalent of an Object reference which is both their strength and their biggest limitation.
A Java replacement for Progress 4GL variables which is fully compatible must address a wide range of language features. To meet that compatibility objective, Java primitives (e.g. int
, double
) and the Java object wrappers (e.g. Integer, Double) cannot be directly used from converted business logic. To do so is impractical since would cause that converted code to be greatly expanded with unnecessary and complicated code. The best solution is to create a set of Java classes which perfectly duplicate the behavior of the original 4GL type. These FWD wrapper classes hide the complexity needed to provide the features, keeping the converted code (the user of these wrappers) as simple, clean and straightforward as possible.
The following provides details on each of the major 4GL features causing the need for FWD wrappers:
Core Data Type Behavior¶
Many of the 4GL data types have no Java primitive or Java wrapper class that is directly compatible. For example, the 4GL date
has no suitable Java replacement that has the same behavior. Likewise, the 4GL decimal
cannot be properly duplicated using a Java double
or the java.lang.Double
.
Of the 4GL types, only character, logical and integer have Java types which are compatible with the basic behavior. Respectively, the Java replacement options are java.lang.String
, boolean
or java.lang.Boolean
and int
or java.lang.Integer
. Any other types must be implemented with a FWD class in order to get a compatible result.
Unknown Value¶
All Progress 4GL types can be set to the unknown value. This means that any Java replacement type must be able to represent all of the type-specific state as well as the special state of "unknown".
The Java primitive int
can represent the exact same set of whole numbers which can be represented by the 4GL integer
, however it cannot represent the unknown value. The Java int
cannot represent any data except for a number and there are no numbers that can be reserved for the purpose of symbolizing unknown, since they must all be used to symbolize the corresponding number that can be represented by the 4GL integer
.
The Java primitive boolean
only has two values (true
and false
). The Progress 4GL logical has three values (true
, false
and unknown
).
These are the only two data types that have compatible primitive types in Java (see Core Data Type Behavior above). This means that primitive variables cannot be used as a replacement in converted code without having additional state. For example, each 4GL integer
variable could be converted into two variables, an int
for the date value and a boolean
for whether the variable is unknown. Laying aside the problems of how this breaks all the common best practices for object-oriented programming, this would have severely negative effects on the resulting converted code. All variable usage that could read or modify the state of the unknown value for that variable would have to be refactored to be aware of the additional state variable. Worse yet, any FWD library code or any application code that does not have visibility to allow assignment back to the boolean
flag would have to have some other object instance that could be modified to signal to the called code that the boolean
flag must be set or cleared. This is just not practical to do, nor would the results be at all acceptable from a readability or maintainability perspective.
An alternate approach would be to use the Java object wrappers as a replacement (String
for character
, Integer
for integer
and Boolean
for logical
). Each of these types can be set to null
which in Java is the rough equivalent of unknown value. A mismatch exists since Progress 4GL silently allows unknown value to "propagate" where Java tends to catastrophically fail at runtime (e.g. NullPointerException
). For example, in Progress an arithmetic sub-expression will return an unknown value if either or both operands are the unknown value. The entire enclosing expression would finish evaluation and the result would usually be the unknown value. In Java, if auto-boxing was used to allow an Integer
to be processed in an arithmetic sub-expression, any null
value for that Integer
would cause a NullPointerException
to be raised, which would abort the processing of the expression. To be more specific, null
cannot be assigned to a primitive and any unboxing or usage of a null
Java wrapper instance will result in a NullPointerException
. When the exception occurs, the flow of control aborts the assignment or expression evaluation in Java, which would break compatibility.
Just as with the forced use of primitives, the converted Java code could be refactored to implement null
checks and other necessary null
processing to allow expressions and other variable usage to silently tolerate the presence of null
references. But in doing so, the code would expand into a complete mess of complicated, impossible to read logic. It would be severely fragile and would remove all benefit of using native Java types, making the result much worse.
This single 4GL feature by itself is enough to justify the design decision to create custom FWD classes to most cleanly replace the 4GL data types.
Other Type-Specific State¶
Some Progress 4GL data types have additional state that is not present in either Java primitives or Java object wrappers. For example, the decimal
type in Progress has a DECIMALS
setting which sets the number of significant digits to the right of the decimal point. Likewise, the character
type has a CASE-SENSITIVE
setting which determines if string comparisons are or are not to honor case-sensitivity.
Just as with unknown value (see Unknown Value above), trying to maintain that state external to the variable is not practical and would have significant negative effects on the resulting converted code.
This causes FWD wrapper classes to be the best solution.
Undo¶
In Progress 4GL, UNDO is a feature by which the control flow of the application can reset the state of a variable to match a prior snapshot. This is done based on the block structure of the program and there is no explicit application code needed. Using FWD wrapper classes for variables allows the FWD runtime to provide UNDO processing integrated with the block structure of the converted application. This is because the wrappers are object references that are registered for UNDO support at the moment the variable instance is created. Then the FWD runtime can take snapshots and restore prior state exactly as the Progress 4GL runtime would do it, without any additional code to be emitted into the converted application sources. This means that the FWD wrapper approach simplifies the resulting code, by allowing the runtime to handle UNDO without extra code.
This feature requires that the FWD wrapper classes be mutable.
Operator Behavior¶
Even if the Java primitives or Java object wrappers could be used as replacements for 4GL data types, the using 4GL behavior of comparison and arithmetic operators would make it impossible to use Java operators directly with that data. For example, all of the 4GL operators have specific behavior in regards to unknown value.
J2SE 5.0 and later provides a new language feature for auto-boxing and auto-unboxing. This largely makes the use of Java wrappers transparent (it wraps or "boxes" a primitive into a wrapper when needed and unwraps or "unboxes" a wrapper into a primitive when needed). This makes it possible to write arithmetic and logical expressions directly using Java wrappers like java.lang.Integer
.
Unfortunately, the following limitations (which are also quite significant ) apply:
- Poor
null
handling (see Unknown Value above). - The == operator cannot be used because it does not give consistent results when comparing two operands where either one or the both of them are wrappers. This is due to the fact that when this operator is dealing with objects (versus primitives), it is comparing object references and this will generally yield false-negatives in many situations.
- Overloaded method signatures can be tricky because method resolution changes with auto-boxing added. These method resolution changes are supposed to be compatible but this remains to be seen.
- The combination of auto-boxing and generics means that the ternary operator will auto-cast the 2 expression results to the least common denominator result if their types are different.
As noted in the above sections, many expressions would require significant refactoring to make this work. For example, in the case where a null
check fails, all possible assignments that could be forced to the unknown value must be detected. In addition (possibly more important), any side effects of the expression must be triggered in a safe manner. For example, if a function is called as part of an expression, that function may have side effects that the rest of the procedure relies upon. Not only is the refactoring of the expression highly non-trivial but there are very strong reasons to actually allow the expression to execute even in the presence of the unknown value (which is how Progress works today). If null
is used as a proxy for the unknown value, this aspect of the behavior cannot be duplicated since Java generates a runtime error for these cases. Since the Jave object wrappers have no way of encoding the unknown value except setting the current reference to null
, this eliminates them from being a useful/good solution to this problem.
Auto-boxing/unboxing only works with the built-in Java wrappers (java.lang.*
). Since Java has no ability to overload operators (at least at the time of this writing), custom Java methods will be written to implement the exact behavior of 4GL operators. Any use of 4GL operators will be converted to Java method calls. Those methods are coded to accept and return the proper data types.
Shared Variables¶
In Progress 4GL, named variables can be shared between code at different levels of the call stack, without passing the variables as parameters. Somewhere in the call stack the variable is created as DEFINE NEW SHARED VARIABLE
(optionally with a GLOBAL context). All other locations more deeply nested in the stack will access that same variable using a DEFINE SHARED VARIABLE
statement. All usage of the variable modifies the same instance.
This is a similar behavior to that of INPUT-OUTPUT
or OUTPUT
parameters. The idea is that all modfications must be visible to all code that has access to the shared variables. This is not possible with primitives or with the Java object wrappers.
The solution is to create mutable FWD wrapper classes for each data type.
Mutable Wrappers¶
The best solution to the above design problems is to implement custom classes (corresponding to each Progress data type) to handle all of the following:
- Stores the data in a form that provides an exact match with the Progress data.
- Stores the state of whether this instance is really equal to the unknown value.
- Stores any other state necessary to properly process the data type (e.g. case-sensitive for character).
- Implements all of the operators and replacements for the Progress built-in functions such that the 100% Progress compatible results are generated. For some types (e.g. integer and logical) this is primarily a matter of handling the unknown value in a transparent manner. For other types, there are other behaviors to implement. For example, with decimal there is the added requirement for proper rounding/truncation to be implemented after each operator and function.
- Is mutable.
- Provides support to taking a snapshot and for restoring that snapshot to allow
UNDO
to be fully and transparently implemented.
All of these custom classes will reside in the com.goldencode.p2j.util
package. They will all share a common ancestor class of BaseDataType
which will be abstract to define a common set of interfaces. Each will have a class name the same as the lowercase data type name (e.g. class com.goldencode.p2j.util.integer
for the integer
data type). This is unique (does not conflict with the java.lang.*
), is easy to read and looks more like a primitive is being used. The down side is that this is an exception to the normal Java-recommended naming rules for classes.
In a perfect world, the exact look of (arithmetic and logical) expressions could be maintained if Java supported operator overloading. Since that is not possible, methods (with intuitive names) will be written to substitute for all operators. Unwrapping will only be needed in those places where the expression result requires a primitive type. Assignments and many method calls will use the object form returned as an expression result. However, when expression results are used directly with a Java language construct (e.g. if/else
, array subscripts) or in a Java-provided method that has primitives in the signature, the primitives must be used. In these cases, the custom wrapper object will be unwrapped.
Each of these classes will internally track whether the variable is set to the unknown value. This means that all variables will have a valid (non-null) reference. As long as the conversion code is written with this in mind, null pointer exceptions will be eliminated (from expressions).
By passing mutable references to these FWD wrapper classes as parameters to the methods that replace Progress 4GL functions or procedures. This will allow INPUT-OUTPUT
and OUTPUT
parameter behavior to be duplicated exactly. To support mutability, all of the classes will provide an assignment-type interface and setters to allow modification of the wrapped data.
CHARACTER¶
The replacement class is com.goldencode.p2j.util.character
. If no initial value is specified in the Progress 4GL source code, the default will be the empty string ""
.
Progress 4GL uses a single data type to handle both single character values and strings. This will be maintained so that method signatures and expressions require no refactoring.
The data storage inside the character class is handled by java.lang.String
. This is robust enough to store any character data that can be stored or processed in the 4GL.
CLASS¶
There is only a rough summary at this time explaining how legacy OE classes are implemented in FWD. In fact, for each class object created, an instance of com.goldencode.p2j.util.object<T extends _BaseObject_>
is created. It can be seen that _BaseObject_
is used. This is an interface implemented by com.goldencode.p2j.oo.lang.BaseObject
which is extended by all of the converted OE classes.
Objects are generated using com.goldencode.p2j.util.ObjectOps.newInstance
based on the Java class of the converted type:
obj = new myclass().
Presuming that myclass
is a legacy class, the following is the converted code:
obj.assign(ObjectOps.newInstance(com.goldencode.dataset.MyClass.class));
More details can be found in #3751.
COM-HANDLE¶
This data type contains a reference to an OCX object on the Windows platform. It allows 4GL code to make direct calls to OCX methods and access/change OCX properties.
FWD has a placeholder class com.goldencode.p2j.util.comhandle
which is only a stub at this time. It has no real functionality. It will allow some 4GL code to convert and compile, but the runtime backing is not yet there.
DATE¶
The Progress 4GL date type is based on a julian day number. It is most similar to the "Chronological Julian Day Number" which is a whole number that specifies the number of days since a base date in the Gregorian calendar of 01/01/4712 BC or 01/01/-4713 (which is the date with 0 as its day number). The Progress 4GL implementation is 1day off. It has a base date of 12/31/-4714 for day 0.
The replacement class is com.goldencode.p2j.util.date
. If no initial value is specified in the Progress 4GL source code, the default will be the unknown value.
Date processing has been made compatible with Progress including the implementation of controllable Y2K windowing (the start year defaults to 1950). This is equivalent of using the -yy startup parameter or the session:year-offset attribute. For details on how to configure this in FWD, please see the chapter entitled Progress 4GL Runtime Compatibility Options in the book FWD Runtime Installation, Configuration and Administration Guide.
The order of the date fields when reading from a string or when writing to a string defaults to 'mdy' format, but this can be changed using a directory setting. That is equivalent to the -d startup option or the session:date-format attribute in Progress. For details on how to configure this in FWD, please see the chapter entitled Progress 4GL Runtime Compatibility Options in the book FWD Runtime Installation, Configuration and Administration Guide.
The FWD implementation is dependent upon the Gregorian calendar and the Progress 4GL seems to have no concept of different types of calendars (e.g. lunar...) so this is OK. The two implementations (Progress and J2SE) appear to be quite compatible including how they handle the missing days from 10-05-1582 through 10-14-1582. Both implementations allow these dates to be instantiated and both generate a date that is equivalent to 10-15-1582 through 10-24-1582 respectively. This leads to apparent logical contradictions such as 10-14-1582 comparing as greater than 10-15-1582.
The Progress date has no notion of a time of day and the Java java.util.Date
is inherently built with this notion (and the notion of a timezone). When being created from any Java generated date source (e.g. an instance of the Date class or the default constructor which accesses the current system time), this timezone specific data must be taken into account and neutered in the resulting stored date. Likewise, whenever using this data, no timezone is used unless one is converting this instance into an instance of java.util.Date
, in which case the timezone can be overridden. By default, the JVM timezone will be used for interpreting timezone aware inputs AND for generating any java.util.Date
output.
DATETIME¶
The datetime
datatype is composed of a date and the time elapsed from midnight computed in milliseconds. In fact, the implementation puts datetime
as the (only) derived class from date
having one extra field for time part.
The replacement class is com.goldencode.p2j.util.datetime
. If no initial value is specified in the Progress 4GL source code, the default will be the unknown value. datetime
will use the time information from a Java java.util.Date
if built with the corresponding constructor but, like date
, will ignore the extra information about the timezone.
DATETIME-TZ¶
The next class in date
hierarchy is datetimetz
. It is composed of a date, the time elapsed from midnight computed in milliseconds and a time offset in minutes from UTC.
The replacement class is com.goldencode.p2j.util.datetimetz
. If no initial value is specified in the Progress 4GL source code, the default will be the unknown value. datetimetz
will use the time information from a Java java.util.Date
and the extra information about the timezone.
DECIMAL¶
The Progress 4GL decimal data type is signed base-10 number with both integer and fractional portions. Each decimal value has a maximum of 50 significant digits. Up to 10 of those digits can be to the right of the decimal point. Thus the largest value is 99999999999999999999999999999999999999999999999999 and the minimum value is -99999999999999999999999999999999999999999999999999. If all of the 10 possible digits of precision are used to the right of the decimal point, then only 40 digits can be used on the left. Overflows and underflows both cause the same runtime error "Decimal number is too large (536)".
The internal math and representation used by the 4GL runtime is not known. However, some tests of its behavior indicate that there are no obvious signs of the use of binary floating point (e.g. IEEE 754) for either the mathematics or the representation. Certainly, the maximum size of the decimal type is by itself a key reason that standard floating point math is not being used (no common binary floating point implementations provide 50 significant digits). The 4GL implementation has many features in common with fixed point decimal systems.
Floating point math such as IEEE 754 approximates the representation of some real numbers using binary representation of the significant digits (the significand) and a base-2 exponent. This cannot accurately represent all possible decimal numbers. The maximum error introduced by this approximation is known as the epsilon value for the implementation. The epsilon for a given implementation can be calculated programmatically. If no epsilon value is found, then that may suggest that the math system is not binary floating point or that the rounding behavior of the implementation largely eliminates the inaccuracies inherent in the floating point system.
An epsilon is the smallest floating point value that can be added to another floating point value such that the result can be differentiated as not equivalent by the native environment's operators. As with everything else about Progress 4GL decimal support, the Progress and Java differences are substantial.
In Java, the epsilon is defined (by the language specification) as being equal to 2 -52 or 2.22044604925 X 10 -16 . This is an exceedingly small number, especially in comparison with the smallest possible Progress 4GL decimal fraction which is .0000000001
. The following 4GL code will identify the epsilon for Progress:
def var possibleEpsilon as decimal init 1.0. def var epsilon as decimal init 0.0. def var mantissaBits as integer init 0. repeat. if ((1.0 + possibleEpsilon) = 1.0) then leave. epsilon = possibleEpsilon. possibleEpsilon = possibleEpsilon / 2.0. mantissaBits = mantissaBits + 1. display possibleEpsilon format "99999999999.999999999999999" mantissaBits. end. message "Epsilon = " + string(epsilon). message "Mantissa Bits = " + string(mantissaBits).
It turns out that this program will generate an infinite loop. This is because once the possibleEpsilon
is reduced down to 0.0000000001
(the smallest representable Progress decimal fraction), the result of the possibleEpsilon / 2.0
sub-expression will always be 0.0000000001
instead of 0.00000000005
. Since a difference of 0.0000000001
can be detected as different from any other decimal value, another way of looking at this result is that there is no epsilon value that is "visible" to the programmer. There may be an internal Progress epsilon value but due to the fact that Progress rounds all intermediate results (after each operator runs), the result is always something that cannot be smaller than 0.0000000001
unless it is 0.0@! This means that:
0.0000000001 / 2.0 results in 0.0000000001 0.0000000001 / 3.0 results in 0.0
The important result here is that epsilon testing (the "normalization" of floating point comparisons to detect if something is really different or not) is not an issue for Progress applications because of the rounding/truncation behavior. This suggests that the 4GL decimal is based in fixed point or a close facsimile to fixed point math.
The Java primitive double can easily handle the smallest values that can be represented by a Progress decimal. The Java double is based on a 64-bit IEEE 754 floating point value where the maximum value is 1.7976931348623157 * 10 308 and the minimum value is 4.9 * 10 -324 . There is a problem in duplicating Progress behavior with small numbers near its limit of precision. For example:
0.0000000001 / 2 * 2 = 0.0000000002
In Java this would be calculated as 0.0000000001
.
The Progress 4GL rounding behavior for very small numbers is not limited to just the operators but rather it seems that it has an internal floating point representation that is precise to 11 digits. Every time an external representation is converted into this form (whenever accessed from an application via an operator, assignment, etc.), it is always truncated to the lesser of 10 digits (or the value of the decimals option for that variable) and the truncation uses rounding to determine the least significant digit.
Given this rounding implementation on the Java side, the normal epsilon testing is not necessary AND it is reasonably easy to implement the Progress "decimals" option which allows the programmer to specify an arbitrary number of digits of precision that is less than the default (and maximum) 10 digits.
For example:
define var n1 as decimal. n1 = 0.000000000149 + 0.000000000001. message string(n1) string(0.00000000005) string(0.00000000015).
This prints:
.0000000001 .0000000001 .0000000002
Another way to show this, in Progress 4GL, the following expressions are all true
:
0.000000000149 + 0.000000000001 = .0000000001 0.00000000005 = .0000000001 0.00000000015 = .0000000002
In this case, the important rounding occurs before the operation rather than after. Any Progress 4GL decimal variables which are sensitive to the boundary between the 10th and 11th digit of precision (to the right of the decimal point) would cause problems in a Java floating point implementation. For example it is possible that the result of a calculation is something like 2.999999999999996
which in Progress would be represented as 3.0
but which is directly represented in Java.
Even though the Java double
can have a big exponent values, its significand is limited to 53 bits or ~16 decimal digits. This means that whole number of 17 digits or more can't be saved in double
without precision loss. Subtraction of two big numbers that are close to each other can give an incorrect result.
Some of these problems can be somewhat improved by duplicating the Progress 4GL rounding/truncation behavior in the Java environment. But it is not possible to eliminate all differences between Progress 4GL decimal and the Java floating point support.
The conclusion is that Java IEEE floating point support is not suitable for use in replacing the Progress 4GL decimal type. More details of how IEEE 754 floating point works and the two major limitations can be found in the DECIMAL portion of the Constants section of this chapter. The first limitation is that the Java double
can only reliably store less than 17 significant digits, whereas the Progress 4GL decimal can reliably store up to 50 significant digits. The second limitation is that the Java double
represents many valid real numbers in an inexact manner, causing flawed comparisons, error prone arithmetic and data loss/corruption. The Progress 4GL decimal type does not suffer from these weaknesses.
The replacement class is com.goldencode.p2j.util.decimal
. If no initial value is specified in the Progress 4GL source code, the default will be 0.0
. Internally, this class will use the java.math.BigDecimal
and the scale (which is similar in concept to the 4GL DECIMALS
specification) will be managed carefully. The BigDecimal
class can handle over 50 significant digits as well as all the scale values (from 0 through 10) that set the number of digits to the right of the decimal point. BigDecimal
aware math and comparisons will need to be implemented as well as the proper conversion to/from other types. The result should match the Progress 4GL behavior exactly.
In addition, the com.goldencode.p2j.util.decimal
class extends (inherits as a parent class) from com.goldencode.p2j.util.NumberType
. This is the same parent class as is used in com.goldencode.p2j.util.integer
which means that Java method signatures which can take either a decimal
or integer
type can be specified as NumberType
.
HANDLE¶
This class is really just a container that holds a backing object. It provides unknown value processing, comparison/assignment operator support and the valid-handle() built-in function implementation. By default there is no contained object, which is the equivalent of a handle being assigned the unknown value.
In Java, the contained object is always one which implements the com.goldencode.p2j.util.WrappedResource
interface. A WrappedResource
is an object that can be checked for validity (using the isValid()
method) and if it is unknown (using the isUnknown()
method). Most importantly, the object itself has some other functionality which is not directly accessible from the handle, but must be dereferenced for access
Dereferencing is the act of obtaining the contained object. The method used to dereference the object is Object get()
. Since the get()
method returns the non-specific java.lang.Object
type, the specific type of the contained object will need to be determined on a case by case basis.
As in Progress, the type of the contained object is not known until it is assigned at runtime. That means that there are ways to write 4GL code in which the same handle variable contains different types of resource depending on the control flow of the program at runtime. As such, each dereferencing location in the 4GL code makes an implicit assumption about the contained object's type. In the Java equivalent, each dereferencing location contains a cast to convert the contained resource's type to the equivalent type expected by the Progress 4GL code at that location.
Since different types of contained object can be assigned to the same handle instance, Java generics cannot be used to eliminate the need for a cast at the time of dereferencing.
One of the most common cases of handle usage is to call a method or get/set an attribute value. The handle gets dereferenced when used as a referent for methods and attributes. For example, the following 4GL code:
if hndl:data-type = "Date" then
looks like this in FWD:
if (isEqual(((GenericWidget) hndl.get()).getDataType(), "Date") { ...
INTEGER¶
The Progress integer is a 32-bit signed integral value whose maximum value is 2147483647 and minimum value is -2147483648. Overflows do not occur. An attempt to wrap around to the minimum or maximum value will automatically promote any intermediary value to int64
or attempted to be stored in a variable or table field an error will be raised. This is not an exact match to the Java primitive int
also because for the case where the unknown value is assigned or tested on this variable. For this reason, the Java int
cannot be used to duplicate all the functionality of a Progress 4GL integer
.
The FWD replacement is com.goldencode.p2j.util.integer
which is derived form com.goldencode.p2j.util.int64
class and having storage restrictions to 32-bit values.
INT64¶
The Progress int64 is a 64-bit signed integral value whose maximum value is 9223372036854775807 and minimum value is -9223372036854775808. Overflows wrap around to the minimum value and underflows wrap around to the maximum value. This is an exact match to the Java primitive long
, except for the case where the unknown value is assigned or tested on this variable. For this reason, the Java long
cannot be used to duplicate all the functionality of a Progress 4GL int64
.
The FWD replacement is com.goldencode.p2j.util.int64
which internally uses a Java long
for the value and a Java boolean
for tracking if the instance is equivalent to the unknown value.
In addition, the com.goldencode.p2j.util.int64
class extends (inherits as a parent class) from com.goldencode.p2j.util.NumberType
. This is the same parent class as is used in com.goldencode.p2j.util.decimal
which means that Java method signatures which can take either a decimal
, int64
or integer
type can be specified as NumberType
.
LOGICAL¶
The Progress logical has multiple "modes" yes/no, true/false and a user defined value1/value2. However, all of these are the exact equivalent of the Java boolean
type which has only 2 states, true
or false
, except for the case where the unknown value is assigned or tested on this variable. For this reason, to properly duplicate the Progress logical type, the com.goldencode.p2j.util.logical
class has been created. This class stores two Java boolean
members, one for the value and one for determining if the instance is the unknown value.
There are cases in the 4GL where a logical that is set to the unknown value will act the same as a logical that is set to false
. An example is when the logical is used for control flow purposes:
if my-logical then ...
But this only occurs in some cases. It is important to note that setting a logical to the unknown value is something very different/distinct from setting it false
. The logical operators respond differently to having the unknown value as an operand:
Operator | Left Operand Unknown | Right Operand Unknown | Both Operands Unknown |
---|---|---|---|
EQ | false | false | true |
NE | true | true | false |
LT | ? | ? | false |
LE | ? | ? | true |
GT | ? | ? | false |
GE | ? | ? | true |
AND (logical type operands only) | false if right operand is false, ? if right operand is true | false if left operand is false, ? if left operand is true | ? |
OR (logical type operands only) | true if right operand is true, ? if right operand is false | true if left operand is true, ? if left operand is false | ? |
NOT (logical type operands only) | n/a | ? | n/a |
Since the Progress logical really does have 3 states, it must be implemented in a wrapper class.
LONGCHAR¶
The replacement class is com.goldencode.p2j.util.longchar
. It extends the com.goldencode.p2j.util.Text
class which is also extended by Java character representation. The backing data is stored inside a native Java String.
The class also implements com.goldencode.p2j.util.LargeObject
, allowing extended support for binary representation.
Straight out of the 4GL documentation, the longchar
data type should be sensitive to the codepage and other information; FWD offers such support.
MEMPTR¶
The Progress memptr
type allows a variable to represent a single contiguous portion of memory. That memory can represent anything, but at its core it is simply an array of bytes. Progress does not provide direct access or dereference of bytes in this memory area. There are no operators that provide access. For example, although the memory area is essentially an array of bytes, one cannot directly dereference those bytes using the subscript operator []. Rather, all access is through an API defined by getter and setter built-in functions. These functions invariably take a 1-based index position in the memory area from which to read data or to which to write data.
Since the data in a memptr
is essentially hidden away behind this interface, it is convenient to provide a replacement API using Java methods, allowing the data storage to be completely hidden (and implementation-specific). The Progress implementation of a memptr
is likely to be a true pointer to a location in memory which is the starting position (byte 1) of a single contiguous area. In the Java-based runtime, this is not backed by a pointer to memory. Instead, it is backed by a byte array.
The Progress memptr
is very similar from the 4GL code usage to a Progress variable of type raw
. In fact, most of the API works interchangeably with either data type. Since there is so much commonality in Progress, it naturally leads to the fact that the Java implementation is largely located in a single class named com.goldencode.p2j.util.BinaryData
. The actual class that implements the memptr-specific behavior is com.goldencode.p2j.util.memptr
. This class has BinaryData
as its parent class, and in that manner it inherits and leverages common code.
The memptr
class implements by default, an uninitialized byte array. The converted 4GL code must allocate memory explicitly. If used before this allocation occurs, an error will be raised. The allocation is a fixed size and it cannot be dynamically changed. There is no explicit limit on length (although the system implicitly limits its length since there is a finite amount of memory). When done using that memory, explicit deallocation is achieved by setting the size to 0. When used, it does not automatically extend its size. In other words, when you write past the current size, an error will occur.
All of these memptr-specific behaviors are duplicated in the FWD implementation.
RAW¶
The Progress raw
type allows a variable to represent a single contiguous portion of memory. That memory can represent anything, but at its core it is simply an array of bytes. Progress does not provide direct access or dereference of bytes in this memory area. There are no operators that provide access. For example, although the memory area is essentially an array of bytes, one cannot directly dereference those bytes using the subscript operator []. Rather, all access is through an API defined by getter and setter built-in functions. These functions invariably take a 1-based index position in the memory area from which to read data or to which to write data.
Since the data in a raw
is essentially hidden away behind this interface, it is convenient to provide a replacement API using Java methods, allowing the data storage to be completely hidden (and implementation-specific). The Progress implementation of a raw
is likely to be a true pointer to a location in memory which is the starting position (byte 1) of a single contiguous area. In the Java-based runtime, this is not backed by a pointer to memory. Instead, it is backed by a byte array.
The Progress raw
is very similar from the 4GL code usage to a Progress variable of type memptr
. In fact, most of the API works interchangeably with either data type. Since there is so much commonality in Progress, it naturally leads to the fact that the Java implementation is largely located in a single class named com.goldencode.p2j.util.BinaryData
. The actual class that implements the raw-specific behavior is com.goldencode.p2j.util.raw
. This class has BinaryData
as its parent class, and in that manner it inherits and leverages common code.
The raw
class implements by default, a 0-sized byte array. There is no explicit allocation or deallocation of memory, but rather the array automatically extends its size when you write past the current end of the memory area. The maximum size of the memory area is 32 kilobytes. The data can be easily persisted and restored to/from a string representation which will maintain all binary data unchanged inside a BASE64 encoding. This makes a raw
most suitable for use in exporting data to files or databases.
All of these raw-specific behaviors are duplicated in the FWD implementation.
RECID¶
In Progress a recid
is a unique identifier for a specific database record. The Progress documentation states this is a 4 byte value (32-bits). It is obtained by using the RECID built-in function for a specific buffer, which will return the identifier for the current record in that buffer or unknown value if there is no record in the buffer. That recid
can be used in queries (to obtain that specific record) or otherwise compared with other recid
instances.
The Java version of a recid
is implemented by an instance of the com.goldencode.p2j.util.recid
class, which has a parent class of com.goldencode.p2j.util.integer
, which represents a 32-bit whole number. The entire recid
implementation is in its parent class except that the recid
class has a different default format string (same as in Progress).
In Java, the 32-bit recid
value is obtained from the 64-bit id
column of the table associated with the buffer. Each converted table has a primary key which is the id
column. This is not application data, but is used internally by the persistence layer. The id
is a 64-bit whole number which uniquely identifies the specific row in that table. Of great importance is that this identifier is unique across the entire database, not just within that table. The identifier is assigned automatically at row insertion and it will never be null
in the database. So long as that record isn't deleted, the id
will remain the same.
Since a recid
is a 32-bit value where the internal FWD id
is a 64-bit value, there is a potential for loss of data when the 64-bit value is truncated to 32-bits. In any case where the upper (most significant) 32-bits of the id
column value contains real data, the recid
will not contain the correct value.
This issue is somewhat ameliorated by the fact that the persistence layer tracks unused id
values and reuses any previous values that are freed up on deletion. This greatly increases the chances that all active id
values can fit inside a 32-bit integer without losing data. However, in a case where the database does have more than 4 billion rows, the recid
implementation will potentially have problems. While the specific Progress implementation is not known, it is likely that this same limitation exists in the Progress 4GL (it is a 32-bit identifier after all).
In Progress, dumping or exporting database tables does not include the recid
values in the dump or export. This means that reloading any dumped or exported data will potentially change the recid
values. Developers are cautioned not to write code that depends upon specific recid
values.
When data is migrated to FWD, the exported/dumped data is imported and upon each row insertion, new id
values are assigned. Since there is no recid
data in the dump files, all new id
values have no relation to the original Progress recid
values. This means that any recid
values that were somehow used again in the Java system (for example, as read from some flat file in which the 4GL application recorded the values) will fail to match the proper record.
The Progress behavior in reallocating recid
values is not documented and should not be relied upon since it is an arbitrary implementation detail that is known to be subject to change. It is possible that application code has been explicitly or implicitly written with this dependency. Any such reliance upon Progress-specific reallocation behavior is dangerous since the reallocation algorithm is not known or documented and the FWD internal implementation is virtually assured to be different.
ROWID¶
In Progress a rowid
is a unique identifier for a specific database record. The Progress documentation states this is a "byte string" of variable length, depending on the database or data-server in use. It is obtained by using the ROWID built-in function for a specific buffer, which will return the identifier for the current record in that buffer or unknown value if there is no record in the buffer. That rowid
can be used in queries (to obtain that specific record) or otherwise compared with other rowid
instances.
The Java version of a rowid
is implemented by an instance of the com.goldencode.p2j.util.rowid
class. This stores the 64-bit rowid
value as a java.lang.Long
which is obtained from the 64-bit id
column of the table associated with the buffer. Each converted table has a primary key which is the id
column. This is not application data, but is used internally by the persistence layer. The id
is a 64-bit whole number which uniquely identifies the specific row in that table. Of great importance is that this identifier is unique across the entire database, not just within that table. The identifier is assigned automatically at row insertion and it will never be null
in the database. So long as that record isn't deleted, the id
will remain the same.
Since both the rowid
and the internal FWD id
are 64-bit whole numbers, this implementation is safe and clean. There is no potential for loss of data when there are more than 4 billion rows in the database (see the RECID section above).
The persistence layer tracks unused id
values and reuses any previous values that are freed up on deletion. This means that id
values only stable until the record is deleted. Then even if the record is recreated, it may get a different id
.
In Progress, dumping or exporting database tables does not include the rowid
values in the dump or export. This means that reloading any dumped or exported data will potentially change the rowid
values. Developers are cautioned not to write code that depends upon specific rowid
values.
When data is migrated to FWD, the exported/dumped data is imported and upon each row insertion, new id
values are assigned. Since there is no rowid
data in the dump files, all new id
values have no relation to the original Progress rowid
values. This means that any rowid
values that were somehow used again in the Java system (for example, as read from some flat file in which the 4GL application recorded the values) will fail to match the proper record.
The Progress behavior in reallocating rowid
values is not documented and should not be relied upon since it is an arbitrary implementation detail that is known to be subject to change. It is possible that application code has been explicitly or implicitly written with this dependency. Any such reliance upon Progress-specific reallocation behavior is dangerous since the reallocation algorithm is not known or documented and the FWD internal implementation is virtually assured to be different.
WIDGET-HANDLE¶
All instances of this type are treated as instances of the HANDLE
type. See that section above for details. This conversion is done very early (in the FWD parser itself) and after that point, there is no further reference to WIDGET-HANDLE
as a different thing from HANDLE
.
Variable Definitions¶
Summary¶
There are 10 language constructs in the Progress 4GL which have the result of defining named variables in business logic. The following table summarizes the FWD support for each:
4GL Language Feature | Description | Supported |
---|---|---|
ASSIGN Database Trigger Parameter | A database trigger that is invoked when the associated field is assigned, passing the old field's value as a named parameter. Specified as a TRIGGER PROCEDURE or in an ON statement. | Yes |
Constructor Parameter | Object-oriented 4GL extensions allow class definitions. Class definitions may contain a CONSTRUCTOR statement (a special method called to create a new instance of the object). Parameters to the constructor are local variables that are scoped to that method. | Yes |
DEFINE PARAMETER | Specifies a parameter for an external procedure or internal procedure. | Yes |
DEFINE PROPERTY | A readable and/or writable data member of a Progress 4GL class. | Yes |
DEFINE VARIABLE | Yes |
|
Format Phrase with AS or LIKE | Yes |
|
Function Parameter | Specifies a parameter for a user-defined function. | Yes |
MESSAGE SET/UPDATE with AS or LIKE | Yes |
|
Method Parameter | Yes |
|
Property Setter Parameter | Yes |
DEFINE VARIABLE¶
This statement creates a new variable or accesses an existing variable instance for the local procedure. This statement determines the data type and name of the variable. It can optionally specify certain configuration.
At a high level, Progress 4GL has 2 main forms of this statement. The following summarizes FWD support for these forms:
Form | Description | Supported | Notes |
---|---|---|---|
DEFINE <sharing_flags> VARIABLE { AS [HANDLE TO] <primitive_type_name> @|@LIKE <field> } [options]. |
Defines a variable in the local procedure or function OR imports a reference to a shared variable that was defined elsewhere. | Yes | The following are NOT supported: • data types which are not yet supported by FWD (see the Variables section of this chapter) • HANDLE TO |
DEFINE <access_modifiers> VARIABLE AS [CLASS] <classname> [options]. |
Defines a variable which is a reference to an instance of a valid class (object oriented 4GL extension). | No | |
This section of the book will only describe how the non-shared form of this statement is converted. For details on how the <sharing_flags>
are implemented, see Shared Variables below.
All of the primitive data types which are supported in FWD as variables can be used in the AS
clause. The use of both the LIKE
is fully supported. With LIKE
the data type and configuration (options) of the field are copied into the newly defined variable as if they were explicitly defined in the source code. All other processing is the same as with the AS
clause.
The following is a simple 4GL example:
define variable txt1 as character. define variable num2 as decimal. define variable dat3 as date.
If this code is in an external procedure, the resulting Java code will look like this (the comments have been added by hand to make it easier to understand):
@LegacySignature(type = Type.VARIABLE, name = "txt1") character txt1 = UndoableFactory.character(); @LegacySignature(type = Type.VARIABLE, name = "num2") decimal num2 = UndoableFactory.decimal(); @LegacySignature(type = Type.VARIABLE, name = "dat3") decimal dat3 = UndoableFactory.decimal(); @LegacySignature(type = Type.MAIN, name = "dt2.p") public void execute() { externalProcedure(Dt2.this, new Block((Init) () -> { // initialization }, (Body) () -> { // body })); }
Non-shared variables that are unreferenced by any other 4GL code in the same external procedure, are considered dead code and may be removed by the conversion process (they will not appear in the Java output).
Variables are always created as members of a class. Some variables are created as data members of the business logic class (the main class for the file) and other variables are created as data members of the anonymous inner class of type Block
to which the variable is scoped. In the above example, the variables are all created as members of the class that corresponds to the 4GL procedure. For details on how the location of the variable definition is chosen, see the section below entitled Scoping.
Each 4GL primitive data type that has a supported replacement in FWD will emit as a new instance of that replacement class. The section above on each data type describes which types are supported and the Java wrapper class they convert into.
Each 4GL variable name will be converted to a valid Java variable name. All references to that variable will likewise use that same converted name. For details on how variable names convert, please see the Naming section of the Other Customization chapter of the FWD Conversion Handbook.
Variables are always initialized and the value will be something non-null. There are 3 approaches that can be used. First, the default constructor for the type can be used. The default constructor is almost always equivalent to initializing to the unknown value, except for the date
class. Second, a constructor can be used that accepts a Java literal (a String
, an int
, ...) that encodes a specific value for the variable. This value can either be the default value for the type (where no INITIAL
option is provided) or an explicitly defined value (through the INITIAL
option). Third, a special factory method can be used to return a new instance. For example, this is done to create a date
instance with the unknown value. For more details on variable initialization, see Initialization below.
User-interface specific options are removed from the replacement business logic but their settings will be honored in any frames or user-interface processing that reference that parameter. Non-user-interface options are set into the variable instances in the init()
method, as described in the section below entitled Data Type Options.
Options summary:
Option | Description | Supported | Notes |
---|---|---|---|
BGCOLOR |
Background color in GUI. | Yes | See Part 6 of this book. |
CASE-SENSITIVE |
The character variable will be compared and sorted on a case-sensitive basis. | Yes | See Data Type Options below. |
COLUMN-LABEL |
When this variable is used as a widget in a frame, this defines the descriptive text column label for the widget. | Yes | See Part 6 of this book. |
CONTEXT-HELP-ID |
Help file index. | No | See Part 6 of this book. |
DCOLOR |
Display color in CHUI. | Yes | See Part 6 of this book. |
DECIMALS |
Sets the number of digits to the right of the decimal point for this decimal variable. | Yes | See Data Type Options below. |
DROP-TARGET |
Drag and drop support. | No | See Part 6 of this book. |
EXTENT |
Defines an array variable with a specific number of elements. | Yes | See Arrays below. |
FONT |
Font for the widget in GUI. | No | See Part 6 of this book. |
FGCOLOR |
Foreground color in GUI. | Yes | See Part 6 of this book. |
FORMAT |
Specifies the pattern used to format the output of this variable when used as a widget in a frame. | Yes | See Part 6 of this book. |
INITIAL |
Defines an explicit initial value for the variable. | Yes | See Data Type Options below. |
LABEL |
When this variable is used as a widget in a frame, this defines the descriptive text label for the widget. | Yes | See Part 6 of this book. |
MOUSE-POINTER |
Mouse pointer for the widget. | No | See Part 6 of this book. |
NO-UNDO |
UNDO support is disabled for the variable. | Yes | See UNDO below. |
NOT CASE-SENSITIVE |
The character variable will be compared and sorted on a case-insensitive basis. | Yes | See Data Type Options below. |
PFCOLOR |
Prompt-for color in CHUI. | Yes | See Part 6 of this book. |
TRIGGERS phrase |
Widget-specific trigger definition. | No | See Part 6 of this book. |
VIEW-AS phrase |
Sets the type of widget and type-specific configuration (by default type is FILL-IN). | Yes | See Part 6 of this book. |
Format Phrase with AS or LIKE¶
Inside any statement that accepts a format phrase, the 4GL programmer can define a new variable "on the fly". This concept is similar to the inline creation of a new variable in a MESSAGE SET/UPDATE
statement (see MESSAGE SET/UPDATE with AS or LIKE below). The syntax can take 2 forms:
AS <primitive_data_type>
Or:
LIKE <database_field>
No explicit variable options can be specified (e.g. CASE-SENSITIVE
or INITIAL
). In the case of a LIKE
clause, the data type and configuration of the field are used as the pattern on which to create the variable.
The effective result of this 4GL construct is the same as if a matching DEFINE VARIABLE
statement had been specified in the 4GL code just above the statement containing the format phrase.
This simple example:
DISPLAY invoice AS integer LABEL "Invoice".
is equivalent to this 4GL code:
DEFINE VARIABLE invoice AS INTEGER. DISPLAY invoice LABEL "Invoice".
The use of LIKE works the same way, with the exception that the core configuration (e.g. CASE-SENSITIVE
, DECIMALS
, EXTENT
, INITIAL
...) will be the same as the field on which the variable is patterned. This has the same effect as if a DEFINE VARIABLE
statement with these same values was explicitly coded in the 4GL.
For details on the Java conversion, please see the DEFINE VARIABLE section above.
MESSAGE SET/UPDATE with AS or LIKE¶
This form of the MESSAGE
statement (with the SET
or UPDATE
clause) implements an editing operation, which provides a quick way for prompting the user for a single value. It is possible to use an AS
or LIKE
clause with this edit operation, such that a variable is defined as part of the same statement. This concept is similar to the inline creation of a new variable in a format phrase (see Format Phrase above).
The syntax can take 2 forms:
MESSAGE … { SET | UPDATE } AS <primitive_data_type> [edit_options]
Or:
MESSAGE … { SET | UPDATE } LIKE <database_field> [edit_options]
The edit options include a FORMAT
string and the AUTO-RETURN
option. Both of these options only serve to modify or control the editing operation, so they are user-interface features (see Part 6 of this book) and thus have no impact on the variable definition itself. No explicit variable options can be specified (e.g. CASE-SENSITIVE
or INITIAL
). In the case of a LIKE
clause, the data type and configuration of the field are used as the pattern on which to create the variable.
The effective result of this 4GL construct is the same as if a matching DEFINE VARIABLE
statement had been specified in the 4GL code just above the MESSAGE
statement.
This simple example:
MESSAGE UPDATE invoice AS integer FORMAT "999999" AUTO-RETURN.
is equivalent to this 4GL code:
DEFINE VARIABLE invoice AS INTEGER. MESSAGE UPDATE invoice FORMAT "999999" AUTO-RETURN.
The use of LIKE works the same way, with the exception that the core configuration (e.g. CASE-SENSITIVE
, DECIMALS
, EXTENT
, INITIAL
...) will be the same as the field on which the variable is patterned. This has the same effect as if a DEFINE VARIABLE
statement with these same values was explicitly coded in the 4GL.
For details on the Java conversion, please see the DEFINE VARIABLE section above.
Initialization¶
Variables created with DEFINE VARIABLE
get initialized in the same line of code as the variable declarations itself. Since these variable instances are always data members of the containing business logic class or of an anonymous inner class (of type Block
), that is the same location in which the initializer will be found. The initializer will differ depending on whether there is a default value or an explicit initializer that was specified with the INITIAL
option.
For procedure parameters, initialization occurs as an assign()
method call during the Block.init()
method of the anonymous inner class in which the parameter is a member. See the DEFINE PARAMETER section above and the Assignment section below for details.
Function parameters work the same way, except they are always initialized to the unknown value (they cannot have an explicit initializer specified with the INITIAL
option).
The reason that parameters (both procedure and function) need to be initialized in the Block.init()
method is that the replacement Java variable is also a parameter in the method's signature list. Java doesn't provide a mechanism to initialize method parameters inside the signature itself, so in FWD the Block.init()
method is used. This location is guaranteed to be executed once (and only once) and that execution will occur before the Block.body()
method will ever run. This means that the initialization is guaranteed to occur before the first "real" reference to the value of that variable.
The following table shows the initializers used for each type, depending on the situation:
Type | Default Value | INITIAL Option (Explicit) | Unknown Value |
---|---|---|---|
character | "" (empty string) | new character(String) |
new character() |
date | ? (unknown value) | date.fromLiteral(String) ORdate.today() (used for the 4GL TODAY function) |
new date() |
datetime | ? (unknown value) | datetime.fromLiteral(String) ORdatetime.now() (used for the 4GL TODAY function) |
new datetime() |
datetime-tz | ? (unknown value) | datetimetz.fromLiteral(String) ORdatetimetz.now() (used for the 4GL TODAY function) |
new datetimetz() |
decimal | 0 | decimal.fromLiteral(String) ORnew decimal(int) |
new decimal() |
handle | ? (unknown value) | n/a | new handle() |
integer | 0 | new integer(int) ORnew integer(double) |
new integer() |
Int64 | 0 | new int64(int) ORnew int64(double) |
new int64() |
logical | false | new logical(boolean) |
new logical() |
longchar | ? (unknown value) | new longchar(String) |
new longchar() |
memptr | uninitialized byte array (not the same as unknown value!) | n/a | memptr.instantiateUnknown() |
raw | zero length byte array (not the same as unknown value!) | n/a | raw.instantiateUnknown() |
recid | ? (unknown value) | n/a | new recid() |
rowid | ? (unknown value) | n/a | rowid.instantiateUnknown() |
For those data types which allow the INITIAL
option, the constant which is the parameter for that option will be rendered as the proper type and emitted as a parameter to the associated variable's constructor. The only exceptions are the 4GL TODAY
and NOW
functions used as a "constant" for the date
and datetime
/ datetimetz
respectively, which are equivalent to the today
and now
static methods of the respective classes.
Scoping¶
Progress 4GL has certain top-level block types. Top-level blocks are those which can be named or invoked directly. FWD supports external procedures, internal procedures, user-defined functions (non-forward) and triggers as top-level blocks. Inside top-level blocks, there can be "inner blocks" such as DO
, REPEAT
and FOR
. These inner blocks can be nested arbitrarily.
Progress 4GL variables can be declared in any top-level block and it is possible to declare variables in one block (e.g. the external procedure) and then to access those variables from other top-level blocks in the same file. Even if the variable is declared deep inside some nested inner block in the external procedure, it can be accessed by any other code in the entire file so long as that code exists sequentially later in the file than the declaration.
In Java, the scope of a variable is largely determined by where in the program the variable is declared. If it is declared as a data member of the outermost class, then everything in the class (including inner classes) can access that variable. If a variable is declared inside a specific method or inner class, then the visibility and lifetime of that member will be limited to that scope.
The FWD approach is to duplicate the 4GL behavior when needed, but to do it in a Java way. This means that the FWD conversion must analyze the usage (all references) of each variable throughout the entire file. As a general rule, the scope of a variable is set to current inner block so long as it contains all references to that same variable instance. That location will be an anonymous inner class of type Block
and the variable will be a data member of that inner class. This handles many common cases of variable declaration.
There are cases in which variables are accessed across top-level blocks or which in Java (due to how the converted code must be written to duplicate 4GL behavior) will have a need to be accessed from methods of the business logic class as well as from inner classes that only exist at the top-level of the business logic class. In such cases, the variable is "promoted". Promotion is the idea that a variable needs to be a data member of the outermost class (the main or business logic class for the file, which contains everything else) because it simply must have entire-file scope.
It is important to note that the primary reason for a top-level inner class in the converted Java code is to provide some kind of callback expression or logic. For example, validation expressions for user-interface widgets are registered and evaluated by the FWD runtime (just as in the 4GL). But to do this in Java, an inner class is created which contains a method that provides that expression evaluation. The class definition for the inner class exists inside the topmost level of the business logic class. Instances of that inner class are created and passed to the FWD runtime. Inside those instances, the validation expression can reference any data that is in scope in the 4GL business logic at the point that validation expression was defined. This means that variables can be directly accessed from that expression. In that case, those variables would be promoted so that they are visible to both the validation expression (which is now an inner class) as well as to the rest of the business logic class.
The following describes the reasons to promote:
- If the variable is defined in one top level block and then used in another top level block (without a local redefinition of the same name).
- Any variables that were defined in a nested block of the current top-level scope where that defining block does not enclose the current location. In this case, the "simple" approach of promoting is taken. A better approach would be to find the nearest inner block which encloses both the definition and all references (if such a thing exists, which is not guaranteed). This optimal approach is not yet implemented.
- Any variables that are part of a validation expression (or validation message expression).
- Any variables used in accumulate expressions.
- Any variables in client-side
WHERE
clause expressions (see Part 5 of this book). - Any variables in query substitution expressions (see Part 5 of this book).
- Any variables in an embedded assignment expression (see Part 6 of this book,
UPDATE
orSET
statement). - Any variables in an expression in a
FORM HEADER
statement (see Part 6 of this book). - Any variables referenced in a
BY
clause in a query (see Part 5 of this book). - Any variables in a dynamic
COLUMN
,ROW
,DOWN
orTITLE
expression (see Part 6 of this book). - Any variables in a complex expression or an array reference (use of the subscript operator) in a
PUT
orEXPORT
statement. A complex expression is one that is not a simple literal or variable reference. See the Streams chapter of Part 4 of this book.
It is possible to have more than one variable of the same name, which all need to be promoted. To resolve this, one of the variable definitions will keep the standard converted name and the other definitions will have a Ref<num>
(where num
is 2 or greater) appended to the end of the name, making the definition and all references unique. The same conversion logic that calculates promotion, also disambiguates variable names that would conflict in Java but which did not conflict in the 4GL.
References¶
Variable references in the 4GL convert differently depending on context.
Those references which are the left operand of the assignment operator (=
) will have their value assigned to the result of the expression that is the right operand of the assignment operator. For details on how this is converted, please see the Assignment section below.
References to EXTENT variables (array references) use a subscript operator ([]
) in the 4GL. That operator obtains a specific element from the array. For details of how this is converted, please see the Arrays section below.
Some text in 4GL programs appear to be variable references, but instead are actually references to a specific widget (of the same name and patterned off the corresponding variable) in a frame. These widget references are part of the static user-interface 4GL code that is re-factored into frame definitions. As such, it is removed from the business logic. For details on widget references, see Part 6 of this book. The key point here is that these are not actually variable references, so they do not emit into Java variable references.
All other variable references emit into Java as standard Java variable references. Of course, the converted variable name is used as the Java name, so that it matches the Java variable declaration. A 4GL variable named my-date-var
might be converted to a Java name of myDateVar
. For example, this 4GL:
if my-date-var > 12/31/1999 then
Would look like this in Java:
if (myDateVar._isGreaterThan(new date("12/31/1999"))
The key point here is that the simple reference is emitted in the same expression location using the converted name.
The handle
data type emits like a regular variable reference, but in some use cases handles require an additional "unwrapping" of the contained Java object reference inside. To unwrap the contained instance, the handle.get()
method is called and the result is cast to the type needed. For details, see the Expressions chapter in Part 4 of this book.
4GL WHERE
clauses convert to HQL expressions that are String literals. Any variables 4GL WHERE
clause must be passed into the query as a list of "query substitution parameters". The variable reference will emit as normal, but it will emit into an array of these parameters instead of inside the HQL expression. Inside the HQL expression, instead of the variable reference, there is a ?
character, which indicates that a query substitution parameter must be substituted. For details, please see Part 5 of this book.
Data Type Options¶
Generally, the Java replacement code for core data type options are emitted in the Block.init()
method of the anonymous inner class of which the variable is a member. The exception to this rule is for variables that are defined as data members of the business logic class itself (instead of inside any specific anonymous inner class). This can happen due to scoping requirements (a variable that must be accessed from the entire class). For data members of the business logic class, the Java options code is emitted in the Block.init()
of the Block instance (an anonymous inner class) which is passed to the BlockManager.externalProcedure()
method.
Case-sensitivity is implemented in the com.goldencode.p2j.util.character
class. Decimal precision is implemented in the com.goldencode.p2j.util.decimal
class. For both of these options, Progress 4GL has an unusual behavior. Shared variables that are imported (DEFINE SHARED VARIABLE...
) can temporarily override the CASE-SENSITIVE
and DECIMALS
configuration of the shared instance. Then when the scope of the importing procedure is complete, the original configuration values are restored. To implement this difference, the conversion emits different method calls for setting the same configuration value.
The following describes the conversion:
4GL Syntax | Standard Java Syntax | Import Shared Variable Java Syntax |
---|---|---|
CASE-SENSITIVE |
var.setCaseSensitive(true); | var.setTemporaryCaseSensitive(true); |
DECIMALS <num> |
var.setPrecision(num); | var.setTemporaryPrecision(num); |
EXTENT <num> |
See Arrays below. | See Arrays below. |
INITIAL <literal> |
See Initialization above. | See Initialization above. |
NO-UNDO |
See UNDO below. | See UNDO below. |
NOT CASE-SENSITIVE |
var.setCaseSensitive(false); | var.setTemporaryCaseSensitive(false); |
For options relating to the user-interface, please see Part 6 of this book. The following user-interface options can be used in data type definitions and are documented there: BGCOLOR
, COLUMN-LABEL
, CONTEXT-HELP-ID
, DCOLOR
, DROP-TARGET
, FONT
, FGCOLOR
, FORMAT
, LABEL
, MOUSE-POINTER
, PFCOLOR
, VIEW-AS
phrase, TRIGGERS
phrase.
Assignment¶
Scalar (non-array) variable assignment takes one of two forms. If the right operand of the assignment operator is the unknown value literal (?
), then the BaseDataType.setUnknown()
method will be called. Otherwise, the value is set via the BaseDataType.assign()
.
The key point here is that the variable reference is NOT modified, only the variable's value is modified. This allows use of Java features like the final
modifier to be used in parameters. It also reduces the need for null checking or other more complex expression re-factoring, since the reference is always non-null and does not change. This approach also matches the Progress 4GL semantic which is to set the value rather than to change the reference to point to a new instance (which is the Java assignment operator's behavior).
This 4GL code:
txt1 = ?. num2 = 0. dat3 = 12/31/1999.
Results in this Java code:
txt1.setUnknown(); num2.assign(0); dat3.assign(date.fromLiteral("12/31/1999"));
All variables inherit from BaseDataType
. This parent class is abstract
and requires these methods to be present in each subclass. The setUnknown() method requires no variants, but it is implemented in the subclasses since each subclass may have a unique mechanism to detect if the instance is equivalent to the unknown value. The assign() method is different. An abstract
version of assign(BaseDataType, boolean)
exists in BaseDataType
and it is used in some cases. In addition, BaseDataType
implements the Undoable
interface (but doesn't implement the assign(Undoable)
, leaving that method abstract
). But more importantly, each subclass implements its own set of assign()
methods. This enables each type to provide direct assignment support for all the primitive Java types that makes sense, in addition to a version that takes the different wrapper types. For example, the decimal
type (which is a child class of NumberType
, which in turn is a child class of BaseDataType
) has these 6 assign()
methods:
public void assign(BaseDataType, boolean) /* casts the 1st parm to NumberType and calls the next */ public void assign(NumberType, boolean) public void assign(double) public void assign(int) public void assign(NumberType) public void assign(Undoable)
For the assignment of array elements, see Arrays below.
For attribute assignment, see the Attributes section of the Expressions chapter of Part 4 in this book.
For database field assignment, see Part 5 of this book.
Assignment can also occur using the ASSIGN
language statement. See the next section for details.
ASSIGN Statement¶
The ASSIGN
language statement provides a way to group assignments into a batch. Due to the internal 4GL runtime implementation, this form of assignment is more efficient than assigning values individually. The FWD equivalent has no such difference in efficiency. The result in FWD is the same as individual assignments except there is a batch notification for database fields which duplicates the transactional behavior of the Progress 4GL ASSIGN
statement. Please see Part 5 of this book for more details on batching.
All forms and options of this statement are fully supported.
Syntax¶
There are 2 main forms:
4GL | Purpose | Supported | Notes |
---|---|---|---|
ASSIGN record [EXCEPT field_list] [NO-ERROR]. |
Fields in the record are assigned the corresponding values from the associated frame's screen buffer. | Yes | |
ASSIGN [[INPUT] FRAME frame | BROWSE browse] { lvalue [ = expression ] [WHEN expression] } … [NO-ERROR]. |
In this form, there is a list of variables, database fields and attributes which are assigned EITHER from the corresponding screen-buffer OR from an explicit assignment expression in the statement itself. | Yes | |
The 1st syntax form (the record form) is fully supported including the EXCEPT
list. This is handled at conversion time by expanding the record reference into an complete list of fields in that table (minus any excluded fields). When the expansion is complete, it is as if the fields has been explicitly defined in the 4GL source code. Everything else works just like the 2nd form of the ASSIGN
statement.
For example, this 4GL code (assume that "tablename" is a database table with fields named f1, f2, f3 and f4):
ASSIGN tablename EXCEPT f4.
is logically equivalent to the more verbose form:
ASSIGN tablename.f1 tablename.f2 tablename.f3.
Since the conversion process transparently handles converts the 1st syntax form into the 2nd syntax form, all output will be converted as documented for 2nd syntax form.
The 2nd syntax form has 2 different logical modes. The first logical mode could best be called the "implicit screen-buffer assignment". The second logical mode is best called "normal assignment".
Options:
4GL syntax | Description | Supported |
---|---|---|
[ FRAME frame | BROWSE browse ] lvalue |
The name of the variable or database field to be assigned from screen-buffer value or the evaluated expression. The variable/field name must be qualified by a frame or browse name if the widget name is ambiguous (it exists in more than one frame). | Yes |
expression |
The expression to evaluate. The resulting value is assigned to the variable or database field. | Yes |
WHEN expression |
The assignment of the preceding variable or database field only occurs if the expression used in the WHEN option evaluates to TRUE (at assignment time). | Yes |
record |
Specifies the name of the record buffer to implicitly assign from the screen-buffer. All of the fields of the buffer will be processed just as if it each database field was listed explicitly. | Yes |
EXCEPT field ... |
This is an optional list of database fields to exclude from assignment. | Yes |
NO-ERROR |
Specifies that any errors that occur in the attempt to set the value are suppressed. See the Transactions chapter for more details on "silent error mode". | Yes |
Implicit Screen-buffer Assignment¶
In implicit screen-buffer assignment, the purpose is to assign changes from the screen-buffer back into the associated variables (or database fields). Both of the syntax forms shown above, can implement this logical form of the statement.
Here is an example:
define variable num as integer. define variable txt as character. define variable flag as logical initial true. set num txt. assign num txt when flag.
The resulting Java code (ASSIGN
statement only):
frame0.assignScreenValue(new Element(num, frame0.widgetNum())); if ((flag).booleanValue()) { frame0.assignScreenValue(new Element(txt, frame0.widgetTxt())); }
The idea is that this form of the statement is a shorthand that can be specified instead of explicitly coding the assignment operator and an expression for obtaining the screen-buffer value of the associated widget.
The commonly used syntax is to specify explicit widget lists (which can be either variables or database fields) in the statement. This is shown in the example above and it corresponds with the syntax variant 2 in the above table.
Array expansion can also be used in this syntax variant, instead of explicitly listing each element of the entire array (or a range of the array). The array expansion form effectively gets expanded into the same result as the explicitly defined list.
This implicit screen-buffer assignment mode can also be used with the "record" syntax form (1st syntax form in the above table), which just gets expanded into the 2nd syntax form.
This is a user-interface feature. More details and examples regarding this can be found in the Editing chapter of Part 6.
Normal Assignment¶
The normal assignment logical form of the ASSIGN
statement is that which just is a list of variable, database field or attribute references, each followed by an assignment operator and an expression which evaluates to a compatible type. Such a form is converted the same way that normal ("standalone") assignments are converted.
For example, this 4GL code:
define variable num as integer. define variable txt as character. define variable flag as logical initial true. assign num = 14 txt = "whatever" when flag.
This results in the following Java code (ASSIGN
statement only):
num.assign(14); if ((flag).booleanValue()) { txt.assign("whatever"); }
This is the same as if it had been coded as separate assignment operations. In Java there is no penalty for separating the code in this manner. That performance difference is something that is an unfortunate consequence of the Progress 4GL implementation, which FWD does not duplicate.
The WHEN
clause that can be used in the ASSIGN
statement is fully supported. In both examples above, it simply creates a conditional test around the Java assignment code to ensure it is only assigned if the test expression evaluates to true
.
NO-ERROR
is also supported. For details, see the Transactions chapter.
Arrays¶
Progress arrays (EXTENT
variables) are very similar in nature to Java arrays. A variable definition can be an array if it is explicitly specified using the EXTENT
keyword or if the variable is defined LIKE
another variable or field that is defined with extent that is more than 1.
Progress 4GL and FWD also support EXTENT
database fields. For details on EXTENT
fields, please see Part 5 of this book.
FWD only supports extent variables when explicitly created from the DEFINE VARIABLE
statement (with an EXTENT
option) or when implicitly created using a LIKE
clause. Interestingly, the LIKE
clause can be used in the Format Phase and the MESSAGE SET/UPDATE
variable definitions. FWD supports those.
In later versions of the 4GL, DEFINE PARAMETER
and Function Parameters support the EXTENT
option. While FWD does support such parameters, FWD does not support extents for parameters at this time.
Definition and Initialization¶
4GL array definition and initialization map closely to Java array variable definitions and initializers. Some helper methods are used to fully duplicate the implicit initialization behavior that occurs when not all elements of the array have explicitly specified initialization constants.
There are 3 forms that can be seen. The differences between these forms is in how many initialization constants are specified in the INITIAL
option (or if there is an INITIAL
option at all).
One form is the no INITIAL
option form. 4GL code:
define variable txt1 as character extent 3. define variable dat2 as date extent 3.
Here is the resulting Java code:
@LegacySignature(type = Type.VARIABLE, extent = 3, name = "txt1") character[] txt1 = UndoableFactory.characterExtent(3, ""); @LegacySignature(type = Type.VARIABLE, extent = 3, name = "dat2") date[] dat2 = UndoableFactory.dateExtent(3);
Scoping and UNDO is done the same way as for scalar (non-array) variables. TransactionManager.register()
method has special processing for when one of the parameters is an array of Undoable
instances. It loops through the array and registers each element in the list.
The next form of array definition is the "complete" INITIAL
option form. 4GL code:
define variable txt5 as character extent 3 initial [ "hello", "world", "!" ]. define variable bool6 as logical extent 3 initial [ yes, no, true ].
This results in the following Java code:
@LegacySignature(type = Type.VARIABLE, extent = 3, name = "txt5") character[] txt5 = UndoableFactory.characterExtent(3, "hello", "world", "!"); @LegacySignature(type = Type.VARIABLE, extent = 3, name = "bool6") logical[] bool6 = UndoableFactory.logicalExtent(3, true, false, true);
In this case, every element in the array has an explicit initializer constant in the INITIAL
option. This means that the Java in-line initializer can be used instead of needing to emit additional initialization.
The last form is the "partial" INITIAL
option form. 4GL code:
define variable num3 as integer extent 10 initial [ 14, 30 ]. define variable dat4 as date extent 10 initial [ ?, 01/01/2000, 12/31/1999 ].
This results in the following Java code:
@LegacySignature(type = Type.VARIABLE, extent = 10, name = "num3") integer[] num3 = UndoableFactory.integerExtent(10, (long) 14, (long) 30); @LegacySignature(type = Type.VARIABLE, extent = 10, name = "dat4") date[] dat4 = UndoableFactory.dateExtent(10, date.fromLiteral("01/01/2000"), date.fromLiteral("12/31/1999"));
In the 4GL, this form will initialize each explicitly defined element of the array to the given value and all non-specified values will be initialized to the same value as the last explicitly specified element. FWD duplicates this behavior exactly. In the example above, the dat4
array has 10 elements but only 3 explicit initializers are specified in the INITIAL
option. The first (da4[0]
) and second (dat4[1]
) elements are initialized using the corresponding explicit initializers. Then the third and all subsequent elements (dat4[2]
through dat4[9]
) are initialized with separate copies of the same value. The 2nd parameter to that call forces the assignment starting index to 2.
All three of these definition and initialization forms can also be used in conjunction with shared variable support. Here is 4GL example code:
define shared variable txt1 as character extent 3. define new shared variable dat2 as date extent 3. define new global shared variable dec3 as decimal extent 3. define shared variable num4 as integer extent 10 initial [ 14, 30 ]. define new shared variable dat5 as date extent 10 initial [ ?, 01/01/2000, 12/31/1999 ]. define new global shared variable txt6 as character extent 10 initial [ "hello", "world" ]. define shared variable txt7 as character extent 3 initial [ "hello", "world", "!" ]. define new shared variable bool8 as logical extent 3 initial [ yes, no, true ]. define new global shared variable num9 as integer extent 3 initial [ 0, 1, 14 ].
And here is the resulting Java code:
@LegacySignature(type = Type.VARIABLE, extent = 3, name = "dat2") date[] dat2 = SharedVariableManager.addVariable("dat2", new date[3]); @LegacySignature(type = Type.VARIABLE, extent = 3, name = "dec3") decimal[] dec3 = SharedVariableManager.addVariable(ScopeLevel.GLOBAL, "dec3", new decimal[3]); @LegacySignature(type = Type.VARIABLE, extent = 10, name = "dat5") date[] dat5 = SharedVariableManager.addVariable("dat5", new date[10], () -> new date[] { new date(), date.fromLiteral("01/01/2000"), date.fromLiteral("12/31/1999") }); @LegacySignature(type = Type.VARIABLE, extent = 10, name = "txt6") character[] txt6 = SharedVariableManager.addVariable(ScopeLevel.GLOBAL, "txt6", new character[10], () -> new character[] { new character("hello"), new character("world") }); @LegacySignature(type = Type.VARIABLE, extent = 3, name = "bool8") logical[] bool8 = SharedVariableManager.addVariable("bool8", new logical[3], () -> new logical[] { new logical(true), new logical(false), new logical(true) }); @LegacySignature(type = Type.VARIABLE, extent = 3, name = "num9") integer[] num9 = SharedVariableManager.addVariable(ScopeLevel.GLOBAL, "num9", new integer[3], () -> new integer[] { new integer(0), new integer(1), new integer(14) });
The SharedVariableManager
is used to import shared variable references or create new shared variables. This is done just as explained in the Shared Variables section above. One difference is that any explicit or implicit initializers are dropped (not implemented) for any import of a shared variable using lookupVariable()
). This is the same behavior as the 4GL, it is just more obvious in the resulting Java. For new variables, the initializers are the same as for non-shared arrays. The other difference to note is that UNDO registration is only done for the new shared array variables and is not done for the imported shared variables.
References¶
The referencing of array elements is also very similar between Java and Progress (both use numeric expressions inside postfixed square brackets to index the element). The biggest difference: Progress 4GL indexes arrays using a 1-based index. Java uses a 0-based index.
At first glance, it seems that the 1-based versus 0-based indexing difference can be handled during conversion. The Java subscript operator []
could be directly mapped to the Progress subscriipt operator []
. Constant subscript expressions could be emitted directly into the source code by decrementing 1 from the 1-based constant, to provide the proper 0-based version.
For non-constant expressions, a method could be used to calculate the index or the expression could be re-factored to subtract 1.
The problem with these two approaches is that they do not allow for the proper duplication of Progress 4GL control flow and error handling when indexes are out of range (the result would be an IndexOutOfBoundsException
in Java). If a FWD runtime method is used to calculate the index, then it could raise the proper error condition, but so long as the Java []
operator is used with the result, then with use of NO-ERROR
(silent error mode) array access will still fail with an IndexOutOfBoundsException
. This occurs because the in silent error mode, the error condition is suppressed and the evaluation of any expression continues.
To eliminate this issue, static
methods ArrayAssigner.subscript(T[] array, one_based_index)
are used to hide the 1 to 0 based index calculation, the actual dereferencing of the array and all associated error handling. This allows proper duplication of the 4GL behavior. The trick is that the reference to the array is passed as the first parameter to the subscript() method. This allows the method to do range checking since the index must be between 0 and (array.length - 1)
. The second parameter is the 1-based index which must be decremented and bounds-checked.
For details on how array elements are dereferenced when they appear as the left operand of an assignment operator, see Assignment below.
For details on array field references, see Part 5 of this book.
For details on frame widget array references cases, see Part 6 of this book.
Assignment¶
Just as there are control flow and error handling issues with dereferencing array elements inside normal expression processing (see References above), these same issues exist when assigning the value of an array element. For this reason, ArrayAssigner static
methods are available to match the proper behavior. Here is a summary:
4GL Syntax | Java Syntax | Purpose | Supported |
---|---|---|---|
array[index] = ? |
assignSingleUnknown(array, index) |
Set a single element to the unknown value. | Yes |
array[index] = expression |
assignSingle(array, index, value) |
Set a single element to the given expression value. | Yes |
array = ? |
assignUnknown(array) |
Set all elements of the array to the unknown value. | Yes |
array = array2 |
assignMulti(array, array2) |
Set all elements of array to the correspondingly indexed elements in array2. | Yes |
Here is 4GL example code:
define variable num1 as integer extent 10 initial [ 14 ]. define variable num2 as integer extent 10 initial [ 30 ]. num1[1] = ?. num1[2] = num2[1]. num2 = ?. num2 = num1.
In Java this the resulting code:
@LegacySignature(type = Type.VARIABLE, extent = 10, name = "num1") integer[] num1 = UndoableFactory.integerExtent(10, (long) 14); @LegacySignature(type = Type.VARIABLE, extent = 10, name = "num2") integer[] num2 = UndoableFactory.integerExtent(10, (long) 30); @LegacySignature(type = Type.MAIN, name = "start.p") public void execute() { externalProcedure(Start.this, new Block((Body) () -> { assignSingleUnknown(num1, 1); assignSingle(num1, 2, subscript(num2, 1)); assignUnknown(num2); assignMulti(num2, num1); })); }
Data Type Options¶
Data type options for arrays are different, since they must be configured in every element of the array, rather than for the array itself. This means that static
methods are used in the BaseDataType
subclass in which the option is implemented. The first parameter is the reference to the array itself. This allows the static
method to iterate though all element of the array and set the option on each one.
The following describes the conversion:
4GL Syntax | Standard Java Syntax | Import Shared Variable Java Syntax |
---|---|---|
CASE-SENSITIVE |
Not supported. | Not supported. |
DECIMALS <num> |
decimal.setPrecision(decimal[] array, int num); | Not supported. |
NOT CASE-SENSITIVE |
Not supported. | Not supported. |
The unsupported options can be easily enabled with the addition of static methods for setting the option and minor changes to conversion rules for variable definition.
Unsubscripted Array Reference Expansion¶
Unsubscripted array variable references are possible in certain conditions in the 4GL. They are not legal inside expressions, but can be used in the following language statements:
DEFINE FRAME FORM CHOOSE … FIELD DISPLAY DEFINE BROWSE … DISPLAY COLOR ON widget-list SET … TEXT UPDATE … TEXT PROMPT-FOR … TEXT ENABLE … TEXT SET UPDATE PROMPT-FOR ENABLE DISABLE ASSIGN ACCUMULATE EXPORT IMPORT
The usage is a syntax shortcut. It is used instead of specifying a list of all elements, each using the subscripting syntax. The syntax is to use the array variable name by itself, with no subscript at all. This form can't be used inside normal expressions but in the above language statements it is implicitly expanded to be the same as a list of those elements.
Here is a 4GL code example:
define variable num as integer extent 3. set num. assign num.
The key here is that the num array is expanded as if the 4GL had an explicit list like this:
SET num[1] num[2] num[3].
This results in the following Java code (SET
statement only):
FrameElement[] elementList0 = new FrameElement[] { new Element(subscript(num, 1), frame0.widgetNumArray0()), new Element(subscript(num, 2), frame0.widgetNumArray1()), new Element(subscript(num, 3), frame0.widgetNumArray2()), }; frame0.set(elementList0);
FWD fully supports this syntax, by detecting and expanding the implicit references during conversion.
Range Subscripting Syntax Expansion¶
The same language statements (see Array Expansion above) that support the array expansion syntax, also support the specification of a range of elements using a FOR
construct inside the subscript operator (after the first numeric expression). This form can't be used inside normal expressions but is implicitly expanded to be the same as a list of those elements.
The general syntax is:
array_var[numeric_expr FOR num_literal]
This syntax can't be used everywhere, but it is known to work anywhere an unsubscripted array reference will work. This means it is something that can and should be expanded. FWD does this during conversion, except instead of expanding the entire array, it only expands the specific referenced range.
Range expansion in FWD only works if the numeric_expr
is a literal. The end of the range specification (the number after the FOR
keyword) must always be a numeric literal in the 4GL. When the start of the range is specified as a constant, the expansion can be statically handled. The conversion can know exactly the list of array elements that must be expanded.
FWD has no support for array subscripts with ranges that use a non-constant expression as the start index. For example, myvar[ 2 + j FOR 3 ]
is something that can only be evaluation at runtime, so the conversion cannot know in advance which start element is being referenced. The dynamic nature of this syntax has implications (for example, it could dynamically change a frame definition). As such, it is unsupported in FWD.
The following is an example of valid 4GL range expansion:
define variable num as integer extent 5. update num[2 FOR 2].
This would be the equivalent of specifying this UPDATE
statement:
update num[2] num[3].
Here is the resulting Java code (UPDATE
statement only):
FrameElement[] elementList0 = new FrameElement[] { new Element(subscript(num, 2), frame0.widgetNumArray1()), new Element(subscript(num, 3), frame0.widgetNumArray2()), }; frame0.set(elementList0);
Limitations¶
This summarizes the limitations currently present in the array support in FWD.
Arrays are fixed size. The v10 support for arrays that have their length determined at runtime is not available.
Use of the range support (FOR
clause) must have a start index which is a constant.
There is no support for arrays as a parameter (this is a v10 feature for procedures and functions). Since arrays cannot yet be used as parameters, there is no support in TransactionManager.deregister()
for arrays.
UNDO¶
By default the following 4GL syntax creates variables that must be registered for UNDO support:
4GL Language Feature | UNDO Disabling Syntax | Supported |
---|---|---|
DEFINE PARAMETER | Use of NO-UNDO option in the statement itself or in the LIKE variable's definition. |
Yes |
DEFINE VARIABLE | Use of NO-UNDO option in the statement itself or in the LIKE variable's definition. |
Yes |
Format Phrase with AS or LIKE | Use of NO-UNDO option in the statement itself or in the LIKE variable's definition. |
Yes |
MESSAGE SET/UPDATE with AS or LIKE | Use of NO-UNDO option in the LIKE variable's definition. |
Yes |
Although Function Parameters are fully supported in FWD, the 4GL always makes function parameters NO-UNDO. This is matched in FWD behavior with a lack of a registration.
The wrapper classes have very little idea about the concept of UNDO (except for the fact that they implement the Undoable
interface which provides the necessary API for capturing a snapshot and restoring that snapshot.
In Progress, all of the above variable definition mechanisms by default result in an UNDO variable. However, in Java the default behavior has been reversed. By default a new instance of a wrapper class (e.g. new integer(14)
) is NO-UNDO
. Instead, in Java, an explicit registration of an Undoable
instance must be made to force a given instance to be UNDO.
UNDO behavior is implemented by registering the associated variable with the TransactionManager
for UNDO support. This must be done in the init()
method of the block in which the variable is defined.
The following TransactionManager
registration methods are used:
Method | Usage |
---|---|
registerUndo(Undoable) | Used to register a single variable for UNDO support starting in the current block. |
register(Object ...) | Used to register a list of variables for UNDO support starting in the current block. |
registerUndo(true, Undoable) | Used to register a single global variable for UNDO support in all blocks. |
register(true, Object ...) | Used to register a list of global variables for UNDO support in all blocks. |
For examples of this, please see the corresponding variable definition sections above. For details on forcing parameters to be NO-UNDO
(TransactionManager.deregister()
), see DEFINE PARAMETER above.
More details on the UNDO implementation can be seen in the Transactions chapter.
© 2004-2022 Golden Code Development Corporation. ALL RIGHTS RESERVED.