Project

General

Profile

Feature #4349

add support for 4GL ENUM

Added by Constantin Asofiei over 4 years ago. Updated almost 3 years ago.

Status:
Closed
Priority:
Normal
Assignee:
Target version:
-
Start date:
Due date:
% Done:

100%

billable:
No
vendor_id:
GCD

Related issues

Related to Base Language - Feature #3751: implement support for OO 4GL and structured error handling Closed
Related to Base Language - Feature #4373: finish core OO 4GL support New

History

#1 Updated by Constantin Asofiei over 4 years ago

  • Related to Feature #3751: implement support for OO 4GL and structured error handling added

#2 Updated by Constantin Asofiei over 4 years ago

  • Tracker changed from Bug to Feature

We need to add conversion (I'm not sure, but 4069a may have some fixes related to this) and runtime support for OpenEdge ENUM. This will require refactoring any skeleton (openedge or .NET) which defines an ENUM - currently, these are emulated by static properties.

#3 Updated by Greg Shah over 4 years ago

#4 Updated by Marian Edu about 4 years ago

Hi Greg, we need FlagsEnum for reflection so tried to implement something for this but there seems this is not yet supported in the conversion - the parser works just fine, the AstGenerator on the other hand seems to be missing any support for enums. There is a TODO line but that's about all it is, those using OO in 4GL are often using enums other than just the built-in ones. I have a draft implementation for Enum and FlagsEnum that I've refactored so maybe the generator could start using that or whatever version you are thinking of, for now we will probably just go ahead and work with the built-in ones.

#5 Updated by Greg Shah about 4 years ago

4GL Behavior/Functionality

  • Enums are implicitly final subclasses of Progress.Lang.Enum (regular enums) or Progress.Lang.FlagsEnum (flags enums). You cannot subclass from an enum.
    • Using reflection you can prove the final flag is true and the parent class for regular enums is Progress.Lang.Enum. See oo/enums/non_flag_enums_are_final_classes_with_superclass_progress_lang_enum.p.
    • Useing reflection you can prove the final flag is true and the parent class for flags enums is Progress.Lang.FlagsEnum. See oo/enums/flag_enums_are_final_classes_with_superclass_progress_lang_flagsenum.p.
    • Using an INHERITS oo.enums.Ordinals in an ENUM statement causes compile error ** Unable to understand after -- "enum oo.enums.". (247) (see oo/enums/EnumsAreImplicitlyFinal1.cls.does_not_compile).
    • Using an INHERITS oo.enums.Ordinals in a CLASS statement causes compile error Cannot inherit from 'oo.enums.Ordinals'. It is an enum. (17968) (see oo/enums/EnumsAreImplicitlyFinal2.cls.does_not_compile).
    • Using an INHERITS oo.enums.Ordinals in an INTERFACE statement causes compile error Cannot inherit from 'oo.enums.Ordinals'. It is a class. (16471) (see oo/enums/EnumsAreImplicitlyFinal3.cls.does_not_compile).
  • Enum definition structure and restrictions.
    • Each enum definition does need to be in its own .cls file.
      • An enum statement must be in a .cls file. It causes the compile failure Enum statement may only be used in files with '.cls' extension. (17950). See oo/enums/enum_stmt_must_be_inside_cls_file.p.does_not_compile.
      • 2 enum statements cannot be in the same .cls file. It causes the compile failure END ENUM statement already encountered. Cannot compile more statements. (17974). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
      • A class statement and enum statement cannot be in the same .cls file. It causes the compile failure END CLASS statement was already encountered. Cannot compile more statements. (12981). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
      • An interface statement and enum statement cannot be in the same .cls file. It causes the compile failure END INTERFACE statement was already encountered. Cannot compile more statements. (12982). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
      • An enum statement cannot be defined inside another enum statement. It causes the compile failure Already defining an ENUM. Cannot compile a CLASS, INTERFACE or another ENUM statement. (17965). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
      • An enum statement cannot be defined inside an interface statement. It causes the compile failure Already defining an INTERFACE. Cannot compile an ENUM statement nested within an INTERFACE statement. (17962). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
      • An enum statement cannot be defined inside a class statement. It causes the compile failure Already defining a CLASS. Cannot compile an ENUM statement. (17963). See oo/enums/EnumsMustBeOnTheirOwn1.cls.does_not_compile.
    • Each enum definition cannot have any members except those defined in the DEFINE ENUM.
      • Variables, properties, temp-tables and widgets cannot be defined inside an enum statement. It causes the compile failure Only specific types of members may be defined in an enum file. (17977). See oo/enums/EnumsCannotHaveDataMembersOrMethods.cls.does_not_compile.
      • Methods cannot be defined inside an enum statement. It causes the compile failure A method may only be defined in a class or an interface file. (12909). See oo/enums/EnumsCannotHaveDataMembersOrMethods.cls.does_not_compile.
    • DEFINE ENUM
      • Can not appear in a procedure file. It causes the compile failure Only an enum type may define enum members. (17976). See oo/enums/define_enum_must_be_inside_enum_stmt.p.does_not_compile.
      • Can not appear in a class. It causes the compile failure Only an enum type may define enum members. (17976). See oo/enums/DefineEnumMustBeInsideEnumStmt.cls.does_not_compile.
      • Can not appear in an interface. It causes the compile failure Only datasets, temp-tables, and properties may be defined in an interface. (13149). See oo/enums/DefineEnumMustBeInsideEnumStmt2.cls.does_not_compile.
      • You can indeed specify any number of DEFINE ENUM statements inside a single ENUM statement. The result appears to always be the same as if all members were defined in a single DEFINE ENUM statement. The order of the multiple statements matters in just the same way that the member definitions would work if each of the members were defined in the same order in a single DEFINE ENUM statement. See oo/enums/MultipleDefineEnum[1-4].cls and oo/enums/MultipleDefineFlagsEnum[1-11].cls.
      • Definition Behaviors
        • Number of Members
          • All enum members seem to store their value in an int64 value. For flag enums, sometimes the value also operates as a set of 64 bits. This limits the number of possible unique members since there is limited space for the value storage.
          • A single-bit enum is one where only one of the 64 bits is set and all others are cleared. This can be defined implicitly or explicitly with the correct literal.
          • A multi-bit enum is one where 2 or more of the 64 bits are set and all others are cleared. This can be defined explicitly with the correct literal or with a comma-separated list of previously defined member names (making an alias).
          • An alias is an enum whose value is defined using one or more names of previously defined enums. If more than one name is specified, it must be a comma-separated list. If one name is used, then this aliased enum will have a value that is the same as the referenced member (which means it can be a single-bit or multi-bit member). If a list of names is used, then the value is the same as if each of these members was bitwise OR'd together. Unless all of these names are the same single-bit value, this should generate a multi-bit value.
          • A duplicate of an enum is really just an alias for that same enum value but it will appear separately in lists of values and lists of names. This can be defined implicity, explicitly using an alias or explicitly using a numeric literal.
        • Regular Enums
          • The value of a regular enum is treated as a regular int64 value. As such, it can have any value that a can be stored.
          • As a consequence, regular enums can have more than 64 non-aliased members. No specific number of members has been found. See oo/enums/MoreThan64RegularEnumsOK.cls.
        • Flag Enums
          • See oo/enums/MoreThan64FlagsIsRightOut*.cls.does_not_compile, oo/enums/AliasesAllowMoreThan64Flags.cls, oo/enums/MultibitAllowMoreThan64Flags.cls and oo/flags/InitializersMustBeSigned64Bits.cls.does_not_compile.
          • Only 64 single-bit members are allowed (bits 0 through 63). These bits can be defined implicitly or explicitly with a numeric literal. Aliases do not create a unique bit flag since they refer to one or more already defined bits.
          • Trying to define any member using the 65th (or higher) bit (implicitly or explicitly) will cause a compile failure.
            • If one tries to create an implicitly calculated member (one defined without an initializer, immediately following any member which has bit 63 set), the compile error The value for enum member 'm2' is too large. (18218) will occur.
            • If one tries to explicitly define a value using a hexadecimal literal that is larger than 64-bits, the compile error Hexidecimal input greater than 16 digits is not supported. (14342) will occur. The actual value does not matter, even if that value would fit into an int64. For example, even 0x00000000000000000000 will fail.
            • If one tries to explicitly define a value using a numeric literal that is larger than a signed 64-bit value, the compile errors ** Value too large for integer. (78) and Invalid value specified for enum member '<whatever>'. (18215) are caused.
            • These last two issues are broader than having only 64 single bit values since they can occur anytime one tries to define a member with this "too large" incorrect input.
          • It is possible to have more than 64 members of a flags enum, but all of the extra members must be one of the following:
            • A multi-bit member, defined via a numeric literal or via alias (single name or list of names).
            • A duplicate of a single-bit member, defined via a numeric literal or via alias.
            • Any number of members can have the value 0 (no bits set).
        • Names
          • Enum names have two representations:
            • The symbols as defined statically in the 4GL source code.
            • The text as rendered using enum:ToString() or STRING(enum).
          • Symbol Name
            • The enum name as referenced in 4GL code is always the case-insensitive symbol defined statically for the member in the DEFINE ENUM statement.
            • This same name can be used in a character expression for certain lookup methods (e.g. enum_class:GetEnum("<enum_symbol_name")).
          • Text Rendering
            • By default, the text rendering of an enum member e (as reported by e:ToString() and STRING(e)) is the exact text of the member as written in the original 4GL source code.
            • Exceptions
              • Alias Initializers
                • If an alias is used which references a single enum name, then that referenced enum instances text representation will be used instead of the default. In DefineEnum15, the text representation of both four and ten is tWO.
                • A comma-separated alias list (which can only be used for flags enums), will cause a multibit enum to be created as a pattern but only the value will be used, the text representation will NOT be overridden. This means that the text representation will default to the symbol name. In DefineFlagsEnum18, the text representation of enum three is three even though the alias is defined as two, one.
              • Duplicates
                • If an enum definition is a duplicate of an existing enum value, the text representation is copied from the existing enum instead of using the default symbol.
                • This occurs for the explicit numeric literal case. In DefineFlagsEnum12, the text representation of nine will be EIGHT.
                • This occurs for the implicit value case (where an implicitly calculated value duplicates an existing value). In DefineFlagsEnum18, the text representation is@one@ for both eight and ten enum members.
            • Enum member names are case-preserving when rendered as a character value, so the name of the second member of oo.enums.DefineEnum15 is tWO even though it can be referenced on a case-insensitive basis (oo.enums.DefineEnum15:two).
          • There is no difference in member name processing between regular enums and flag enums, except for the behavior of comma-separated aliases above.
        • Values
          • Explicit
            • An explicit initializer can be specified after the enum member name using enum_member_name = initializer_literal syntax. More than one initializer_literal is not allowed for a single enum member.
            • Any (negative, 0 and/or positive) int64 or integer literal (which is base-10) can be used as the initializer for either regular or flag enums. See oo/enums/DefineEnum*.cls, oo/enums/DefineFlagsEnum*.cls and oo/enums/NegativeInitializersInFlags1.cls.
            • With flag enums an explicit negative initializer followed by an implicitly initialized member will cause a compile failure (in this example the implicit member name is two) The value for enum member 'two' is too large. (18218). See oo/enums/NegativeInitializersInFlags?.cls.does_not_compile. Presumably, this is an artifact of the compiler's "next power of two" algorithm, which will find the most significant bit (bit 63) already set for any negative number and thus it cannot set the non-existent bit 64.
            • Only non-negative hexadecimal (base-16) literals up to 16 hex digits in size can be used to initialize regular and flag enum values. This means that everything from 0x00 to 0x7FFFFFFFFFFFFFFF (max 64-bit signed integer, same as 9223372036854775807) are valid. See oo/enums/DefineFlagsEnumHexLiteral1.cls.
            • Any usage of a hexadecimal form which has bit 63 (most significant bit, also the sign bit) set will cause two compile errors ** Value too large for integer. (78) and Invalid value specified for enum member '<member_name>'. (18215). This means that everything from 0x8000000000000000 (min 64-bit signed integer, only the leftmost bit set, which is also -9223372036854775808) to 0xFFFFFFFFFFFFFFFF (-1) are invalid. See oo/enums/DefineFlagsEnumHexLiteral2.cls.does_not_compile. This seems to be a problem (bug?) with how the 4GL compiler converts hex literals into int64 values. I guess it is treating the value as an unsigned 64-bit integer and choking on it. The workaround is simply to use the base-10 representation of the same number.
            • Any value may specify 0 bits, 1 bit or any number of bits set.
            • Invalid Initializers
              • Decimal usage causes a compile error Invalid value specified for enum member 'something'. (18215). See oo/enums/CannotInitializeUsingDecimals.cls.does_not_compile.
              • Character usage causes a compile error ** Unable to understand after -- "def". (247). See oo/enums/CannotInitializeUsingCharacter.cls.does_not_compile.
              • Expressions (even ones with only literals) cause a compile error ** Unable to understand after -- "something = 1". (247). See oo/enums/CannotInitializeUsingExpressions.cls.does_not_compile.
              • Using a hexadecimal literal that is larger than 64-bits, causes the compile error Hexidecimal input greater than 16 digits is not supported. (14342). The actual value does not matter, even if that value would fit into an int64. For example, even 0x00000000000000000000 will fail.
              • Using a numeric literal that is larger than a signed 64-bit value, the compile errors ** Value too large for integer. (78) and Invalid value specified for enum member '<whatever>'. (18215) are caused. See oo/enums/InitializersMustBeSigned64Bits.cls.does_not_compile.
              • Using a comma-separated list of numeric literals causes the compile failure ** Unable to understand after -- "enum whatevs = 9". (247). See oo/enums/MultipleLiteralsNotAllowed.cls.does_not_compile.
            • References to other previously defined members of the same enum are a form of explicit initialization. See "Aliases" below for syntax and rules.
          • Implicit
            • An implicit initializer is any enum name which is not followed by an initializer (a literal or an alias).
            • The compiler calculates a value based on the value of the last member defined (the member defined just prior to the implicit member). It does not matter if that last enum got its value implicitly or explicitly.
            • The highest value of all prior enum members is irrelevant. Only the value of the most recently defined enum determines the implicit member's value.
            • Regular Enums
              • If the member is the first member of the enum, then the implicit value will be 1.
              • For the 2nd and later members of the enum, the implicitly calculated value will be 1 more than the last member's value.
              • This means that if the last member's value is 1 then the calculated value will be 2, last member's value 0 then the calculated value will be 1, last member's value is -5 the calculated value will be -4.
              • By setting an explicit value in the middle of otherwise implicitly calculated list has the effect of "resetting" the next implicitly calculated value.
              • It does not matter how the last member's value was set (through implicit or explicit initializers or aliases), the implicit value will be 1 more than that value.
              • Even if the resulting value is a duplicate of an existing member, it will still be used.
            • Flag Enums
              • Each implicit value set will have 1 bit set and 63 bits cleared.
              • If the member is the first member of the enum, then the implicit value will be bit 0 set (value 0x01).
              • For the 2nd and later members of the enum, the last enum member's value is inspected. The implicitly calculated bit to be set will be the next bit after the last member's highest set bit. It does not matter if there are multiple bits set, the highest bit will determine what bit will be set for the implicit calc.
              • For last value 0x00, 0x01 will bet set. For last value 0x01, 0x02 will be set. For 0x02 or 0x03, 0x04 will be set. For 0x04 through 0x07, bit 0x08 will be set. For 0x08 through 0x0f, bit 0x10 will be set. And so forth.
              • By setting an explicit value in the middle of otherwise implicitly calculated list has the effect of "resetting" the next implicitly calculated bit.
              • It does not matter how the last member's value was set (through implicit or explicit initializers or aliases), the implicit value will be next next higher bit than that value's highest set bit.
              • Even if the resulting value is a duplicate of an existing member, it will still be used.
          • Duplicates
            • It is OK to specify (implicitly or explicitly) a value which is the same as an existing enum member.
            • This can happen by referencing an existing enum using its name or by setting the same value.
            • There can be any number of duplicates.
            • When the name of a duplicate is referenced as an initializer, it just makes another enum with the same value (another duplicate).
          • Aliases
            • The name or names of previous enum members can be referenced as an initializer. The syntax is new_enum_member_name = previous_enum_member [, previous_enum_member ...].
            • For regular enums, only one previous_enum_member can be referenced. This is the equivalent of copying the named enum's value into this new member.
            • For flag enums, either one previous_enum_member or a comma-separated list of previous_enum_member names can be used. If it is a single previous_enum_member, then that enum's value is copied into this new member. If it is list of previous_enum_member, the new enum will have the values of each of the enums bitwise OR'd into its resulting value.
            • Single previous_enum_member references create duplicate enum members, a kind of alias to the referenced member.
            • Lists of previous_enum_member references create new multi-bit members that may or may not fully duplicate the other members (depending on their definitions). At a minimum, every input enum will partially overlap with the resulting enum.
            • Attempting to reference an enum name that is not yet defined causes the compile error Could not find enum member 'three'. (18217). See oo/enums/ForwardAliasReferencesFail.cls.does_not_compile.
            • Attempting to mix numeric literals and aliases in the same initializer, causes the compile failure ** Unable to understand after -- "three = one,". (247). See oo/enums/MixingAliasesAndLiterals.cls.does_not_compile.
            • Multiple comma-separated aliases cannot be used in regular enums. Doing so creates a compile failure: Can only specify multiple values when the enum has the FLAGS option. (18216). See oo/enums/MultipleAliasesOnlyWorksInFlagsEnums.cls.does_not_compile.
          • The values of members do not need to be in ascending or descending order. They can be defined in any order. Only a sequence of uninterrupted implicit definitions will inherently be in ascending order.* Life Cycle
          • It is OK to intermix explicit and implicit definitions in any way (all explicit, all implicit, explicit at beginning and/or middle and/or end with implicit in between... whatever). The key is that the implicit values will always follow the rules above, which are completely determined by the value of the most recently defined member.
    • Instantiation
      • Each named member of an enum class is the equivalent of static final. They exist without any explicit instantiation on the part of the 4Gl programmer. The can be referenced at any time without any initialization needed.
      • The NEW operator cannot be used with an enum. For example, new oo.enums.Ordinals() causes the compile failure Cannot use NEW statement with 'oo.enums.Ordinals' because it is an enum. (17970). See oo/enums/new_operator_cannot_be_used_with_enum.p.does_not_compile.
      • The DYNAMIC-NEW operator cannot be used with an enum. For example, dynamic-new ch-var() (where ch-var is the string "oo.enums.Ordinals") causes the runtime error (error-status:error is true) Cannot NEW class oo.enums.Ordinals because it is an interface or enum. (17971). See oo/enums/dynamic_new_cannot_be_used_with_enum.p.
      • The Progress.Lang.Class:NEW() method cannot be used with an enum. For example, assuming cls = GET-CLASS(oo.enums.Ordinals). then cls:new() causes the runtime error (error-status:error is true) Cannot NEW class oo.enums.Ordinals because it is an interface or enum. (17971). See oo/enums/class_new_method_cannot_be_used_with_enum.p.
      • The only known way to "create" a new instance of an enum is with the return value from bitwise operators and/or the bitwise helper methods. This only works for flags enums. See "Bitwise Operations" below.
    • Enum instances do not appear in the SESSION:FIRST-OBJECT/SESSION:LAST-OBJECT chain. See oo/enums/enums_are_not_in_the_session_object_chain.p.
    • Calling NEXT-SIBLING() or PREV-SIBLING() on an enum instance returns unknown value. See oo/enums/enums_are_not_in_the_session_object_chain.p.
    • DELETE OBJECT silently returns with no change when executed on an enum instance. The enum instance remains available. It doesn't matter if the reference passed is a static reference oo.enums.Ordinals:first or if it is a variable of type enum. The statement appears to be a NOP. See oo/enums/delete_object_silently_ignores_enums.p.
    • VALID-OBJECT(enum_instance) returns true, before and after DELETE OBJECT. See oo/enums/delete_object_silently_ignores_enums.p.
    • Use of Clone() on a regular enum or flags enum will cause the runtime warning Clone method is undefined for the class. (13444). See oo/enums/clone_method_undefined_for_enums.p.
  • Casting
    • CAST() works as one would expect for enums (same as classes).
      • If the type of the parm parameter to CAST(parm, typename) is incompatible (not assignable) with typename, then a compile failure ** Incompatible data types in expression or assignment. (223) will be generated. See oo/enums/cast_treats_enums_as_expected.p.does_not_compile.
      • If the return type (typename) of CAST(parm, typename) is incompatible with the assignment or containing sub-expression type, then a compile failure ** Incompatible data types in expression or assignment. (223) will be generated. See oo/enums/cast_treats_enums_as_expected.p.does_not_compile.
      • If all types match, then CAST() works as expected. See oo/enums/cast_and_dynamic_cast_with_enum_and_flags_enum.p.
    • DYNAMIC-CAST()
      • If the type of the parm parameter to DYNAMIC-CAST(parm, typeexpr) is incompatible with typeexpr, then a runtime warning (error-status:error is false but an error message/number is recorded in silent error mode or an alter-box is shown if not in silent error mode) occurs Invalid cast from oo.enums.Ordinals to oo.enums.Permissions. (12869). See oo/enums/cast_and_dynamic_cast_with_enum_and_flags_enum.p.
      • If the return type (typeexpr) of DYNAMIC-CAST(parm, typename) is a valid type but is incompatible with the assignment or containing sub-expression type, then a runtime error A variable of class 'oo.enums.Ordinals' cannot be assigned to a variable of class 'oo.enums.Permissions'. (13448) will be generated. See oo/enums/cast_and_dynamic_cast_with_enum_and_flags_enum.p.
      • If the return type (typeexpr) of DYNAMIC-CAST(parm, typename) is NOT a valid type, then a runtime warning (error-status:error is false but an error message/number is recorded in silent error mode or an alter-box is shown if not in silent error mode) CAST to 'oo.enums.InvalidName' not allowed. CAST target must be a user-defined type. (12900). See oo/enums/cast_and_dynamic_cast_with_enum_and_flags_enum.p.
      • If all types match, then DYNAMIC-CAST() works as expected. See oo/enums/cast_and_dynamic_cast_with_enum_and_flags_enum.p.
  • Enum Instance Lookup
    • There are 6 ways to lookup an arbitrary enum instance
      • By int64 enum value:
        • <class_reference>:GetEnum(int64)
        • Progress.Lang.Enum:ToObject(<fully_qualified_class_name_char_expr>, int64)
        • DYNAMIC-ENUM(<fully_qualified_class_name_char_expr>, int64)
      • By character enum name:
        • <class_reference>:GetEnum(character)
        • Progress.Lang.Enum:ToObject(<fully_qualified_class_name_char_expr>, character)
        • DYNAMIC-ENUM(<fully_qualified_class_name_char_expr>, character)
    • For all cases below, passing unknown value as one of the parameters causes a runtime error as follows:
      • oo.enums.Ordinals:GetEnum(int64) results in Invalid value specified for oo.enums.Ordinals:GetEnum (15246)
      • oo.enums.Ordinals:GetEnum(character) results in Invalid value specified for oo.enums.Ordinals:GetEnum (15246)
      • Progress.Lang.Enum:ToObject(valid_class, ?) results in Invalid value specified for parameter 'value' of method "ToObject". (18193)
      • Progress.Lang.Enum:ToObject(?, int64) results in Invalid value specified for parameter 'enumTypeName' of method "ToObject". (18193)
      • Progress.Lang.Enum:ToObject(?, character) results in Invalid value specified for parameter 'enumTypeName' of method "ToObject". (18193)
      • DYNAMIC-ENUM(valid_class, ?) results in Could not evaluate 'member-name-or-value' parameter for DYNAMIC-ENUM. (18007)
      • DYNAMIC-ENUM(?, int64) results in Could not evaluate 'enum-type-name' parameter for DYNAMIC-ENUM. (18007)
      • DYNAMIC-ENUM(?, character) results in Could not evaluate 'enum-type-name' parameter for DYNAMIC-ENUM. (18007)
    • Both GetEnum() static methods are implicitly added by the compiler. They returns the same type as the class used for the lookup.
    • Given the same enum class and the same character or int64 parameter, the behavior of both ToObject and DYNAMIC-ENUM() is identical to GetEnum() with these exceptions:
      • Unknown value generates a different error (see above).
      • There is an extra step to lookup the referenced class. The first parameter of ToObject/DYNAMIC-ENUM() is a character expression which references a valid fully qualified class name that can be found via the PROPATH. If that class cannot be found (because it is not properly qualified or because it just does not exist), then a runtime error Failed to find enum type 'whatever'. (18109) will be raised (in this case, we passed "whatever" as the first parameter).
    • Regular Enum Results
      • GetEnum(int64) static method, Progress.Lang.Enum:ToObject(class_char_expr, int64), DYNAMIC-ENUM(class_char_expr, int64)
        • See oo/enums/getenum_int_value_method.p and oo/enums/dynamic_enum_int_value.p.
        • Passing an exact match to an existing enum value will return that associated enum.
        • Passing any other non-unknown integer causes a runtime error (for example using -1 on an enum where -1 is not a valid value) Could not get enum of type 'oo.enums.Ordinals' for value -1. (18207).
      • GetEnum(character) static method, Progress.Lang.Enum:ToObject(class_char_expr, character), DYNAMIC-ENUM(class_char_expr, character)
        • See oo/enums/getenum_string_value_method.p and oo/enums/dynamic_enum_string_value.p.
        • Passing an exact match to an existing enum name will return that associated enum. No abbreviations of enum names are possible.
        • Optionally one can pass leading and/or trailing whitespace ("second", " second", " second ", "second " are all equivalent). Any leading/trailing whitepace is ignored for purposes of matching.
        • Passing any other non-unknown and non-comma-separated character value (including empty string) causes a runtime error (for example using "one" on an enum where "one" is not a valid name) Could not find 'one' in enum 'oo.enums.Ordinals'. (18008).
        • Passing a comma-separated character value causes a runtime error (for example using "first,third") Comma separated list of names 'first,third' not valid for non-flag enum 'oo.enums.Ordinals'. (18017).
    • Flag Enum Results
      • Flag enums act as bitfields some of the time and as int64 numbers at other times. The behavior makes sense the internal representation of the flags is in a signed 64-bit integer value encoded using two's complement. This is also consistent with the int64 implementation in the 4GL. As a result, one must be prepared for strangeness when dealing with negative numbers and flag enums. Please remember that in this form of representation, the most significant bit (bit 63) is the sign bit. If bit 63 is set, then the number is negative. The other thing is that small negative numbers tend to set all the bits on their left (e.g. -1 is 0xFFFFFFFFFFFFFFFFF and -2 is 0xFFFFFFFFFFFFFFFE). This means that using the 64th flag can cause weirdness and bit flag matching can be very strange.
      • GetEnum(int64) static method, Progress.Lang.Enum:ToObject(class_char_expr, int64), DYNAMIC-ENUM(class_char_expr, int64)
        • See oo/enums/getenum_int_value_method_for_flag_enum.p, oo/enums/dynamic_enum_int_value_for_flag_enum.p, oo/enums/flag_enums_with_positive_values_lookup_*.p and oo/enums/flag_enums_with_negative_values_lookup_*.p.
        • Passing an exact match to an existing int64 enum value will return that associated enum.
        • If there is no exact int64 match, then "bitflag matches" can still occur. First, the "maximal bitfield" will be calculated. This is the same as taking each pre-defined enum and OR'ing it with all the others. In this mode, if all the set bits (binary 1) in the given int64 input value are also set in the maximal bitfield, then a new enum instance is created with that exact int64 input value will be created.
        • Bitflag matches can have real names. The name associated with the instance will be a comma separated list of all flags which are fully present in the enum OR "" if no single flag is fully present. As occurs with STRING() (see below), if the only flags present are the exact match to an aliased compound set of flags (see oo.enums.Permissions and the read which is 0x04, write which is 0x08 and readwrite which is read OR write or 0x0C) then the name will have the compound alias instead of the individual flags (e.g. passing 12 or 0x0C will return an enum with the name "readwrite" instead of "read,write").
        • Non-exact bitflag matching is complicated by some quirks in regard to negative numbers:
          • If no enum flag is explicitly or implicitly defined as a negative number (bit 63 is not set), then all negative numbers will fail to match. This makes sense since all negative numbers will have the sign bit set and no such bitfield match can occur when bit 63 is not set. Besides setting that bit explicitly (by using a negative number for a flag), it is also possible to implicitly set that bit by defining 64 separate (non-aliased) flags and letting the compiler calculate the next highest power of 2. If a flag has bit 62 set and the next flag has an implicit value, then the next power of two will set bit 63. See oo.enums.DefineFlagsEnum33 for an example.
          • If at least one enum flag is defined as a negative number between -1 and -32768 (inclusive), a special lookup mode is enabled. The two's complement notation means that for numbers greater than or equal to -32768, at least the most significant 49 bits will all be set. -1 is 'extra special' because its encoding is 0xFFFFFFFFFFFFFFFF which means that all bits are set. In this special matching mode any input value (positive or negative) which is a subset of the set bits will be returned as a valid enum with the same value as input (see above for the name rules). It is as if the input value is being treated as just a bitfield instead of a signed int64, so the sign extension bits are considered active for bitfield matching. For example, if there is a valid flag of -10, the input -9223372036854775808 (which is 0x8000000000000000 and has binary 1000 in most significant nibble, i.e. only bit 63/the sign bit is set) will yield a valid enum even though the only valid negative enum is a number that is >= -32768.
          • This special lookup mode is also activated if there is any negative number AND the maximal bitfield in in this range of -1 and -32768 (inclusive). This can be seen in oo/enums/flag_enums_with_negative_values_lookup_13.p where it uses DefineFlagsEnum33.cls in which all 64 flag bits are implicitly created. It is only the last a64 enum def that is a "negative number" but it is -9223372036854775808 which normally would not trigger the quirk. However, in this case the maximal bitfield is -1 and that triggers the special lookup mode.
          • If the only negative numbers as flags are values less than -32768 OR the maximal bitfield is not in that range, then this special lookup mode is not used. For example, if there is no valid flag of >= -32768 but there is a flag -4611686018427387904 (0xC000000000000000 or 1100 in most significant nibble, which is both bit 63 and bit 62 set), then the input -9223372036854775808 (which is 0x8000000000000000 and has binary 1000 in most significant nibble, i.e. only bit 63/the sign bit is set) will NOT yield a valid enum even though the bit 63 is actually set in at least one of the enums. In other words, this is an exception to the bitfield matching depending on the state of the negative numbers usage.
        • Passing any int64 value in which at least one set bit is not matched by any possible flag in the enum causes a runtime error (in this example we are trying to obtain the value 128 in oo.enums.Permissions) Could not get enum of type 'oo.enums.Permissions' for value 128. (18207).
      • GetEnum(character) static method, Progress.Lang.Enum:ToObject(class_char_expr, character), DYNAMIC-ENUM(class_char_expr, character)
        • See oo/enums/getenum_string_value_method_for_flag_enum.p, oo/enums/dynamic_enum_string_value_for_flag_enum.p, oo/enums/flag_enums_with_positive_values_lookup_*.p and oo/enums/flag_enums_with_negative_values_lookup_*.p.
        • There is no bitfield matching with string/character lookups.
        • Lookup of flag enums from a string can have a single flag name or a comma separated list of multiple flag names.
        • Each name must be an exact match to an existing enum name. For each flag name in the list, that associated enum will be OR'd into the enum that is returned.
        • Names may not be abbreviated.
        • Optionally each name in the input list can have leading and/or trailing whitespace ("read", " read", " read ", "read " are all equivalent). Any leading/trailing whitepace is ignored for purposes of matching.
        • If the list includes any other non-unknown character value (including empty string) causes a runtime error (for example using "rea" on an enum where "one" is not a valid name) Could not find 'one' in enum 'oo.enums.Ordinals'. (18008). This means that for oo.enums.Permissions, "rea", "", "reade", "read,wri" and "read,,write" are all invalid inputs that generate the runtime error 18808.
        • The enum will report a name that is a comma separated list of the enum names, in the order of definition. It is rationalized from the OR'd enum instead of being defined from the input string.
  • Reflection-Based Enum Inspection
    • See oo/enums/reflection_getenum_methods_for_enum.p, oo/enums/reflection_getenum_methods_for_flag_enum.p, oo/enums/reflection_getenum_methods_for_enum_values_and_names_ordering.p.
    • All of the following are static methods which can only be called on the Progress.Lang.Class instance associated with a given enum or flag enum instance. For example, an instance obtained using GET-CLASS(oo.enums.Ordinals).
    • You cannot call these methods directly on a Progress.Lang.Enum or Progress.Lang.FlagsEnum. The methods can only be called on a sub-class of Progress.Lang.Enum or Progress.Lang.FlagsEnum. Calling it on ANY other class or interface, including Progress.Lang.Class (non-enum class) itself or on Progress.Lang.Error (non-enum interface) will generate the same runtime error Class 'Progress.Lang.Class' is not an enum type. (18110). See oo/enums/reflection_getenum_methods_only_work_on_enum_types.p.
    • GetEnumValues()
      • This returns the comma separated list of all enum definition values.
      • Each value is a base-10 integer no matter how it was defined (e.g. 0x0C will be reported as 12).
      • The definition order of the enum is ignored.
      • For regular enums, the values are sorted smallest to largest. This means negative values (if present) are on the left, followed by 0 and positive values in ascending order. For oo.enum.DefineEnum13, the result will be "-1000,-5,3,9,11,49,54,700,702,754" even though the definition order is quite different.
      • For flag enums, the values are sorted 0 to the largest positive number and then if there are any negative values from smallest to largest negative number. This means negative values (if present) are on the left, followed by 0 and positive values in ascending order. For oo.enums.DefineFlagsEnum21, the result will be "0,0,1,-5,-1" even though the definition order is quite different.
      • Duplicate values (enum instances with different names but the same values) will cause those values to appear as many times as they were defined. For oo.enums.DefineEnum15, the result will be "0,1,2,2,3,3,4,4,4,4".
    • GetEnumNames()
      • This returns the comma separated list of all enum definition names.
      • Although the text rendering of the enum "name" is sometimes copied from a different enum instance (see the Names section of DEFINE ENUM above), the returned list of this method always uses the 4GL source code symbol name. This may be different from what is seen as the text representation of the enum.
      • The definition order of the enum is ignored.
      • Given the list of values as returned by GetEnumValues(), the corresponding names will be substituted in for each value. This means that the each enum's corresponding name and value will have the same index postion in the returned lists for GetEnumValues() and GetEnumNames(). These lists can be used to map bidirectionally.
      • For regular enum oo.enum.DefineEnum13, the result will be "five,nine,four,ten,three,two,one,seven,eight,six" even though the definition order is quite different.
      • For flag enum oo.enums.DefineFlagsEnum21, the result will be zero,nil,one,neg-five,neg-one even though the definition order is quite different. Please note that the nil enum instance will render its name as zero (e.g. using ToString()) but the correct symbol name will appear inside the GetEnumNames() output.
      • For duplicate values (zero and nil have the same 0 value), the sorting appears to match the order of definition in the DEFINE ENUM.
    • GetEnumValue(character)
      • Regular Enums
        • Passing an exact match to an existing enum name will return the int64 value of that associated enum. No abbreviations of enum names are possible.
        • One CANNOT pass leading and/or trailing whitespace (" second", " second ", "second " are NOT equivalent to "second"). This is different from how the GetEnum methods work.
        • Passing any non-unknown, non-exact match character value (including empty string or a comma-separated value) causes unknown value to be returned. This means that for oo.enums.Ordinals, "fir", "first ", "" and "first,third" all return unknown value.
      • Flag Enums
        • Lookup of flag enums from a string can have a single flag name or a comma separated list of multiple flag names.
        • Each name must be exact match to an existing enum name. For each flag name in the list, that associated enum will be OR'd into the enum whose value is then returned.
        • Names may not be abbreviated.
        • Optionally each name in the input list can have leading and/or trailing whitespace ("read", " read", " read ", "read " are all equivalent). Any leading/trailing whitepace is ignored for purposes of matching.
        • If the list includes any other character value (including empty string) then unknown value is returned. This means that for oo.enums.Permissions, "rea", "", "reade", "read,wri" and "read,,write" are all invalid inputs that return unknown.
      • If unknown value is passed as an argument, this runtime error is generated: Invalid value specified for Progress.Lang.Class:GetEnumValue (15246)..
    • GetEnumName(int64)
      • Regular Enums
        • Passing an exact match to an existing enum value will return the name for that associated enum.
        • Passing any other non-unknown integer returns unknown value.
        • For oo.enums.Ordinals, passing 1 will return "first" and passing 0 will return unknown value.
        • Duplicate values (enum instances with different names but the same values) will return the name of the first enum (in the order of definition in DEFINE ENUM) that has that value.
      • Flag Enums
        • Passing an exact match to an existing int64 enum value will return the name for that associated enum. For oo.enums.Permissions, the value 4 will return a name of "read". Since it is an exact match, only that enum name is returned even though the 0x04 bit is also set in other enum definitions. In this case, the result will NOT be a comma-separated list.
        • Compound (i.e. an enum member that is aliased to multiple other flags) members are treated as a unique member that can be matched exactly using their "combined" int64 value. For oo.enums.Permissions, the value 12 will report a name of "readwrite" instead of "read,write" (which each are present as part of the compound flag).
        • If there is no exact int64 match, then a "bitflag matches" can still occur, generating a possible comma-separated list of results. The name is created by iterating through each defined enum member (in the order they appear in DEFINE ENUM). For each member, this algorithm is used (<input_int64_value> BITWISE_AND <member_under_test>) EQ <member_under_test> (it checks if all bits in the member under test are set in the input int64). For each case where this test is true, that member's name will be added to the comma-separated output string.
        • Compound members are not matched in this bitflag matching phase. For oo.enums.Permissions, passing 13 or 0x0D will return the name "create,read,write" instead of "create,readwrite".
        • The two's complement encoding of int64 means that negative numbers will always have bit 63 set and will often have many bits set. For example, if -1 is the value, then the encoding is 0xFFFFFFFFFFFFFFFF which means that all non-compound (and non-zero) enum members will match. For oo.enums.Permissions, an input value of -1 will yield a name of "create,delete,read,write,exec". Note how denied (0x00) and the compound readwrite (0x0C@) are not present.
        • For exact matches and for bitflag matches, duplicate values (enum instances with different names but the same values) will return a single name of first defined enum (in the order of definition in DEFINE ENUM) that has that value.
        • Any input value for which there is no enum which can have all its corresponding bits set, will cause unknown value to be returned. For oo.enums.Permissions, an input value of 128 will not allow any member to be matched so unknown will be returned. For oo.enums.DefineFlagsEnum21, passing either 128 (0x80) or -9223372036854775808 (MIN 64-BIT SIGNED INT, 0x8000000000000000) will cause no enum members (0, 0, 1, -5. -1) to have all their bits set in the input value so the result is unknown.
      • If unknown value is passed as an argument, this runtime error is generated: Invalid value specified for Progress.Lang.Class:GetEnumValue (15246)..
  • ASSIGNMENT
    • Only the same exact enum or flag enum type (e.g. oo.enums.Ordinals) can be assigned to a variable of type oo.enums.Ordinals.
    • Although enum members all have an int64 value, no numeric types can be assigned to an enum. Enums are truly treated as specific data types like an OO class instance.
    • A CAST() or DYNAMIC-CAST() can be used to assign an expression of the proper superclass which is actually an instance of the same exact specific class. If my-prog-lang-enum is a var of type Progress.Lang.Enum which actually holds an instance of oo.enums.Ordinals, the my-ordinal = CAST(my-prog-lang-enum, oo.enums.Ordinals). will work correctly.
    • Any mismatch in types will cause a compile failure ** Incompatible data types in expression or assignment. (223). See oo/enums/assign_enum_requires_exact_type_match.p.does_not_compile.
    • The members of an enum cannot be assigned (they are treated as implicitly final members). Trying to assign to oo.enums.Ordinals:first causes a compile error Cannot update first because it is read-only. (17979). See oo/enums/enum_members_cannot_be_assigned.p.does_not_compile.
  • CASE Statement
    • See oo/enums/enum_case_stmt.p, oo/enums/flag_enum_case_stmt.p and oo/enums/getenum_int_value_method_for_flag_enum.p.
    • The statement starts with CASE <enum_expression>:. <enum_expression> must be an expression of regular enum or flag enum type.
    • Each WHEN <enum_member_expression> THEN clause is a possible match to a specific enum member. <enum_member_expression> can be a hardcoded enum member reference like oo.enums.Ordinals:first but it can also be any valid expression that returns an enum of the same exact type. For example, a user-defined function call first-ordinal() which returns a oo.enums.Ordinals instance. This is especially necessary for flag enums, where one will often want to use bitwise operations to specify an exact list of flags that are to be matched, like oo.enums.Permissions:create OR oo.enums.Permissions:read OR oo.enums.Permissions:exec or oo.enums.Permissions:create OR oo.enums.Permissions:random XOR (oo.enums.Permissions:delete OR oo.enums.Permissions:read).
    • For both kinds of enums, the first WHEN clause detected as a match will have its THEN block executed.
    • It is OK to have duplicate conditions in subsequent WHEN clauses, but these cannot be matched.
    • If no match is found the OTHERWISE block will execute (if it is present).
    • For both kinds of enums, the matching is based on the member's int64 value. Only an exact match will select the corresponding WHEN clause. With flag enums, this means that only the same exact bit pattern can be matched. There is no subset matching going on.
    • It is valid to use the OR WHEN <enum_member_expression> construct with both types of enums. Please note that the meaning of when oo.enums.Permissions:write or when oo.enums.Permissions:exec then is very different than when oo.enums.Permissions:write or oo.enums.Permissions:exec then. The first matches write or exec individually but will NOT match if both bits are set. The second ONLY matches if both bits are set.
    • The following WHEN clauses all generate a compile failure Incompatible datatypes in CASE and a WHEN branch expression. (3536). See oo/enums/mismatching_when_types_in_enum_case_stmt.p.does_not_compile and oo/enums/mismatching_when_types_in_flags_enum_case_stmt.p.does_not_compile.
      • Referencing a different enum type (the CASE expression is of type oo.enums.Ordinals but there is a WHEN oo.enums.Permissions:create THEN.
      • Hexadecimal literals.
      • integer or int64 literals.
      • character literals.
  • Bitwise Operations
    • See oo/enums/bitwise_operators_cannot_have_enums_and_number_operands.p.does_not_compile, oo/enums/bitwise_operators_cannot_have_flags_enums_and_number_operands.p.does_not_compile, oo/enums/bitwise_operations_unknown_value.p, oo/enums/test_flag_enum_bitwise_ops.p and oo/enums/test_flag_enum_bitwise_ops_including_non_positive_numbers.p and oo/enums/test_flag_enum_bitwise_ops_non_intersecting_bitsets.p.
    • Bitwise operations only operate on flag enums. They cannot be used with numeric data and they cannot be used with regular enums.
    • There are 3 binary operators (OR, XOR, AND), 1 unary operator (NOT), 3 methods which are implicitly added to each subclass of Progress.Lang.FlagsEnum (SetFlag(), ToggleFlag(), UnsetFlag()) and 1 method IsFlagSet() that is implemented directly in Progress.Lang.FlagsEnum.
    • In the 4GL, the 3 implicitly added methods cannot be placed in Progress.Lang.FlagsEnum because they both receive as a parameter and have a return type which is the specific type of enum that is the child of Progress.Lang.FlagsEnum. This means that if you call one of these methods on flags enum A and pass it an instance of flags enum B, then this compile error will occur Parameter 1 for METHOD <methodname> is not type compatible with its definition. (12905). This 12905 will also occur if the parameter is of type Progress.Lang.FlagsEnum even if it really is the correct subclass type. Trying to assign the return value from one of these methods to flags enum A when the method is called on flags enum B, will cause the compile error ** Incompatible data types in expression or assignment. (223). See oo/enums/implicit_flags_enum_methods_are_type_specific.p.does_not_compile.
    • Except for the IsFlagSet(), all the operators and the other methods return a new instance of the same enum type of the operand(s). The compiler enforced strict type matching in all of these cases, so this result is consistent. If the result of a bitwise operation is an exact match to the value of an existing (predefined) enum instance, then that instance will be returned. If the result is NOT an exact match then a new enum will be created which is a combination of existing enums or which is a completely unique value that is not directly represented by any existing enum. This means that it must be possible to create new instances at runtime with unique calculated values.
    • Unknown Value
      • If any operand or both of the operands to the binary bitwise operators is unknown value, this runtime error is raised Cannot perform bitwise operation with the unknown value. (18210). This holds true for the unary NOT bitwise operator.
      • If the parameter to SetFlag, ToggleFlag or UnsetFlag is unknown value, this runtime error is raised Invalid value specified for <enum_class>:<method> (15246).
      • If the parameter to IsFlagSet is unknown value, this runtime error is raised Could not check flags because the specified enum instance is invalid. (18205).
      • If the referent for SetFlag, ToggleFlag, UnsetFlag or IsFlagSet is unknown value, this runtime error is raised Invalid handle. Not initialized or points to a deleted object. (3135).
    • Between all these operators and methods, there are 6 types of bitwise operations:
      • OR operator/SetFlag() implicit method
        • Given the same inputs, both op1 OR op2 and op1:SetFlag(op2) will generate the same output.
        • The result is a true bitwise OR of the operands. All bits that are set in each operand will also be set in the output enum, regardless of whether it is set in the other operand.
        • There are no ordering considerations of the operands. Passing the same operands in switched order will yield the same result.
        • There is no special behavior for negative values, other than the fact that the two's complement notation means that the sign bit will be set AND for negative numbers near zero, it is likely that most of the bits are set (e.g. -1 is 0xFFFFFFFFFFFFFFFF). For oo.enums.DefineFlagsEnum20
        • With unique single-bit enums as input, the result will be a multi-bit enum that is a superset of the two input enums. For oo.enums.Permissions, create OR delete (0x01 and 0x02 values respectively) will yield an enum with a value of 0x03 and a name of "create,delete".
        • If either operand has a value of 0, the result will be equivalent to the other operand.
      • XOR operator/ToggleFlag() implicit method
        • Given the same inputs, both op1 XOR op2 and op1:ToggleFlag(op2) will generate the same output.
        • The result is a true bitwise XOR of the operands. All bits that are set differently in each operand will be set in the output enum.
        • There are no ordering considerations of the operands. Passing the same operands in switched order will yield the same result.
        • There is no special behavior for negative values, other than the fact that the two's complement notation means that the sign bit will be set AND for negative numbers near zero, it is likely that most of the bits are set (e.g. -1 is 0xFFFFFFFFFFFFFFFF).
        • With unique single-bit enums as input, the result will be a multi-bit enum that is a superset of the two input enums. For oo.enums.Permissions, create XOR delete (0x01 and 0x02 values respectively) will yield an enum with a value of 0x03 and a name of "create,delete".
        • If the bit sets are disjoint (no set bits in common), the result will be a multi-bit enum that is a superset of the two input enums. For oo.enums.Permissions, create XOR readwrite (0x01 and 0x0A values respectively) will yield an enum with a value of 0x0B and a name of "create,read,write".
        • Any overlap in set bits will be set to 0 in the result. For oo.enums.Permissions, read XOR readwrite (0x04 and 0x0A values respectively) will yield an enum with a value of 0x08 and a name of "write".
        • Repeatedly XOR'ing the same 2nd operand into a given enum result will have the effect of toggling the 2nd operand's bits on and off.
        • If either operand has a value of 0, the result will be equivalent to the other operand.
        • XOR'ing two enums that have the same value will result in an enum with the value 0.
      • AND operator
        • The result is a true bitwise AND of the operands. All bits that are set the same in both operands will be set in the output enum.
        • There are no ordering considerations of the operands. Passing the same operands in switched order will yield the same result.
        • There is no special behavior for negative values, other than the fact that the two's complement notation means that the sign bit will be set AND for negative numbers near zero, it is likely that most of the bits are set (e.g. -1 is 0xFFFFFFFFFFFFFFFF).
        • With unique single-bit enums as input, the result will be an enum with value 0. For oo.enums.Permissions, create AND delete (0x01 and 0x02 values respectively) will yield an enum with a value of 0x00 and a name of "denied".
        • If the bit sets are disjoint (no set bits in common), the result will be an enum with value 0. For oo.enums.Permissions, create AND readwrite (0x01 and 0x0A values respectively) will yield an enum with a value of 0x00 and a name of "denied".
        • Any overlap in set bits will be set to 1 in the result. For oo.enums.Permissions, read AND readwrite (0x04 and 0x0A values respectively) will yield an enum with a value of 0x04 and a name of "read".
        • If one enum is a subset of the other, the resulting enum will be the same as the subset. For oo.enums.DefineFlagsEnum20, neg-five AND neg-one (0xFFFFFFFFFFFFFFFB and 0xFFFFFFFFFFFFFFFF) will yield an enum with value -5 and name neg-five.
        • If either operand has a value of 0, the result will be an enum of value 0.
        • AND'ing two enums that have the same value will result in an enum with that same value.
      • NOT operator
        • Traditional bitwise NOT is an inversion of all bit state. For example, the in C or Java the inverse of 0x0000000000000000 is 0xFFFFFFFFFFFFFFFF and the inverse of 0x0000000000000006 (0110 in least signficant nibble) is 0xFFFFFFFFFFFFFFF9 (1001 in least signficant nibble). In the 4GL the result is not a true bitwise NOT. The result depends upon the maximal set of bits which can possibly be set. This "maximal bitfield" can be calculated by taking each defined enum and OR'ing it with all the others. This becomes a "mask" that limits the inversion of bits to only those in this set. All others will be cleared.
        • If the maximal bitfield contains all 64 bits, then the bitwise NOT acts the same as the traditional inversion. For example, in oo.enums.DefineFlagsEnum34 the NOT zero yields the neg-one enum (value -1 or 0xFFFFFFFFFFFFFFFF) and NOT four yields neg-five enum (value -5 or 0xFFFFFFFFFFFFFFFB which has 1011 in the least significant nibble).
        • This behavior changes when maximal bitfield contains a subset of all 64 bits. For example, in oo.enums.Permissions the maximal bitfield is 0x000000000000003F (00111111 in least significant byte). In this case, NOT denied (denied has value 0) yields an enum with value 0x3F (62 in base-10) and name "create,delete,read,write,exec" (note that the bit at 0x20 comes from the random member and has no single bit name on its own so it does not appear in the name). Similarly, NOT exec yields an enum with value 0x2F (00101111 in least significant byte) and name "create,delete,read,write".
        • This means that the algorithm for 4GL bitwise NOT is ((INVERT enum_member) AND maximal_bitfield) where INVERT is traditional bitwise NOT (full inversion of bit state), AND is traditional "bitwise AND" and maximal_bitfield is the previously described mask for this operation.
      • UnsetFlag() implicit method
        • Where op1 and op2 are both members of the same flag enum, the use of op1:UnsetFlag(op2) clears all set bits in op2 which are also set in op1 and returns the resulting enum.
        • This is consistent with the expression op1 AND (NOT op2). The 4GL version of bitwise NOT yields the same result as the traditional bitwise NOT in this use case.
        • The order of operands is important here. Switching the order is not sure to yield the same results.
        • Unsetting the same enum from itself will yield an enum with no bits set. For oo.enums.Permissions, create:UnsetFlag(create) results in an enum which is equivalent to denied (value 0 and name "denied").
        • Unsetting a single bit unique enum from a different single bit unique enum will return the op1 enum (no bits changed). For oo.enums.Permissions, create:UnsetFlag(delete) results in an enum which is equivalent to create.
        • If op1 and op2 are disjoint sets of bits, then the result will always be equivalent to op1 since there is not subset of common bits to clear.
        • Only if there is an overlap of set bits, will a change be made. For oo.enums.Permissions, readwrite:UnsetFlag(write) results in an enum which is equivalent to read.
        • Arbitrary subsets of the maximal bitfield are possible. For oo.enums.Permissions, random:UnsetFlag(readwrite) (random has value 0x26 which is "delete,read" and an unspecified/unnamed 0x20 bit; readwrite is 0x0C or 0x04 + 0x08) yields an enum with value 0x22 and name "delete".
        • No special behavior occurs for negative values, except it is more likely to yield unusual results. For oo.enums.DefineFlagsEnum20, neg-one:UnsetFlag(one) will result in an enum with value -2 and name "". This makes sense since 0xFFFFFFFFFFFFFFFF AND NOT 0x0000000000000001 should yield 0xFFFFFFFFFFFFFFFE.
      • IsFlagSet() method
        • The code related to checking this method's behavior can be seen in the bitwise-enum function of common/enum_helpers.i.
        • op1:IsFlagSet(op2) returns true only if all set bits of op2 are also set in op1. If any one of the bits of op2 are not set in op1, then the result will be false.
        • It doesn't matter if op1 EQ op2 or if op1 is a superset of op2. Both cases will cause a true result.
        • The result is consistent with the algorithm (op1 AND op2) EQ op2.
        • If the op2 parameter to op1:IsFlagSet(op2) has a value of 0, then the method invocation will generate the runtime error Cannot check flags if the value for the specified enum instance is zero. (18113).
    • All of the following cannot be used as an operand for any bitwise operator. Use will generate a compile failure ** Incompatible data types in expression or assignment. (223).
      • integer, int64 or decimal values (as lvalues, unknown value, complex sub-expressions or any form of literal)
      • Subclasses of Progress.Lang.Enum as one operand of a binary operator.
      • Two different subclasses of Progress.Lang.FlagEnum operands of a binary operator.
      • Attempting to assign different subclasses of Progress.Lang.FlagEnum to one another.
    • If subclasses of Progress.Lang.Enum are the operand to not or are both operands for a binary operator, then this pair of compile failures is generated: 'oo.enums.Ordinals' is not a flags enum type. (18211) and ** Incompatible data types in expression or assignment. (223).
  • Comparison Operations
    • See oo/enums/comparison_operators_cannot_compare_enums_against_numbers.p.does_not_compile, oo/enums/comparison_operators_cannot_compare_flags_enums_against_numbers.p.does_not_compile, oo/enums/test_enum_comparisons.p, oo/enums/test_enum_comparisons_including_non_positive_numbers.p, oo/enums/test_flag_enum_comparisons.p, oo/enums/test_flag_enum_comparisons_including_non_positive_numbers.p.
    • In the following truth table, the op1 and op2 are the enum values as represented by the int64(enum) function. These values may optionally be set to unknown value (?).
    • When op1 is unknown, the CompareTo() and Equals() methods for enums will cause the runtime error Invalid handle. Not initialized or points to a deleted object. (3135). These are the only known deviations from the results documented in the truth table below.
    • The following all cause this compile failure ** Incompatible data types in expression or assignment. (223).
      • Mixing different types of enums. Two regular enums of different types (oo.enums.Ordinals vs oo.enums.DefineEnum2) cannot be compared against each other. Two flag enums of different types (oo.enums.Permissions vs oo.enums.DefineFlagsEnum20) cannot be compared against each other. Regular enums cannot be compared to flag enums. Only two enums of the exact same type can be compared.
      • No form of numeric data (integer, int64 or decimal) can be used in comparison to an enum. This is true for any type of expression including literals and non-literals with unknown value.
  • String Rendering
    • See the function check-enum-worker in common/enum_helpers.i.
    • Pre-Defined Enums
      • See the Names section of the DEFINE ENUM above for basic details of how each pre-defined enum's name matches the original text (in a case-preserving manner).
      • Pre-defined enums can be obtained by static name reference (e.g. oo.enums.Ordinals:first) OR by some reflective lookup which has the same exact int64 value (e.g. oo.enums.Ordinals:GetEnum(1) or oo.enums.Ordinals:GetEnum("first")).
      • Both regular enums and flags enums can obtained this way.
    • Dynamically Created Enums
      • Flag enums allow dynamic creation through:
        • Bitwise operations.
        • GetEnum(int64) reflective lookup
        • GetEnum(character) reflective lookup using a comma-separated list
      • Dynamic creation allows enums that have multiple values set or even arbitrary values which don't match any single predefined enum.
      • These enums do not have a any dedicated 4GL code which defines the name.
      • Instead, the name is calculated using this algorithm:
        • If there is only a single enum's bit(s) present in the enum, then that enum's name will be the name.
        • If there enum has bits set for more than one enum, the name associated with the instance will be a comma separated list of all flags which are fully present in the enum. This list will be in the order of enum's source code definition.
        • If the only flags present are the exact match to an aliased compound set of flags (see oo.enums.Permissions and the read which is 0x04, write which is 0x08 and readwrite which is read OR write or 0x0C) then the name will have the compound alias instead of the individual flags (e.g. passing 12 or 0x0C will return an enum with the name "readwrite" instead of "read,write").
        • Empty string is returned if no single enum member's bits are fully present.
    • STRING() and ToString()
      • The results of both of these are always the same.
      • This name will be returned as noted above for the given enum (pre-defined or dynamically created).
      • The contents of the character value will be the enum's name, with all case preserved.
      • STRING(oo.enums.Ordinals:first) will report first
      • STRING(oo.enums.DefineEnum15:two) will report tWO
      • STRING(oo.enums.Permissions:exec OR oo.enums.Permissions:readwrite) will report read,write,exec
      • STRING(oo.enums.Permissions:read OR oo.enums.Permissions:write) will report readwrite
      • STRING(oo.enums.DefineFlagsEnum20:one XOR oo.enums.DefineFlagsEnum20:neg-one) will report a character with no contents (the value is -2 and this corresponds to no single flag member in that enum).
      • STRING(oo.enums.DefineFlagsEnum34:neg-ten OR oo.enums.DefineFlagsEnum34:one) will report one,four (the value is -9 and the bits for 1 and 4 are both set).
    • QUOTER()
      • This name will be returned as noted above for the given enum (pre-defined or dynamically created).
      • The contents of the character value will an opening double quote character, then the enum's name(s) (with all case preserved) and then a closing double quote character.
      • QUOTER(oo.enums.Ordinals:first) will report "first"
      • QUOTER(oo.enums.DefineEnum15:two) will report "tWO"
      • QUOTER(oo.enums.Permissions:exec OR oo.enums.Permissions:readwrite) will report "read,write,exec"
      • QUOTER(oo.enums.Permissions:read OR oo.enums.Permissions:write) will report "readwrite"
      • QUOTER(oo.enums.DefineFlagsEnum20:one XOR oo.enums.DefineFlagsEnum20:neg-one) will report "" (the value is -2 and this corresponds to no single flag member in that enum).
      • QUOTER(oo.enums.DefineFlagsEnum34:neg-ten OR oo.enums.DefineFlagsEnum34:one) will report "one,four" (the value is -9 and the bits for 1 and 4 are both set).
  • Numeric Type Conversion
    • See the function check-enum-worker in common/enum_helpers.i.
    • INT64(enum_member) returns the member's value, unchanged.
    • INTEGER(enum_member)
      • If the member's int64 value is less than -2147483648 or greater than 2147483647, a runtime warning (error-status:error is false) Value <whatever> too large to fit in INTEGER. (15747).
      • Otherwise the member's value is returned as an integer.
    • DECIMAL(enum_member) returns the member's value as a decimal.
  • I/O Usage
    • See oo/enums/put_enum_output.p, oo/enums/put_flag_enum_output.p and oo/enums/export_enum_output.p.does_not_compile.
    • PUT
      • The PUT statement allows direct reference to enum members. For example, put <enum_member_expression> format "x(50)" skip..
      • <enum_member_expression> may be a any expression of an enum type.
      • The result for that field is the same output as STRING(<enum_member_expression>) or <enum_member_expression>:ToString().
    • The EXPORT statement cannot directly reference enum type expressions. Any attempt will generate a compile error Use the INTEGER or STRING function to see user defined object variables. (13464).
    • No other I/O statements are known to have direct enum reference support, but the STRING() or INT64() functions can be used to create a non-enum exression for output.

#6 Updated by Greg Shah about 4 years ago

I have a draft implementation for Enum and FlagsEnum that I've refactored so maybe the generator could start using that

I need to put these in place as a first step and then work on the conversion of the ENUM/DEFINE ENUM statements. From there I will work on the rest of the features noted in #4349-5. Each feature includes a range of conversion and runtime code.

I'd like to start with your versions of Enum and FlagsEnum. Are your changes already in 4384b?

Either we can push your changes from 4384b into 4231b and then reset 4384b as a copy of 4231b. Or I'll take your versions of Enum and FlagsEnum, augment them and put them in 4231b/4384b.

Let me know what you want to do. I'd like to start on these today if you have your versions ready.

#7 Updated by Marian Edu about 4 years ago

Greg Shah wrote:

I have a draft implementation for Enum and FlagsEnum that I've refactored so maybe the generator could start using that

I need to put these in place as a first step and then work on the conversion of the ENUM/DEFINE ENUM statements. From there I will work on the rest of the features noted in #4349-5. Each feature includes a range of conversion and runtime code.

I'd like to start with your versions of Enum and FlagsEnum. Are your changes already in 4384b?

Either we can push your changes from 4384b into 4231b and then reset 4384b as a copy of 4231b. Or I'll take your versions of Enum and FlagsEnum, augment them and put them in 4231b/4384b.

Let me know what you want to do. I'd like to start on these today if you have your versions ready.

Enum, FlagsEnum and some built-in enums are already in 4384b... I thought about using an inline java enum in those classes to make it easier to 'define' the members but thought it's better to stop and wait for you on that one.

The enum classes should be defined as final indeed, maybe we can prevent those from being deleted and not show in session objects list somehow. The automatic assignment of the value if not specified seems to be straightforward and I see you already have tests for that but since your conversion works on code that compile on 4GL I would not bother much about compile time validations :)

#8 Updated by Greg Shah about 4 years ago

maybe we can prevent those from being deleted and not show in session objects list somehow

Yes, as noted above this is how it must work.

I would not bother much about compile time validations :)

Exactly right!

#9 Updated by Constantin Asofiei about 4 years ago

A quick note, the enum values should be emitted like this:

   public static ContextLocal<object<? extends CustomEnum> enumValue1 = new ContextLocal<>()
   {
      protected object<? extends CustomEnum> initialValue()
      {
         return // code to create the enum;  
      }
   };

As an enum value is actually compatible with all OO methods (toString, comparison, etc), we need it to be an instance local to the FWD's context, and behave as other object instances.

#10 Updated by Constantin Asofiei about 4 years ago

4231b rev 11498 contains conversion-related fixes for 4GL ENUM. This was done by chasing down INTERFACE_DEF, KW_INTERFAC, CLASS_DEF, KW_CLASS, and making the proper adjustments to include ENUM_DEF or KW_ENUM, accordingly.

The callgraph parts are not complete.

#11 Updated by Greg Shah about 4 years ago

The first set of runtime changes will be to get the parent classes (Progress.Lang.Enum and Progress.Lang.FlagsEnum) integrated with the proper life-cycle behavior. This will include changes to common OO runtime code to properly tolerate/process enums.

Enums have the following inheritance and life cycle behaviors:

  • Enums are final.
  • Enums are not in the SESSION:FIRST-OBJECT/SESSION:LAST-OBJECT chain.
  • Calling enum_instance:NEXT-SIBLING()/enum_instance:PREV-SIBLING() returns unknown value.
  • VALID-OBJECT(enum_instance) is always true, even after DELETE OBJECT.
  • DELETE OBJECT is a NOP.
  • There are no constructors, destructors or members of an enum, except those enum instances that are part of the DEFINE ENUM statement.
  • No way to explicitly NEW, Class:NEW() or DYNAMIC-NEW().

Because of this, I think we can simplify much of the processing for Progress.Lang.Enum, Progress.Lang.FlagsEnum and all subclasses. The idea is to use a simpler Java implementation. In particular, I plan to short circuit much of the normal life cycle processing (e.g. for things like isValid() and delete()), add error handling and to completely avoid ever using ObjectResource for enum instances.

I don't understand why we need to use ContextLocal for the predefined enum instances. They are truly static, are immutable and cannot be different by context. For cases where the BaseObject methods are invalid, I was going to override them.

  • Add
    • _BaseObject_ default method boolean isTracked() { return true; }
  • Remove
    • *_constructor__ method
    • *_constructor__static__ method
    • *_execute__ method
  • Override
    • boolean isTracked() { return false; } (this will allow runtime methods to determine if a quick exit is needed to avoid all the tracking/reference counting/deletion; we won't need reflection and we don't need to hard code knowledge of the enum class itself into this locations)
    • BaseObject.getNextSibling()/BaseObject.getPrevSibling() to return unknown
  • Modify
    • ObjectOps
      • isValid()
      • delete()
      • newDynamicInstance()
      • newInstanceInternal()
      • increment()
      • decrement()
      • hasDestructors()
      • defaultStaticConstructor()
      • getConstructorMethod()
      • getExecuteMethods()
      • getExecuteMethod()
    • object
      • isUnknown()
      • isValid()

My intuition suggests that the following also need changes, but the changes are not clear to me:

  • ObjectOps
    • loadClassInt() - I think the SourceNameMapper.registerLegacyClass() may still be needed but the rest seems unnecessary
    • getStaticInstance()
    • getStaticClass()
    • getClassInstance()
    • asResource()
  • handle
    • fromString()
    • fromResourceId()
  • ControlFlowOps
    • initializeLegacyObject() - I think we can bypass calling this in ObjectOps.loadClassInt()
    • initializeLegacyClass() - I think we can bypass calling this in ObjectOps.newInstanceInternal()
    • invokeLegacyMethod()
    • executeDestructor() - never called if ObjectResource is never used

Constantin: Besides the question about why ContextLocal is needed instead of a real static implementation of enum members, I definitely need feedback on these other changes. The section at the bottom is especially in question but if you see other issues with my plan I would like to know.

#12 Updated by Greg Shah about 4 years ago

  • Status changed from New to WIP
  • Assignee set to Greg Shah
  • Start date deleted (10/15/2019)

#13 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

A quick note, the enum values should be emitted like this:
[...]

As an enum value is actually compatible with all OO methods (toString, comparison, etc), we need it to be an instance local to the FWD's context, and behave as other object instances.

This was the first attempt since you said ContextLocal is to be used instead of real static members, there were two problems with that:
- first is you can't instantiate the same object class inside the ContextLocal initialValue method cause at that point the class is not registered yet. ObjectOps.newInstance will give you a null back.
- all enum instances needs to have GetEnum methods that returns that specific enum type not just Enum, so there must be a list of valid enum values unless we write a big switch/case :(

I think if we use a private constructor and don't use ObjectOps at all to create the enum members but simply override isValid and isUnknown things are pretty much solved. Delete should be a no-op if the reference is an Enum, if the object is unknown it still throws error like now but if the ref is an enum is should simply return.

#14 Updated by Constantin Asofiei almost 4 years ago

Marian Edu wrote:

I think if we use a private constructor and don't use ObjectOps at all to create the enum members but simply override isValid and isUnknown things are pretty much solved. Delete should be a no-op if the reference is an Enum, if the object is unknown it still throws error like now but if the ref is an enum is should simply return.

The converted members need to be object instances in FWD (with a ref to the actual enum value). And this can be pass around to any other object var which is compatible (even progress.lang.object, right?). So we can't override stuff in object, we need to handle the enum case explicitly.

Greg: as these are truly immutable, then yes, we can avoid ContextLocal. Otherwise, I need to review your notes.

#15 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

The converted members need to be object instances in FWD (with a ref to the actual enum value). And this can be pass around to any other object var which is compatible (even progress.lang.object, right?). So we can't override stuff in object, we need to handle the enum case explicitly.

Yes, those are still object and the reference is a valid Enum value. Using a private constructor with new object(ref) and avoid going through the ObjectOps.newInstance all together (plus remove execute and 4gl 'constructors') seems to work. No need to override stuff in object, still ObjectOps needs to be made aware of Enums - isValid, increment, decrement and delete needed to be updated for that.

Greg: as these are truly immutable, then yes, we can avoid ContextLocal. Otherwise, I need to review your notes.

Since those can't be deleted I don't see why you need ContextLocal, it's safe to let all clients use the same enum values and beside I don't think ContextLocal can work with an object that doesn't participate to the regular lifecycle.

#16 Updated by Greg Shah almost 4 years ago

Good. So far, your feedback is completely consistent with my plan.

Constantin: I still need your thoughts on the section "My intuition suggests that the following also need changes, but the changes are not clear to me:" in #4349-11.

#17 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Good. So far, your feedback is completely consistent with my plan.

I've couldn't find a solution for using a Java enum though, one option for the GetEnum methods would be to use reflection to get all members... for some reason this doesn't appeal me much :(

I can push my changes to 4384b if you want, just need to update existing enums and flagenums.

#18 Updated by Constantin Asofiei almost 4 years ago

Greg, about the review:
  • I assume there will be a BaseEnum super-class?
  • we need to override toLegacyString and legacyEquals too (in BaseEnum)
  • what about clone?
  • we need to emit the getEnum() static methods in the converted enum Java code, but these should delegate the call to some helper in BaseEnum
  • you are right, no other c'tor, d'tor or execute method are needed in the generated Java code for an enum
  • we don't need to track the associated 'legacy class' or 'legacy instance' for the enum (as we do for the non-enum code), but:
    • in ObjectOps all usage of staticReferents, referents2static should never be reached - although I think this is already true for staticReferents, as this is used for non-enum usage only, we should protect for it (or at least log something)
    • same as the other cases you mention in for ObjectOps, they should never happen, but we should protect against it
    • ControlFlowOps is involved only in case of dynamic calls for an enum method - in such cases, this code needs to be protected. I'm not sure yet of the implications (as it will need an 'class surrogate' to emulate the 'persistent program' for the static method). As we don't have a 'context-local surrogate' for the enum class, this needs to be changed so that it can work.
    • BlockManager.checkJavaCall should avoid the ObjectOps.loadClass call, and also do not emit ObjectOps.loadClass for enum methods. But this can be protected directly at ObjectOps.loadClass (and related APIs), too.
  • for the other ObjectOps APIs you mention in the review section, I'm not sure all need to be protected, as some may be not be reached because of the caller code changes. Testing will show.
  • for the first section, I aggree with the approach

I want to continue with the enum conversion; do you have a design how the converted enum code will look like? My approach would be, for an enum class like:

enum CustomEnum:
    define enum
        enum-value-1
        enum-value-2
        enum-value-3.
end enum.

to emit this:
public class CustomEnum
extends BaseEnum
{
  public static final object<? extends CustomEnum> enumValue1 = BaseEnum.createEnumValue(CustomEnum.class, 1, "enum-value-1");

  public static final object<? extends CustomEnum> enumValue2 = BaseEnum.createEnumValue(CustomEnum.class, 2, "enum-value-2");

  public static final object<? extends CustomEnum> enumValue3 = BaseEnum.createEnumValue(CustomEnum.class, 3, "enum-value-3");

  public static object<? extends CustomEnum> getEnum(int64 idx)
  {
     return BaseEnum.getEnum(CustomEnum.class, idx);
  }

  public static object<? extends CustomEnum> getEnum(character enumValue)
  {
     return BaseEnum.getEnum(CustomEnum.class, enumValue);
  }
}

(plus the associated LegacySignature).

#19 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

  • I assume there will be a BaseEnum super-class?

Why another BaseEnum class, we already have lang.Enum mapped to Progress.Lang.Enum. The only thing different from 4GL is that this class is not final but again this can be handled in LegacyClass as a special case. If we add another class then the hierarchy will be different compared with 4GL one.

I think right now the Enum class holds references to all enum members (hash map on enum class) so it's basically the same thing as you describe unless I'm wrong and it isn't :(

#20 Updated by Constantin Asofiei almost 4 years ago

Marian Edu wrote:

I think right now the Enum class holds references to all enum members (hash map on enum class) so it's basically the same thing as you describe unless I'm wrong and it isn't :(

Sorry, I forgot about that. Yes, looks like there is no need for another base class, and just add the createEnumValue and getEnum to Enum.java.

OTOH, looking at that code, I can't shake a feeling that whenever we rely on something which requires the this-procedure, we will have problems... for example, we can't use BlockManager.function in Enum.java without changes in ProcedureManager to resolve and push the 'shared enum instance' or the 'shared enum class'.

#21 Updated by Greg Shah almost 4 years ago

I've couldn't find a solution for using a Java enum though

Correct, we cannot use Java enums. The result must be a subclass of the our equivalent to the legacy base class (Progress.Lang.Enum or Progress.Lang.FlagsEnum).

one option for the GetEnum methods would be to use reflection to get all members... for some reason this doesn't appeal me much :(

Agreed. I'm going to have internal data structures to store the pre-defined members.

I can push my changes to 4384b if you want, just need to update existing enums and flagenums.

Yes, please do.

we need to override toLegacyString and legacyEquals too (in BaseEnum)

Yes, my notes above are just about the life cycle issues. I have many other changes needed to fully implement all the helper methods, operators, CASE statement etc...

what about clone?

I'll need to check it with a test. It probably just returns this.

we need to emit the getEnum() static methods in the converted enum Java code, but these should delegate the call to some helper in BaseEnum

Yes. There are other methods like this (SetFlag, ToggleFlag and UnsetFlag) too.

The only thing different from 4GL is that this class is not final but again this can be handled in LegacyClass as a special case. If we add another class then the hierarchy will be different compared with 4GL one.

This is a good point.

I think right now the Enum class holds references to all enum members (hash map on enum class) so it's basically the same thing as you describe unless I'm wrong and it isn't :(

Yes, except the definition order is actually important in some cases so I may change this a little.

I want to continue with the enum conversion; do you have a design how the converted enum code will look like?
...
to emit this:

Yes, this has some parts as I envision. But we also need some other things:

  • the class itself should be final
  • it needs a private constructor so that instances cannot be created
  • there needs to be an abstract non-static "factory" method in the base class (something like Enum create(long, String)) which will actually construct the subclass; this must be implemented in the generated class; this will be used in the common code for the bitwise operators etc... which can return new instances of the same class as the operands
  • the data structure that holds the list of predefined enums is better in the generated class; otherwise all enums will be mixed together and the operations might be quite slow plus they would need special logic to ignore instances of the other enums
  • my initial idea on the data structure was to use an array, but perhaps collections are better; I haven't figured this out yet
  • I was expecting to use a helper in the base class (like your createEnumValue) but was thinking to maintain the data structure(s) in a static initializer in the generated code

I was going to work on the conversion now, but if you want to take it I won't argue. :)

I can't shake a feeling that whenever we rely on something which requires the this-procedure, we will have problems... for example, we can't use BlockManager.function in Enum.java without changes in ProcedureManager to resolve and push the 'shared enum instance' or the 'shared enum class'.

I don't think that the BlockManager stuff is needed at all. I was planning that all of the methods in the enum hierarchy (emitted or inherited) can be implemented as a standard FWD runtime approach with no extra blocks.

#22 Updated by Greg Shah almost 4 years ago

Constantin: If you want to continue with the conversion, do let me know. I would then focus on the runtime parts.

Marian: Let me know when you have your latest enum changes in 4384b. I'll pull them and use them as the basis for the versions in 4231b.

#23 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

Constantin: If you want to continue with the conversion, do let me know. I would then focus on the runtime parts.

Yes, I'll take the conversion. Do you have a comprehensive list of all the static methods which need to be added and their signatures? Or toggleFlag, unsetFlag, setFlag and getEnum are all we need?

the data structure that holds the list of predefined enums is better in the generated class; otherwise all enums will be mixed together and the operations might be quite slow plus they would need special logic to ignore instances of the other enums

I assume this will be exposed via a getEnumValues abstract method (or others similar, for example to get by index and/or name) in the Enum super-class? The idea with the static class initializer is nice; the createEnumValue helper was meant to have a single point for creating these. I'm thinking about the skeleton classes, too, to make it easier to write those. I don't want to duplicate too much code by hand in the skeletons.

my initial idea on the data structure was to use an array, but perhaps collections are better; I haven't figured this out yet

We need lookup both via index and (lowercased) name. So a hashmap and an array should be enough.

#24 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Marian: Let me know when you have your latest enum changes in 4384b. I'll pull them and use them as the basis for the versions in 4231b.

Committed revision 11468 in 4384b, an example of Enum is oo/reflect/AccessMode and oo/reflect/Flags for FlagsEnum.

#25 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

Yes, I'll take the conversion. Do you have a comprehensive list of all the static methods which need to be added and their signatures? Or toggleFlag, unsetFlag, setFlag and getEnum are all we need?

toggleFlag, setFlag, unsetFlag and isFlagSet are all implemented in FlagsEnum, no need to add those to all instances, those must extend FlagsEnum anyway.

We need lookup both via index and (lowercased) name. So a hashmap and an array should be enough.

Array could work if we add all members at once (we know the size), I've used a list with createEnum.

#26 Updated by Constantin Asofiei almost 4 years ago

Marian Edu wrote:

Constantin Asofiei wrote:

Yes, I'll take the conversion. Do you have a comprehensive list of all the static methods which need to be added and their signatures? Or toggleFlag, unsetFlag, setFlag and getEnum are all we need?

toggleFlag, setFlag, unsetFlag and isFlagSet are all implemented in FlagsEnum, no need to add those to all instances, those must extend FlagsEnum anyway.

How do you define an enum to extend FlagsEnum in 4GL?

Greg: I don't see the enum tests on our testcases project.

#27 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

How do you define an enum to extend FlagsEnum in 4GL?

By using the flags option...

enum <EnumName> flags:
end.
<pre>

> 
> Greg: I don't see the enum tests on our testcases project.
I've found those in oo/enums.

#28 Updated by Greg Shah almost 4 years ago

Or toggleFlag, unsetFlag, setFlag and getEnum are all we need?

This should be the list, but the toggleFlag, unsetFlag, setFlag methods are not static. They are instance methods.

I assume this will be exposed via a getEnumValues abstract method (or others similar, for example to get by index and/or name) in the Enum super-class?

Our various helper methods/implementation methods in the base classes will use the data structures directly. The only external access is from Progress.Lang.Class but we will add helpers for this access.

I'm thinking about the skeleton classes, too, to make it easier to write those. I don't want to duplicate too much code by hand in the skeletons.

I was assuming that we just use the standard 4GL enum syntax for skeletons. It should be enough.

We need lookup both via index and (lowercased) name. So a hashmap and an array should be enough.

I don't think we need indexed lookup, but we do need to do an ordered (in the order of definition) search by value. We definitely need name and value lookups.

Yes, I'll take the conversion.

Cool. The majority of this will be easy to implement in templates, with differences for regular vs flags enums. Please note that the tricky part is in the implicit value calculations, the aliasing and the duplication of values. All of it is documented above.

toggleFlag, setFlag, unsetFlag and isFlagSet are all implemented in FlagsEnum, no need to add those to all instances, those must extend FlagsEnum anyway.

For IsFlagSet, this is clear.

I was thinking about this, for toggleFlag, setFlag, unsetFlag don't we need to cast the result to the right sub-class type? The core logic can be in the parent class but we may need a shim layer in the generated enum. I was thinking about class parameters/generics as a possible solution but I'm not sure that really does it.

Greg: I don't see the enum tests on our testcases project.

It is in the new/good testcases, not in old/bad/uast.

#29 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

I don't think we need indexed lookup, but we do need to do an ordered (in the order of definition) search by value. We definitely need name and value lookups.

I mean the GetEnum(idx) - isn't this something like 'get the Nth defined enum'?

It is in the new/good testcases, not in old/bad/uast.

Thanks, found them.

#30 Updated by Constantin Asofiei almost 4 years ago

Constantin Asofiei wrote:

Greg Shah wrote:

I don't think we need indexed lookup, but we do need to do an ordered (in the order of definition) search by value. We definitely need name and value lookups.

I mean the GetEnum(idx) - isn't this something like 'get the Nth defined enum'?

OK, I understand, I see the syntax now.

#31 Updated by Constantin Asofiei almost 4 years ago

Can we rename p2j.oo.lang.Enum to p2j.oo.lang.LegacyEnum? To avoid java conflicts...

#32 Updated by Greg Shah almost 4 years ago

Can we rename p2j.oo.lang.Enum to p2j.oo.lang.LegacyEnum? To avoid java conflicts...

Yes.

#33 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

Greg Shah wrote:

I don't think we need indexed lookup, but we do need to do an ordered (in the order of definition) search by value. We definitely need name and value lookups.

I mean the GetEnum(idx) - isn't this something like 'get the Nth defined enum'?

That has nothing to do with the order, it's the actual value used there. If no explicit value is set, then indeed is the same as defined order but this is caused by the way 'default' values are defined by the compiler so this is something to be handled during conversion..

#34 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Can we rename p2j.oo.lang.Enum to p2j.oo.lang.LegacyEnum? To avoid java conflicts...

Yes.

Right, think we need to do the same for 'Map'... damn, missed that :(

#35 Updated by Constantin Asofiei almost 4 years ago

Greg, can you compile in 4GL all programs/classes in oo/enums? I have some weird errors:
  • oo.enums.Ordinals and DefineEnum3 should have flags, as test_enum_comparisons.p and another program uses ToggleFlag, setFlag, etc with them
  • there is a {common/enum_helpers.i} line in non_flag_enums_are_final_classes_with_superclass_progress_lang_enum.p and non_flag_enums_are_final_classes_with_superclass_progress_lang_enum.p
  • oo/enums/MultibitMembersAllowMoreThan64Flags.cls has oo.enums.MultibitAllowMoreThan64Flags instead of oo.enums.MultibitMembersAllowMoreThan64Flags

#36 Updated by Greg Shah almost 4 years ago

Fixed in rev 559. These were mostly cases where I had tested the code and then later changed the include file without fixing up all locations.

#37 Updated by Constantin Asofiei almost 4 years ago

Greg, I've added stubs for ObjectOps.newDynamicEnum and we also need a ObjectOps.registerEnum(Class) to register an enum (this will be called from the static c'tor).

#38 Updated by Constantin Asofiei almost 4 years ago

Greg, my plan is for something like this:

class CustomEnum:
   define enum
      one 
      two.
   define enum
      three = two
      four = 1234.
end.

to generate:
public final class CustomEnum
extends LegacyEnum
{
   private static java.util.Map<Long, CustomEnum> byValue = new java.util.LinkedHashMap<>();

   private static java.util.Map<String, CustomEnum> byName = new java.util.LinkedHashMap<>();

   public static final object<? extends com.goldencode.testcases.CustomEnum> one;

   public static final object<? extends com.goldencode.testcases.CustomEnum> two;

   public static final object<? extends com.goldencode.testcases.CustomEnum> three;

   public static final object<? extends com.goldencode.testcases.CustomEnum> four;

   static
   {
      one = createEnum(new CustomEnum(), "one", byValue, byName); // first in list
      two = createEnum(new CustomEnum(), "two", "one", byValue, byName); // increment from previous value
      three = createEnum(new CustomEnum(), "three", "two", true, byValue, byName); // alias
      four = createEnum(new CustomEnum(), "four", 1234, byValue, byName); // explicit value
   }

   private CustomEnum()
   {
   }

   public static object<CustomEnum> getEnum(int64 val)
   {
      return getEnum(CustomEnum.class, byValue, val);
   }

   public static object<CustomEnum> getEnum(character val)
   {
      return getEnum(CustomEnum.class, byName, val);
   }
}

The four cases are:
  • first in list - just assign to 1
  • createEnum with 2nd and 3rd argument a String - increment the value using the previously defined enum as specified by the 3rd argument
  • createEnum with 2nd and 3rd argument a String, and a 4th true - assign the exact value for the specified alias
  • assign an explicit value

The conversion rules will take care of linking these together, and the runtime will just make sure to compute everything correctly. I don't want to add code in the conversion to compute the exact value.

The two byValue and byName maps will always hold all the enum values.

#39 Updated by Greg Shah almost 4 years ago

I just checked in new tests for Clone() (it doesn't work) and Progress.Lang.Enum:ToObject() (works like GetEnum() and DYNAMIC-ENUM()). See revision 561. I also updated the details in #4349-3.

#40 Updated by Greg Shah almost 4 years ago

Overall, I like the approach.

The conversion rules will take care of linking these together, and the runtime will just make sure to compute everything correctly. I don't want to add code in the conversion to compute the exact value.

I don't like the idea that there is so much implicit 4GL processing left behind. This makes the resulting code more fragile because people editing it later won't always understand the rules.

Regardless of this, for createEnum with 2nd and 3rd argument a String, I don't want to pass the previously defined name. If we are already relying upon the first implicit item to come first, then we can also rely upon the next implicit ones to be in order. In that case, then the implicit ones are always just defined using the most recently added enum.

This means that first in list and createEnum with 2nd and 3rd argument a String would both look like createEnum(new CustomEnum(), "member_name", byValue, byName);.

And then the createEnum with 2nd and 3rd argument a String, and a 4th true would actually be createEnum(new CustomEnum(), "new_member_name", "old_member_name", byValue, byName);.

The two byValue and byName maps will always hold all the enum values.

I think we need to provide some abstract methods to return these maps. Otherwise we can't implement the necessary logic in the parent classes. Although that does not help for the static methods (see below), it can be a cleaner result for things like createEnum() and the bitwise operations.

For the static methods (GetEnumValues(), GetEnumNames(), GetEnumValue(), GetEnumName(), Progress.Lang.Enum:ToObject() and DYNAMIC-ENUM()) the problem will need reflection to resolve.

#41 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

This means that first in list and createEnum with 2nd and 3rd argument a String would both look like createEnum(new CustomEnum(), "member_name", byValue, byName);.

And then the createEnum with 2nd and 3rd argument a String, and a 4th true would actually be createEnum(new CustomEnum(), "new_member_name", "old_member_name", byValue, byName);.

OK, I'll change it. You are right, it makes sense.

The two byValue and byName maps will always hold all the enum values.

I think we need to provide some abstract methods to return these maps. Otherwise we can't implement the necessary logic in the parent classes. Although that does not help for the static methods (see below), it can be a cleaner result for things like createEnum() and the bitwise operations.

Well... actually I've made some changes so that in the static c'tor there is this call (as we need to know what enums have been registered so far):

registerEnum(CustomEnum.class, "CustomEnum", byValue, byName);

and LegacyEnum will take care of registering the maps. And this allows simple calls for createEnum and getEnum, to avoid passing the maps. Runtime will just interrogate the registry (a simple map get, should be fast for IdentityHashMap).

BTW, the runtime must rely on SourceNameMapper to resolve a enum class, if the first ever access is via reflection.

#42 Updated by Constantin Asofiei almost 4 years ago

Does someone have the FWD equivalent for FlagsEnum ?

#43 Updated by Greg Shah almost 4 years ago

Good, I like it.

Does someone have the FWD equivalent for FlagsEnum ?

These are checked in to 4384b. I will copy Enum and FlagsEnum to 4231b. I haven't made edits yet.

#44 Updated by Constantin Asofiei almost 4 years ago

Greg, the first pass at DEFINE ENUM conversion is in 4231b rev 11503. Please review.

The oo/enums tests convert, but FlagsEnum, bitwise operands, GET-CLASS and some other parts are not supported (so the code doesn't fully compile). What is relevant for non-flags enum compiles.

Note that I've changed another two cases of throwException to printfln, to not abend (for builtin functions and 'invalid operands' case).

#45 Updated by Greg Shah almost 4 years ago

I was just about to check in all the LegacyEnum, FlagsEnum, and the fixups of all the existing enum subclasses. It looks like we duplicated work on that. :(

#46 Updated by Greg Shah almost 4 years ago

From Marian:

One thing I wanted to mention about enums is those can be used in switch/case statements, also would be nice to make it easier to work with in Java... if we can't get some inline enum in place will probably continue with `equals`.

#47 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

From Marian:

One thing I wanted to mention about enums is those can be used in switch/case statements, also would be nice to make it easier to work with in Java... if we can't get some inline enum in place will probably continue with `equals`.

Yeah, I mean not only for supporting case/when statement on conversion but also actually using the LegacyEnums in plain java using switch.

#48 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

The two byValue and byName maps will always hold all the enum values.

Constantin, do you think two maps are needed to handle say about 5-10 enum members that one might use? Those will be used for GetEnum and dynamic access but this is probably about 1% of the code out there, not the most common use of enums imho.

It could work better for flags as there could be more combinations only that there it will be useless cause the order of flags is not relevant, aka this will return the same enum member:

GetEnum('input,append,byreference,output,bind')
GetEnum('input,output,append,byreference,bind')

#49 Updated by Greg Shah almost 4 years ago

One thing I wanted to mention about enums is those can be used in switch/case statements, also would be nice to make it easier to work with in Java... if we can't get some inline enum in place will probably continue with `equals`.

We plan to support all the features of enum including conversion of CASE statements. However, due to the compatibility requirements (especially the need to maintain the 4GL inheritance hierarchy), we are not planning to use real Java enums. As you note, this causes a limitation in how we use the results in a Java switch.

Please note that we already have a mis-match between switch and CASE. In the 4GL, WHEN clauses can be complex expressions (they do not need to be constants) while in Java each case must be a compile-time constant or an enum (which is technically not always considered a compile-time constant). The key point here is that an expression which is not constant cannot be placed as one of the alternatives. Java objects (except for enums and constants like String literals) cannot be used. There are some other differences, but these are the big ones. Any such usage needs to convert to if/else if/else.

We could define a real Java enum in addition to the planned Java class + static members approach documented above. This would have to duplicate each member as a kind of "shadow" or "mirror" to the Java class. This would be error prone (it must be maintained in parallel with any changes to the Java class) and confusing since they would have to be different entities. Since they can't be substituted for each other, they would only work for specific use cases and the developer would have to understand the meaning. I think these drawbacks are too significant. I don't plan this approach.

The plan is to convert as if/else if/else, unless someone has a better idea.

#50 Updated by Greg Shah almost 4 years ago

Constantin, do you think two maps are needed to handle say about 5-10 enum members that one might use? Those will be used for GetEnum and dynamic access but this is probably about 1% of the code out there, not the most common use of enums imho.

You are right that we could eliminate one map. But it would make some method implementations much more complicated. I prefer to take the extra map overhead in order to get the cleaner code. As you say, enums are not so common so there is not much of a memory hit especially since the maps are static.

#51 Updated by Constantin Asofiei almost 4 years ago

I've forgot to add the LegacySignature to the GetEnum method.

I don t plan to continue with enum conversion for now.

get-class was added, but not yet commited. Will do later today.

#52 Updated by Greg Shah almost 4 years ago

I've checked in the first pass at the base classes. They are unfinished.

I've moved the creation of the data structures (byValue, byName and a new one called enums) into LegacyEnum. I want to have control over the structures (and be able to change them later) and there is no advantage to letting them be created in every subclass. Also, this reduces the generated code. I've made the changes to conversion to match.

The registerEnum() call changed too. Of course, it must be executed in the static initializer before the createEnum() calls.

I will continue with this tomorrow. Everything builds but I haven't tested any of it.

#53 Updated by Greg Shah almost 4 years ago

4231b revision 11511 is the next pass at enum runtime and conversion, still unfinished. Registration has been modified to pass the private 2-arg constructor as a lambda. This allows the create methods and future flags enum bitwise processing to have a callback for instantiation. There is no need for reflection or for the subclass to pass uninitialized instances. It also allows the state of the enum to be final.

#54 Updated by Greg Shah almost 4 years ago

Consider this code:

         throw new LegacyErrorException(
                  SysError.newInstance(String.format("Invalid value specified for %s:GetEnum",
                           ObjectOps.getLegacyName(enumClass)), 15246));

LegacyErrorException extends ErrorConditionException which is the equivalent or the ERROR condition in the 4GL. But this code bypasses the ERROR-STATUS processing (recording error/warning state when NO-ERROR is present. The proper processing for ERROR-STATUS is done by the ErrorManager.

Although we may need to make modifications to ErrorManager better integrate with the OO 4GL exceptions (e.g. SysError and related), I think we need to be using the ErrorManager for these cases.

Am I missing something? Perhaps this is part of the reason that some incorrect behavior has been seen with NO-ERROR?

Please note that the code above will result in tracked objects (e.g. in the session object chain) being created for the exceptions. If this is necessary, then it will need some attention as well in ErrorManager.

#55 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Consider this code:

[...]

LegacyErrorException extends ErrorConditionException which is the equivalent or the ERROR condition in the 4GL. But this code bypasses the ERROR-STATUS processing (recording error/warning state when NO-ERROR is present. The proper processing for ERROR-STATUS is done by the ErrorManager.

What we used, and seemed to work fine for our tests, is using undoThrow from BlockManager as in:

undoThrow(SysError.newInstance(String.format("Invalid value specified for %s:GetEnum",
                           ObjectOps.getLegacyName(enumClass)), 15246));

#56 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Please note that the code above will result in tracked objects (e.g. in the session object chain) being created for the exceptions. If this is necessary, then it will need some attention as well in ErrorManager.

This is correct, the error pop up the stack till is being catch and if is not deleted in that catch block it will linger in the session objects chain - eventually will be garbage collected at some point. It was recommended to delete error objects inside catch block, before they changed the recommendation to not delete anything anymore and let the GC handle everything :)

A sample testcase in error_handling/test_object_chain.p.

#57 Updated by Constantin Asofiei almost 4 years ago

Greg, regarding the static c'tor problem: I think the LegacyEnum.createEnumWorker should return null if Configuration.isRuntimeConfig() returns false - this indicates that we are not within the FWD server, and are doing conversion, which doesn't need the actual enum instance.

The other issue I mentioned about the object for enum needing to be detached completed from context-local state still stands. I think simple use-case is to have a main somewhere which sets runtime-config to true and just calls Enum.class on the legacy enums.

#58 Updated by Constantin Asofiei almost 4 years ago

For the infinite recursion - the issue was that WidgetPool was calling TM.getTransactionHelper() and PM.getProcedureHelper() before it was created, and these two depend back on it. I have a fix for it, to set these two fields in locate().

The "bad" news: it no longer fails at all when creating an enum instance, so it will no longer be obvious that it relies on context-local state.

#59 Updated by Constantin Asofiei almost 4 years ago

For FlagsEnum.cls - see ClassDefinition.registerEnumMethods.

Here, we need to add the proper signatures for all flags-related methods, which exist at the child class. There are these in FlagsEnum.cls:

   /* this is mocked here but in the 4GL it is implicitly added to the child enum class */
   /* and the type is coded to that enum */
   method public Progress.Lang.FlagsEnum SetFlag(input flag as Progress.Lang.FlagsEnum):
   end method.

   /* this is mocked here but in the 4GL it is implicitly added to the child enum class */
   /* and the type is coded to that enum */
   method public Progress.Lang.FlagsEnum ToggleFlag(input flag as Progress.Lang.FlagsEnum):
   end method.

   /* this is mocked here but in the 4GL it is implicitly added to the child enum class */
   /* and the type is coded to that enum */
   method public Progress.Lang.FlagsEnum UnsetFlag(input flag as Progress.Lang.FlagsEnum):
   end method.

The registered signatures need to have the proper FlagsEnum sub-type for the parameter and return type.

Also, the templates which generate the getEnum Java method need to have LegacySignature added, so these can be found via reflection.

#60 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

For FlagsEnum.cls - see ClassDefinition.registerEnumMethods.

Here, we need to add the proper signatures for all flags-related methods, which exist at the child class. There are these in FlagsEnum.cls:

Constantin, while is true in 4GL the type for return and input parameters of those methods is the actual FlagEnum at hand and not the base class those methods will just call the one in super and since you probably don't need to worry about code completion/check syntax in FWD I don't see much value in generating those in all flags enums. If needed we can fix that for reflection although I doubt anyone is using reflection on enums to check the signature of those methods :)

#61 Updated by Constantin Asofiei almost 4 years ago

Marian Edu wrote:

Constantin Asofiei wrote:

For FlagsEnum.cls - see ClassDefinition.registerEnumMethods.

Here, we need to add the proper signatures for all flags-related methods, which exist at the child class. There are these in FlagsEnum.cls:

Constantin, while is true in 4GL the type for return and input parameters of those methods is the actual FlagEnum at hand and not the base class those methods will just call the one in super and since you probably don't need to worry about code completion/check syntax in FWD I don't see much value in generating those in all flags enums. If needed we can fix that for reflection although I doubt anyone is using reflection on enums to check the signature of those methods :)

Calling the one in super is fine. The problem is the return type - if the return type is the actual enum type, and not FlagsEnum, then you can chain these together. And this makes it mandatory to have the exact definitions both at parser time, and at conversion. Otherwise, chained calls will not work.

#62 Updated by Constantin Asofiei almost 4 years ago

Constantin Asofiei wrote:

Calling the one in super is fine. The problem is the return type - if the return type is the actual enum type, and not FlagsEnum, then you can chain these together. And this makes it mandatory to have the exact definitions both at parser time, and at conversion. Otherwise, chained calls will not work.

Looks like there is nothing specific to the custom flags enum definition which can be chained via instance method calls. Am I right? The enum members can only be accessed via the class name (statically).

If the above is true, then there is no need to inject these at the flags enum in FWD, and to add the signatures with the correct datatypes.

Another concern is: how about reflection? If you need to specify the actual enum name to get the i.e. ToggleFlag method via reflection, then FWD will need changes at the runtime to properly resolve the method from the FlagsEnum super-class.

#63 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

Constantin Asofiei wrote:

Calling the one in super is fine. The problem is the return type - if the return type is the actual enum type, and not FlagsEnum, then you can chain these together. And this makes it mandatory to have the exact definitions both at parser time, and at conversion. Otherwise, chained calls will not work.

Looks like there is nothing specific to the custom flags enum definition which can be chained via instance method calls. Am I right? The enum members can only be accessed via the class name (statically).

If the above is true, then there is no need to inject these at the flags enum in FWD, and to add the signatures with the correct datatypes.

Yes, this is correct. You could add the signatures if you want though - only methods SetFlag, UnsetFlag and ToggleFlag are defined at each enum instance. IsFlagSet is at FlagsEnum level although given the input parameter one would have expected to also be at enum instance level.

ToString, GetValue, CompareTo are from Enum base class

Another concern is: how about reflection? If you need to specify the actual enum name to get the i.e. ToggleFlag method via reflection, then FWD will need changes at the runtime to properly resolve the method from the FlagsEnum super-class.

It's all 'fake' on 4GL side as well, no static methods are visible in neither specific enum nor P.L.Enum or P.L.FlagsEnum. Enum members does not show as 'properties' although it behaves like a static ones so we will have to handle enums as a specific case in reflection anyway.

Ah, I had to fix the LegacyEnum not to return null in createEnumWorker and increment the value from the last one found if not specified otherwise it was making enums with the same value. And also the members in HashAlgorithmEnum needed to be lowercase.

#64 Updated by Greg Shah almost 4 years ago

Calling the one in super is fine. The problem is the return type - if the return type is the actual enum type, and not FlagsEnum, then you can chain these together. And this makes it mandatory to have the exact definitions both at parser time, and at conversion. Otherwise, chained calls will not work.

Looks like there is nothing specific to the custom flags enum definition which can be chained via instance method calls. Am I right? The enum members can only be accessed via the class name (statically).

If the above is true, then there is no need to inject these at the flags enum in FWD, and to add the signatures with the correct datatypes.

Yes, this is correct. You could add the signatures if you want though - only methods SetFlag, UnsetFlag and ToggleFlag are defined at each enum instance. IsFlagSet is at FlagsEnum level although given the input parameter one would have expected to also be at enum instance level.

I'm planning to emit specific versions of these in each flags enum subclass. They will call the worker at the parent level and are just there to implement the correct signatures/typing.

ToString, GetValue, CompareTo are from Enum base class

Yes, only the GetEnum needs this treatment. I have to fix the annotations there.

Another concern is: how about reflection? If you need to specify the actual enum name to get the i.e. ToggleFlag method via reflection, then FWD will need changes at the runtime to properly resolve the method from the FlagsEnum super-class.

It's all 'fake' on 4GL side as well, no static methods are visible in neither specific enum nor P.L.Enum or P.L.FlagsEnum. Enum members does not show as 'properties' although it behaves like a static ones so we will have to handle enums as a specific case in reflection anyway.

Only the subclasses will have the annotations and the workers will be protected. I think this should yield the same results.

Ah, I had to fix the LegacyEnum not to return null in createEnumWorker and increment the value from the last one found if not specified otherwise it was making enums with the same value. And also the members in HashAlgorithmEnum needed to be lowercase.

I've fixed these issues in my code. Thanks!

#65 Updated by Greg Shah almost 4 years ago

I'm planning to emit specific versions of these in each flags enum subclass. They will call the worker at the parent level and are just there to implement the correct signatures/typing.

To be clear, the reason I plan this is so that we can get full compatibility. I don't want to leave something partial here and then have to find/remember it and fix it later. The extra cost in generated code and conversion effort is minimal.

#66 Updated by Greg Shah almost 4 years ago

Constantin, some questions:

1. Do I need to add the @LegacySignature annotation to the converted enum members?

   @LegacySignature(type = Type.VARIABLE, name = "my-legacy-enum-name")
   public static final object<? extends AccessMode> myLegacyEnumName;

2. I get a very large number of warnings for the implicit methods (2 GetEnum variants for regular enums and the IsFlagSet/SetFlag/ToggleFlag/UnsetFlag for flags enums). This doesn't seem to have any real negative affect, the converted enum classes are correct and the code that references these methods seems OK too. But the logs are full of these warnings.

./oo/enums/DefineEnum1.cls
WARNING: Can't locate converted method for getenum(KW_INPUT int64) in com.goldencode.testcases.oo.enums.DefineEnum1
WARNING: Can't locate converted method for getenum(KW_INPUT character) in com.goldencode.testcases.oo.enums.DefineEnum1
...
./oo/enums/bitwise_operations_unknown_value.p
WARNING: Can't locate converted method for progress.lang.flagsenum() in com.goldencode.p2j.oo.lang.FlagsEnum
WARNING: Can't locate converted method for unsetflag(KW_INPUT object<? extends progress.lang.flagsenum>) in com.goldencode.p2j.oo.lang.FlagsEnum
WARNING: Can't locate converted method for setflag(KW_INPUT object<? extends progress.lang.flagsenum>) in com.goldencode.p2j.oo.lang.FlagsEnum
WARNING: Can't locate converted method for toggleflag(KW_INPUT object<? extends progress.lang.flagsenum>) in com.goldencode.p2j.oo.lang.FlagsEnum

#67 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

1. Do I need to add the @LegacySignature annotation to the converted enum members?

As I understand, we have a separate registry for these in the LegacyEnum class, which will be used for resolving them via reflection. If this is correct, we don't need the LegacySignature for enum members, as these will not be really used.

2. I get a very large number of warnings for the implicit methods (2 GetEnum variants for regular enums and the IsFlagSet/SetFlag/ToggleFlag/UnsetFlag for flags enums). This doesn't seem to have any real negative affect, the converted enum classes are correct and the code that references these methods seems OK too. But the logs are full of these warnings.

I think this can be solved in ClassDefinition.registerGetEnumMethods - add the javaname to javaMethodNames map, like:

      String legacySignature1 = SymbolResolver.calculateLegacySignature(mdat1.name, mdat1.signature);
      javaMethodNames.put(legacySignature1, "getEnum");
      String legacySignature2 = SymbolResolver.calculateLegacySignature(mdat2.name, mdat2.signature);
      javaMethodNames.put(legacySignature2, "getEnum");

I assume you added unsetFlag, setFlag and toggleFlag to registerGetEnumMethods, too?

#68 Updated by Marian Edu almost 4 years ago

There is an issue with using an Enum's GetEnum method passed as input parameter to another method. That fails during conversion, the method loadConvertedClass in SystemResolver being called with null as filename for whatever reason.

[java] EXPRESSION EXECUTION ERROR:
     [java] ---------------------------
     [java] nameinfo = loadConvertedClass(fname, false)
     [java]            ^  { Can not find class associated with null }
     [java] ---------------------------
     [java] ERROR:
     [java] com.goldencode.p2j.pattern.TreeWalkException: ERROR!  Active Rule:
     [java] -----------------------
     [java]       RULE REPORT      
     [java] -----------------------
     [java] Rule Type :   WALK
     [java] Source AST:  [ GetHash ] BLOCK/INNER_BLOCK/BLOCK/ASSIGNMENT/EXPRESSION/OBJECT_INVOCATION/OO_METH_RAW/ @16:17 {17179869350}
     [java] Copy AST  :  [ GetHash ] BLOCK/INNER_BLOCK/BLOCK/ASSIGNMENT/EXPRESSION/OBJECT_INVOCATION/OO_METH_RAW/ @16:17 {17179869350}
     [java] Condition :  nameinfo = loadConvertedClass(fname, false)
     [java] Loop      :  false
     [java] --- END RULE REPORT ---
     [java]     at com.goldencode.p2j.pattern.PatternEngine.run(PatternEngine.java:1070)
     [java]     at com.goldencode.p2j.convert.TransformDriver.processTrees(TransformDriver.java:569)
     [java]     at com.goldencode.p2j.convert.ConversionDriver.back(ConversionDriver.java:573)
     [java]     at com.goldencode.p2j.convert.TransformDriver.executeJob(TransformDriver.java:956)
     [java]     at com.goldencode.p2j.convert.ConversionDriver.main(ConversionDriver.java:1025)
     [java] Caused by: com.goldencode.expr.ExpressionException: Expression execution error @1:12 [OO_METH_RAW id=17179869350]
     [java]     at com.goldencode.p2j.pattern.AstWalker.walk(AstWalker.java:275)
     [java]     at com.goldencode.p2j.pattern.AstWalker.walk(AstWalker.java:210)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.apply(PatternEngine.java:1633)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.processAst(PatternEngine.java:1531)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.processAst(PatternEngine.java:1479)
     [java]     at com.goldencode.p2j.pattern.PatternEngine.run(PatternEngine.java:1034)
     [java]     ... 4 more
     [java] Caused by: com.goldencode.expr.ExpressionException: Expression execution error @1:12
     [java]     at com.goldencode.expr.Expression.execute(Expression.java:484)
     [java]     at com.goldencode.p2j.pattern.Rule.apply(Rule.java:497)
     [java]     at com.goldencode.p2j.pattern.Rule.executeActions(Rule.java:745)
     [java]     at com.goldencode.p2j.pattern.Rule.coreProcessing(Rule.java:712)
     [java]     at com.goldencode.p2j.pattern.Rule.apply(Rule.java:534)
     [java]     at com.goldencode.p2j.pattern.Rule.executeActions(Rule.java:745)
     [java]     at com.goldencode.p2j.pattern.Rule.coreProcessing(Rule.java:712)
     [java]     at com.goldencode.p2j.pattern.Rule.apply(Rule.java:534)
     [java]     at com.goldencode.p2j.pattern.Rule.executeActions(Rule.java:745)
     [java]     at com.goldencode.p2j.pattern.Rule.coreProcessing(Rule.java:712)
     [java]     at com.goldencode.p2j.pattern.Rule.apply(Rule.java:534)
     [java]     at com.goldencode.p2j.pattern.RuleContainer.apply(RuleContainer.java:585)
     [java]     at com.goldencode.p2j.pattern.RuleSet.apply(RuleSet.java:98)
     [java]     at com.goldencode.p2j.pattern.AstWalker.walk(AstWalker.java:262)
     [java]     ... 9 more
     [java] Caused by: java.lang.ClassNotFoundException: Can not find class associated with null
     [java]     at com.goldencode.p2j.uast.SymbolResolver.loadConvertedClass(SymbolResolver.java:1854)

A test case in oo/enums/test_enum_input_param.p, using an intermediate variable solves the issue. First I've thought it could be because there were no qualified set in getEnum methods in HashAlgorithmEnum but adding that do not seem to help.

#69 Updated by Greg Shah almost 4 years ago

4231b revision 11564 is the first full implementation of enum support. Conversion is working well and has been tested. Runtime has all features fully implemented but has not been tested at all. In particular, all features documented in discussed #4349-5, #4349-11 (excluding the ControlFlowOps changes) and #4349-18 are all implemented. This includes the expected changes to ObjectOps and the enum-related reflection implementations in LegacyClass. I did not touch the GET-CLASS() implementation.

I also did NOT implement anything related to the error handling concerns in #4602-9. I think there are definitely changes needed there and I still have open questions to be discussed there. Regardless, I don't think that these error issues will be a problem for the enum tests so I have not worked on the error stuff.

I've tested everything in the testcases oo/enums/ directory, including oo/enums/test_enum_input_param.p. Everything converts and compiles successfully. My checks on the resultng code show it to be as expected, though I did not check every file manually.

I will post next to show examples of how various things convert. One important note: in the end, due to the limitations of Java generics (type erasure and/or constraints on type inference), there was no compile time advantage to emitting the SetFlag/ToggleFlag/UnsetFlag methods into the converted flags enum classes. Instead, I implemented these in FlagsEnum directly using generics and the is an added runtime type check to ensure different flags enum types can't intermix. This would have been needed anyway, since the emitted code would have required casting to make it compile. In other words, emitting the code into the FlagsEnum subclasses will only compile by explicitly casting the instances, which breaks the idea is of compile-time type safety. Lacking that advantage, it is much better to avoid the extra emitted code, thus leaving the converted implementation much smaller.

As part of this, I copied or updated the following Java code from 4384b rev 11565.

modified src/com/goldencode/p2j/oo/core/HashAlgorithmEnum.java
modified src/com/goldencode/p2j/oo/logging/LogLevelEnum.java
modified src/com/goldencode/p2j/oo/net/UriEncodingTypeEnum.java
modified src/com/goldencode/p2j/oo/reflect/AccessMode.java
modified src/com/goldencode/p2j/oo/reflect/DataType.java
added src/com/goldencode/p2j/oo/reflect/Event.java
added src/com/goldencode/p2j/oo/reflect/Flags.java
renamed src/com/goldencode/p2j/oo/reflect/Method.java => src/com/goldencode/p2j/oo/reflect/Legacy4GLMethod.java
added src/com/goldencode/p2j/oo/reflect/LegacyConstructor.java
added src/com/goldencode/p2j/oo/reflect/Parameter.java
added src/com/goldencode/p2j/oo/reflect/ParameterMode.java
added src/com/goldencode/p2j/oo/reflect/Property.java
added src/com/goldencode/p2j/oo/reflect/Variable.java

Although the HashAlgorithmEnum and UriEncodingTypeEnum have been modified in later 4384b versions, I think the versions in 4231b are correct. In addition, I made many corrections to the code formatting etc... in the versions checked into 4231b so these should be versions that remain after 4384b is rebased from 4231b.

I think it is probably a good time to pull the recent changes from 4384b into 4231b and reset 4384b.

Marian: from there, your team should be able to move ahead with enums pretty easily. My only concern is that I've not yet tested the runtime and I would be very surprised if there are no bugs. So if that is going to get in the way, then we may need to wait to reset 4384b until after any runtime fixes are made.

Constantin: I would like you to implement any changes needed for ControlFlowOps, as was discussed in #4349-11. I really am not clear on what is needed.

#70 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

Marian: from there, your team should be able to move ahead with enums pretty easily. My only concern is that I've not yet tested the runtime and I would be very surprised if there are no bugs. So if that is going to get in the way, then we may need to wait to reset 4384b until after any runtime fixes are made.

Greg, the current version we have is missing implementation of getEnum methods so we already have limited runtime support, we should probably do the rebase and with that we can run some tests and see if we find anything.

#71 Updated by Greg Shah almost 4 years ago

Marian: Is 4384b missing any changes from your team?

Constantin: Please merge the 4384b changes into 4231b, avoiding the list of files from #4349-69. Then 4384b can be reset from 4231b.

#72 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

Constantin: Please merge the 4384b changes into 4231b, avoiding the list of files from #4349-69. Then 4384b can be reset from 4231b.

I'll need to rebase before merging the changes. Is the #4349-69 list of files meant to be 'dead' in 4384b, and their current 4231b version survive?

#73 Updated by Greg Shah almost 4 years ago

Is the #4349-69 list of files meant to be 'dead' in 4384b, and their current 4231b version survive?

Yes, exactly this.

#74 Updated by Greg Shah almost 4 years ago

Implementation of the built-in OO class Progress.Util.EnumHelper is not needed because it is only used for .NET enums which won't exist in the converted system. (This is just a note to remember that idea.)

#75 Updated by Greg Shah almost 4 years ago

It seems like ObjectOps.WorkArea.legacyNames and ObjectOps.WorkArea.name2cls are both truly static data. It seems that the only reason these would need to be context-local is if we were to support the same fully qualified legacy class name mapped to more than 1 converted class. Such an approach would only work based on dynamic propath differences at runtime. But I think we would need to make other changes to support that idea. And it is a very bad practice, so I wasn't planning to support it.

Anyway, by moving those two data structures into true static data, it can make it possible to lookup the enum superclasses without a constructor registration. I could change the code to register them in every context as soon as the first enum class is loaded but it would be a bit messy that way.

Constantin: What is your opinion?

#76 Updated by Constantin Asofiei almost 4 years ago

Greg Shah wrote:

It seems like ObjectOps.WorkArea.legacyNames and ObjectOps.WorkArea.name2cls are both truly static data. It seems that the only reason these would need to be context-local is if we were to support the same fully qualified legacy class name mapped to more than 1 converted class. Such an approach would only work based on dynamic propath differences at runtime. But I think we would need to make other changes to support that idea. And it is a very bad practice, so I wasn't planning to support it.

Yes, we can't support duck-typing without getting rid of the 'true inheritance' in the conversion. We would need to implement our own 'dynamic inheritance' and polymorphism, to make that part work.

Anyway, by moving those two data structures into true static data, it can make it possible to lookup the enum superclasses without a constructor registration. I could change the code to register them in every context as soon as the first enum class is loaded but it would be a bit messy that way.

I'm OK with making them context-local static, as long as:
  • the changes takes care of the concurrent access to these maps.
  • the context-local class initialization (the 'static associated c'tor' of the converted class) is still executed properly. You need to take care to not short-circuit this when, for example, class Foo (with a static c'tor) was registered in legacyNames or name2cls by context A, and context B wants to initialize it, too (don't forget that the static c'tor code in 4GL is always context-local and needs to be called by each context when first accessing that class).

#77 Updated by Greg Shah almost 4 years ago

  • % Done changed from 0 to 100
  • Status changed from WIP to Review

4231b rev 11585 completes the enum implementation. With the fixes in this revision, all of the oo/enums/*.p test programs convert, compile and run successfully. This includes all functionality documented in this task except for anything in ControlFlowOps. It is not clear if the CFS stuff is actually needed.

The other thing that is not done is serialization, but I would plan for that to be done when OO serialization is implemented in general (#4658).

Constantin: Please review.

#78 Updated by Constantin Asofiei almost 4 years ago

Review for 4231b rev 11585:
  • ObjectOps.registerClass - now the method is a no-op, the registerClass(clsName, cls); call is not longer there.

Otherwise, the changes look good.

#79 Updated by Greg Shah almost 4 years ago

  • ObjectOps.registerClass - now the method is a no-op, the registerClass(clsName, cls); call is not longer there.

registerClass(clsName, cls); is used heavily in om.goldencode.p2j.oo.* as well as from object.initialize().

I'm removing the one in object.initialize() since it is not needed (the getLegacyName() does it internally). But the other ones are still used. If they can be removed, then we should do so.

#80 Updated by Constantin Asofiei almost 4 years ago

Greg, I meant the public static void registerClass(Class cls, boolean hierarchy). I can't find any calls for this anymore, but Marian found at least a case which required it. And its body is missing the registerClass(clsName, cls); call.

#81 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

  • ObjectOps.registerClass - now the method is a no-op, the registerClass(clsName, cls); call is not longer there.

registerClass(clsName, cls); is used heavily in om.goldencode.p2j.oo.* as well as from object.initialize().

I'm removing the one in object.initialize() since it is not needed (the getLegacyName() does it internally). But the other ones are still used. If they can be removed, then we should do so.

The usage in p2j.oo was mainly in "builders" and to assert is type, for that getLegacyClass that takes a character/string parameter was used along with static string properties for various class/interface names used. Since resolveClass doesn't do much of resolving unless the class was already registered someone must have thought registerClass is the cure. After all the generated code does a lot of registerClass as much as I can tell. The real issue is probably with resolveClass, one should be able to get the class for name if that exists in PROPATH, regardless if it was registered or not before.

#82 Updated by Marian Edu almost 4 years ago

Constantin Asofiei wrote:

Greg, I meant the public static void registerClass(Class cls, boolean hierarchy). I can't find any calls for this anymore, but Marian found at least a case which required it. And its body is missing the registerClass(clsName, cls); call.

A new method is used instead - getAssignableFrom that does not walk through the whole hierarchy but stop as soon as it finds the class/interface we're interested in. The use case was for resolving method signature when the qualified parameter used in LegacySignature was a class/interface that was not already registered - ControlFlow.resolveLegacyEntry. In our case the input parameter was defined as an interface but an actual implementation was passed and there were no variable defined of that type (as suggested by Constantin that should have taken care of registration).

Again, if resolveClass is fixed then there will be no need of doing all that manually.

#83 Updated by Marian Edu almost 4 years ago

Greg Shah wrote:

  • ObjectOps.registerClass - now the method is a no-op, the registerClass(clsName, cls); call is not longer there.

registerClass(clsName, cls); is used heavily in om.goldencode.p2j.oo.* as well as from object.initialize().

I'm removing the one in object.initialize() since it is not needed (the getLegacyName() does it internally). But the other ones are still used. If they can be removed, then we should do so.

Removed all use of ObjectOps.registerClass from p2j.oo package, it's now only used from within ObjectOps and object ctor as far as I can see. As it is now the call in ObjectOps.getLegacyClass is a nop since resolveClass will never resolve anything that was not already registered and it it was then what is the point of calling it again.

The resolveClass needs to be completed to search through propath as for external procedures, there is one catch here where the class name in FWD is different as of that in 4GL so that mapping should be considered first before looking through the file system.

#84 Updated by Greg Shah almost 4 years ago

  • Status changed from Review to Closed

Task branch 4231b has been merged to trunk as revision 11347.

#85 Updated by Marian Edu almost 3 years ago

Greg Shah wrote:

Task branch 4231b has been merged to trunk as revision 11347.

One issue there with flag enums, logical operators (and, or, not, xor) should return the same enum type so most probably the same processing as for getEnum methods should be taken for those - aka, take those out of FlagsEnum and implement them in each flags enum class. Or maybe use cast during conversion but either way the result should of that specific enum type not FlagsEnum as it is now cause the generated code does not compile as-is.

Simple test case in oo/enums/test_flag_enum_bitwise_ops.p:

oo/enums/Permissions.cls
oo/enums/FlagParams.cls
oo/enums/test_flag_enum_bitwise_ops.p

#86 Updated by Greg Shah almost 3 years ago

One issue there with flag enums, logical operators (and, or, not, xor) should return the same enum type so most probably the same processing as for getEnum methods should be taken for those - aka, take those out of FlagsEnum and implement them in each flags enum class. Or maybe use cast during conversion but either way the result should of that specific enum type not FlagsEnum as it is now cause the generated code does not compile as-is.

This is resolved in 3821c revision 12684. I inserted a cast for the return value of all bitwise operators.

Also available in: Atom PDF