# SymbolResolver.java

    /*
    ** Module   : SymbolResolver.java
    ** Abstract : manages the symbol dictionaries and lookup process
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
    ** 001 GES 20041104   @18650 First version, with support for resolving:
    **                           - optionally abbreviated keywords
    **                           - variables with multiple scoping levels
    **                             including a special global scope
    ** 002 GES 20041115   @18651 Added support for:
    **                           - functions
    **                           - procedures
    **                           - labels
    ** 003 GES 20041208   @18937 Added new methods for lookup of variables
    **                           and functions.  These new methods are now
    **                           using the old "default" names and the old
    **                           methods that handle exact matching are under
    **                           new names lookup*Exact().  The new methods
    **                           implement an algorithm that checks to see
    **                           if a match is made to a language keyword,
    **                           if such a match exists, a 2nd level of
    **                           processing occurs. Otherwise the processing
    **                           is the same as before.  This new algorithm
    **                           is necessary to allow built-in functions and
    **                           variables to be properly resolved, since
    **                           these symbols are keywords which must support
    **                           the reserved/unreserved attribute and the
    **                           matching must be done considering
    **                           abbreviations.
    ** 004 GES 20041217   @19041 Added a first pass implementation for the
    **                           lookup of database objects (databases,
    **                           tables and fields) in both unqualified
    **                           and partially/fully qualified forms.  In
    **                           addition, table and field abbreviations are
    **                           fully supported.  Warning: table support is
    **                           hard coded at this time.
    ** 005 NVS 20041225   @19109 Now the label dictionary is scoped, with one
    **                           label per scope (Progress allows only one
    **                           label per block). Modified addLabel creates
    **                           new scope implicitly. Added deleteLabel()
    **                           method which deletes the current scope.
    ** 006 ECF 20050103   @19188 Reimplemented lookupDatabase, lookupTable,
    **                           and lookupField methods to handle exceptions
    **                           thrown by SchemaDictionary. In each case, if
    **                           an exception is caught, error information is
    **                           written to stdout and -1 is returned as the
    **                           token type.
    ** 007 GES 20050103   @19192 Added isTable and isField methods which
    **                           don't throw exceptions. This allows the
    **                           parser to test things that may be ambiguous
    **                           without being hit over the head and left
    **                           bleeding in the gutter.
    ** 008 GES 20050107   @19270 Widgets are handled just like variables
    **                           (they share the same namespace) in Progress.
    **                           However due to the fact that some symbols
    **                           do "double duty" as both variables and as
    **                           widgets, a separate widget namespace is
    **                           kept so that the generic one name to one
    **                           token type mapping can be kept for the
    **                           majority of the standard name lookups. A
    **                           new widgetDict member is added and two
    **                           methods (addWidget and lookupWidget) are
    **                           provided to provide access.  This new
    **                           namespace does participate in the scoping
    **                           just like variables.
    ** 009 GES 20050118   @19336 Added support for modifying the schema
    **                           namespace (adding tables, fields and lists
    **                           of fields).
    ** 010 GES 20050119   @19346 Added support for record scoping (driving
    **                           the schema's scopes and which tables are
    **                           promoted to the current scope).
    ** 011 GES 20050127   @19449 Added new namespaces for frames, menus (+
    **                           sub-menus) and menu-items.
    ** 012 GES 20050128   @19460 Added new namespace for streams.
    ** 013 GES 20050201   @19513 Added additional parser token type to
    **                           schema dictionary data type conversions
    **                           (for variable types like VAR_INT). Also
    **                           added dump support for the schema dictionary.
    ** 014 GES 20050204   @19570 Added loadSchemaDatabase() method to load
    **                           an arbitrary non-default database into the
    **                           schema namespace.
    ** 015 GES 20050207   @19624 Removed deprecated method call to schema
    **                           namespace.
    ** 016 GES 20050209   @19737 Modified addTable() method to allow user
    **                           control over where a table is created,
    **                           either in the global scope or in an internal
    **                           procedure or function scope (buffers only).
    ** 017 GES 20050210   @19749 Added the ability to add a variable using
    **                           data contained in a wrapper class. This may
    **                           be extended later to store more data in the
    **                           dictionary than is possible today. In
    **                           addition a new method of adding lists of
    **                           variables, widgets and frames to the
    **                           respective dictionaries was provided. Also
    **                           added a method for adding aliases to the
    **                           schema namespace.
    ** 018 GES 20050218   @19860 Minor improvements to the loading of
    **                           variables, to allow fields and a special
    **                           token type of DYNAMIC to be loaded by the
    **                           same mechanism.
    ** 019 GES 20050307   @20217 Added methods to lookup the fully qualified
    **                           name of a field or table.
    ** 020 GES 20050331   @20561 Major additions to store options at the
    **                           time that variables and temp-tables are
    **                           defined. These options are saved in the
    **                           variable dictionary in the case of vars and
    **                           are pushed out to the schema dictionary in
    **                           the case of temp-tables. All variable and
    **                           temp-table definitions that are LIKE another
    **                           variable, field or table now honor and
    **                           bidirectionally pass the options as needed.
    **                           Variable and field references (the lvalue
    **                           rule) store annotations for non-default
    **                           options. WARNING: INITIAL, LABEL and
    **                           VIEW-AS are 3 common options that are not
    **                           processed or are only partially processed
    **                           at this time.
    ** 021 ECF 20050331   @20613 Implemented all schema dictionary hooks added
    **                           for 020 (@20561).
    ** 022 GES 20050401   @20596 Added support for a temporary index to allow
    **                           the linking of a variable reference to its
    **                           original definition at parse time. This is
    **                           needed because project-unique IDs can't be
    **                           easily generated at parse time, but are only
    **                           added after the entire tree is complete.
    ** 023 GES 20050406   @20651 Massive rewrite to use ASTs to store variable
    **                           options and to exchange ASTs with the schema
    **                           dictionary and fully handle field and
    **                           variable options.  In addition it is now
    **                           possible to instantiate this class in a
    **                           manner that bypasses the schema dictionary.
    **                           This allows the ProgressLexer to be used
    **                           without dependencies upon an already
    **                           functional schema.
    ** 024 ECF 20050413   @20701 Implemented all schema dictionary hooks which
    **                           needed to be modified as a result of 023
    **                           (@20651). TODO: still need to handle VALIDATE
    **                           keyword.
    ** 025 GES 20050414   @20723 Converted function processing into a wrapped
    **                           approach that allows storing other attributes
    **                           and providing annotation helpers.  This
    **                           required some rework of the variable/field
    **                           processing.
    ** 026 GES 20050509   @21132 Modified annotateVariableOptions() to be
    **                           safe when called by the class
    **                           ProgressExpressionEvaluator which short-
    **                           circuits the normal variable dictionary
    **                           processing and thus can generate nulls
    **                           where they are normally not possible.
    ** 027 GES 20050822   @22187 Support schema dictionary record type
    **                           lookups for fields and buffers.  This
    **                           reports the token type of the backing
    **                           construct (table, temp-table or work-table).
    ** 028 GES 20050830   @22431 Added attribute/method namespace support.
    ** 029 GES 20050922   @22778 Support schema dictionary buffer name
    **                           lookups for fields and records.  This is
    **                           sometimes (often) different from the fully
    **                           qualified schema name and is needed to know
    **                           which buffer this reference is uniquely
    **                           referencing.
    ** 030 GES 20051004   @22931 Added database name lookup for tables (this
    **                           is sometimes different from the fully
    **                           qualified schema name).
    ** 031 GES 20051013   @23058 Moved field annotation here from parser and
    **                           added helper to create a field node based
    **                           on an unambiguous field name.
    ** 032 GES 20051109   @23282 Added more annotations and tempidx support
    **                           for non-var widgets (buttons/browse) by
    **                           using the wrapper approach.
    ** 033 ECF 20060425   @25740 Enhanced support for define temp-table. A
    **                           field can be like a specific element of an
    **                           extent field.
    ** 034 ECF 20060427   @25823 Enhanced support for defining a variable like
    **                           a database field. Enabled specification of a
    **                           specific element of an extent field as the
    **                           model field.
    ** 035 GES 20070329   @32655 Extended the concept in H034 to all forms of
    **                           variable and field definition using another
    **                           variable.
    ** 036 GES 20070404   @32793 Pass through a marker when adding a scope to
    **                           the schema dictionary. This allows the new
    **                           scope to be associated with the fact of
    **                           whether the scope belongs to an internal
    **                           procedure, user-defined function or trigger.
    ** 037 GES 20070418   @33112 Added support for COM-HANDLE types.
    ** 038 GES 20070528   @33788 Added access to function definition object.
    ** 039 GES 20070629   @34346 Changed APIs to pass through enough ASTs
    **                           to allow the schema dictionary to set the
    **                           line and column info for temp-table ASTs
    **                           created during parsing.
    ** 040 GES 20070907   @35010 Support for 10.1B data types.
    ** 041 GES 20070911   @35138 Support for 10.1B class and package
    **                           namespaces.
    ** 042 GES 20070923   @35184 Added removeKeyword().
    ** 043 GES 20070927   @35257 Modified promotion behavior to match new
    **                           interfaces in the schema dictionary. Added
    **                           table promotion when qualified field
    **                           references are encountered.
    ** 044 GES 20071012   @35529 Added separate namespaces for dataset,
    **                           data-source and data-relation resources.
    ** 045 GES 20071107   @35900 Made most schema related methods safe to
    **                           use with a null schema dictionary. This
    **                           allows the parser to operate on a wide range
    **                           of expressions without any knowledge or
    **                           dependency upon the schema dictionary. This
    **                           is important in cases of preprocessor
    **                           expression evaluation, where text may need
    **                           to be parsed as a symbol instead of as a
    **                           field name (but if the schema dictionary
    **                           was loaded, the preprocessor symbol would
    **                           resolve as a field which is an invalid
    **                           result).
    ** 046 GES 20071129   @36187 Removed support for DYNAMIC hints (no longer
    **                           needed).
    ** 047 GES 20071207   @36286 Signature changes to pass more data to the
    **                           schema dictionary. Also added a global flag
    **                           to enable/disable table promotion.
    ** 048 ECF 20080704   @39145 Minor memory optimization. Use Boolean
    **                           constants instead of instantiating new
    **                           Boolean objects.
    ** 049 GES 20090429   @42065 Match package and class name changes.
    ** 050 GES 20110712          Made failure to find a class abend more gracefully
    **                           (and provide more useful data).
    ** 051 GES 20110811          Added simple list of handles that could be possible
    **                           active-x targets. This should probably be a real
    **                           scoped dictionary, but for a start it should handle
    **                           virtually all cases.
    ** 052 GES 20110822          Added support for loading built-in 4GL OO classes/
    **                           interfaces and .NET classes/interfaces. Added
    **                           static support. Added support for define event
    **                           when loading a class via AST. Events are treated
    **                           like properties and vars (they share the same
    **                           namespace). Reworked class loading for prescanning
    **                           and to improve performance.
    ** 053 GES 20111005          Added safety code to avoid class loading issues
    **                           when the OO skeleton class directories don't exist.
    ** 054 CA  20130225          Annotate the fields with the db name.
    ** 055 CA  20130307          Fixed abbreviated field name disambiguation - the tables which are
    **                           weak reference in the current scope have precedence over others.
    ** 056 CA  20130307          When multiple there are multiple definitions for the same var, only
    **                           the first one has its tempidx set and is saved in the dictionary.
    ** 057 CA  20130307          Fixed honoring of VALIDATE clause for temp-table fields defined using
    **                           the LIKE clause and tables defined using the LIKE clause.
    ** 058 GES 20130317          Added support for the USING clause when loading from an AST.
    ** 059 CA  20131029          All the static info is kept context-local, for runtime query
    **                           conversion support.
    ** 060 OM  20131205          Fixed addLocalVariableLike() by creating the new variable with exact
    **                           type as the variable passed as parameter.
    ** 061 GES 20140129          Fixed an exists() use case (this is also an optimization).
    ** 062 GES 20140428          Added tryLoadClass() and differentiated the builtin annotation for
    **                           classes as builtin-cls. Fixed OO lookup logic to match changes in
    **                           ClassDefinition processing.  Added methods to more properly support
    **                           the keyword ignore list.
    ** 063 GES 20141125          Added caching functions to allow local vars to be restored in
    **                           function defs that use the 4GL feature of making their parms list
    **                           optional when a forward function def has already specified the
    **                           parms.
    ** 064 VIG 20141104          Added menu support.
    ** 065 ECF 20151030          Added methods to retrieve schema ASTs for database tables and
    **                           fields, and to manage field widget references by frame.
    ** 066 GES 20160224          Implemented a configuration parameter to allow overriding the
    **                           default location for skeleton classes. Complete rewrite of schema
    **                           dictionary resolution to handle the dynamic lookup in the class
    **                           hierarchy of protected buffers and temp-tables. Added support for
    **                           query resources. Queries, datasets and data-sources are now
    **                           looked up in the class hierarchy too.
    ** 067 ECF 20170303          Honor FOR TEMP-TABLE clause in DEFINE BUFFER/PARAMETER statements.
    ** 068 CA  20170523          Fix parsing of implicit Progress.Lang.Object super-class inheritance.
    ** 069 GES 20170706          Added utility functions used to generate the class map.
    **     GES 20170708          When not in silent mode, will print the class name of any
    **                           recursively parsed class file.
    **     CA  20170711          1st level OO class scanning is decoupled from full parse: first, all
    **                           references from members or inheritance are resolved, for the entire
    **                           graph, and only after that the reached classes are parsed.
    **     CA  20170721          Remove all referenced classes/interfaces loaded during a 1st level
    **                           scan, if it fails when loading a class.
    **     CA  20170724          When pre-parsing a class, failures to resolve a class starting with
    **                           the second or subsequent parsing level will result in 'sink-ing' all
    **                           references to the missing class, and not fail.  Failures will be
    **                           emitted only if the currently parsed class has a direct reference to
    **                           a missing class.  This is to emulate the 4GL compiler, which doesn't
    **                           resolve the entire class reference graph, but only the first level;
    **                           the compromise is needed because FWD requires to resolve all class
    **                           references (members and types) at parsing time.
    **     CA  20170725          When processing the method block during first level parse, try to
    **                           resolve the expected type of the data member from the currently
    **                           processed class first.
    **     CA  20170804          processHierarchy first checks the real class hierarchy if
    **                           ignoreOORefs is set.
    ** 070 OM  20170802          Dropped possible ActiveX detection. Removed redundant casts.
    ** 071 CA  20170825          Initial support for USE-DICT-EXPS: not fully complete for the HELP
    **                           behaviour.
    ** 072 CA  20171010          The key for a frame field AST is schemaname$bufname, and it must be
    **                           used to collect all unique frame fields in addFramefields.
    **                           Fixed convertSeparator() when used from Windows OS.
    ** 073 GES 20171017          Added hexadecimal literal support.
    ** 074 GES 20180303          Fixed javadoc.
    **     HC  20180312          Fixed lookup of STATIC flag of class data members and methods. The
    **                           lookup didn't work correctly during class prescan mode.
    **     CA  20180316          MOCK_CLASS_DEF lookupMethod and lookVariable APIs will check for a
    **                           defined var: if so, it will return -1, as it doesn't match a class
    **                           member, but only if localLookup flag is set.
    **     HC  20180319          Fixed an NPE when a referenced class is not defined (i.e. the class file not
    **                           found). In this case compiler error must be issued.
    **                           Fixed resolution of static class members.
    **     HC  20180320          Added support for referencing .NET inner classes (i.e. namespace.Outer+Inner).
    **     CA  20180404          CAN-FIND allows field abbreviations to be resolved even if ambiguous:
    **                           it will return the first field which matches the prefix.
    **                           DEF BUFFER b FOR TEMP-TABLE t. may target either a temp-table or a
    **                           physical table (temp-table has precedence).
    ** 075 CA  20180410          Unqualified fields referred in BY sort order clauses will look up
    **                           with precedence the current table: if BY clauses are just fields and
    **                           they match an index in the current table, then that will be used.
    **     CA  20180412          Do not fail if we can't parse a class which is reached in
    **                           pre-scan-mode, to mimic the 4GL's lenient compile behaviour, where
    **                           class references in i.e. OO method bodies are resolved and compiled
    **                           at runtime, and not via COMPILE statement.
    **                           Fixed resolveClassName when classes are (partially) qualified and
    **                           also to address missing classes, at callgraph build.
    **     GES 20180415          Changed signatures to allow passing a sequential flag. The rest of
    **                           CREATE-LIKE-SEQUENTIAL is left as a TODO.
    **     GES 20180425          Made failures generate a warning in addIndexFrom() instead of being
    **                           fatal, since the 4GL allows garbage to be specified (and silently
    **                           accepts it).
    **     ECF 20180422          Removed class-level synchronization and static initialization of
    **                           context-local resource.
    **     CA  20180510          Allow the table options to be emitted to the associated .dict file.
    ** 076 CA  20180621          Added promoteAllTables.
    ** 077 CA  20181029          Reworked some APIs related to registration of attribute/methods.
    ** 078 CA  20180927          annotateClassRef should try to load the class, if not found.
    **                           Package names with '.*' support are not reported as abend, if they
    **                           do not exist.
    **     GES 20181030          Added inherits clause support for interfaces.
    **     GES 20181031          Ignore USING stmts that have duplicate base names.
    **     GES 20181106          Mark built-in global variables with a builtin annotation so they can
    **                           be distinguished later.  Simplified addDataMember() and fixed the
    **                           default access mode of define variables to PRIVATE.
    **     CA  20181113          lookupDataMember will look for the member in the currently parsed
    **                           class def - if not found, will default to the 'mock def'.  This is
    **                           useful only in prescan mode, when we don't have the entire class
    **                           parsed.
    **     GES 20181117          Reworked the pre scan class processing to use a cut down parser
    **                           which eliminates the need for the ignoreOORefs approach and the
    **                           related patches.  These have all been removed.
    **     CA  20181120          Removed the class_map.xml approach, as this does not allow multiple
    **                           classes (with the same qualified name) to exist in different source
    **                           folders - PROPATH is used instead.
    **                           Interfaces will allow access to the Progress.Lang.Object methods.
    **     CA  20181121          System.Object is the root class for all .NET classes.
    **                           Un-indexed .NET-style array variable references must act as
    **                           System.Array.
    **     GES 20181122          Fixed the association of data members with the chained instance from
    **                           which it was parsed.
    **     CA  20181123          lookupWrapped - if the text string is not a built-in var or function
    **                           name, then fallback to looking for a variable - as 4GL allows
    **                           definitions of OO data members having a reserved keyword like 'Back'.
    ** 079 GES 20181129          Associate the full filename with any new class or interface def.
    **     CA  20181213          Fixed DEF BUFFER FOR [TEMP-TABLE] - TEMP-TABLE forces a temp-table,
    **                           and without it a persistent table must have precedence over a temp
    **                           table.
    **                           Annotate the USING class reference with the class info (and prescan
    **                           if needed).
    **     GES 20181216          Added method_def tempIdx support.
    ** 080 CA  20190219          tempIdx support is added to constructors, too - so that NEW can
    **                           reference the exact definition via the refid annotation.
    **     OM  20190322          Fixed relative path of mapped classes.
    ** 081 OM  20190415          Added DataSet and DataSource support.
    ** 082 CA  20190423          Missing OO 4GL imports are not emitted.
    **                           Misc OO dismabiguation fixes, fixed 'var leak' during pre-scan and
    **                           tempidx collisions from pre-scan and normal parsing (tempidx for OO
    **                           is maintained at the ClassDefinition).
    ** 083 CA  20190520          Solved converted method names for override and overload cases.
    **                           The 'provisional' members are required to be resolved once the
    **                           initial pre-scan of a class has finished, by doing a full-parse on it
    **                           and any super-classes or interfaces.  This is required because at a
    **                           time a class is referenced, it may not have been by FWD's normal
    **                           processing, and we still need the provisional members solved.
    ** 084 CA  20190604          Fixed a typo for LENGTH-OF name.
    ** 085 SVL 20190614          Support for LIKE-SEQUENTIAL.
    ** 086 ECF 20190620          EmptyIterator API change.
    ** 087 OM  20190611          Improved support for before and after temp-tables.
    ** 088 GES 20190518          Added strong scope marking for the schema dictionary.
    **         20190619          Added multiple inheritance (only needed for interfaces).
    **         20190620          Added enum support.
    **         20190622          Fixed processHierarchy() which was broken with multiple inheritance
    **                           changes.
    **         20190624          Expanded standard class reference annotations in annotateClassRef()
    **                           and fixed tempIdx processing for enum defs.
    **         20190625          Removed previous .NET dependency tracking approach.
    **         20190710          Fixed dump of .NET dependency report.
    **     ECF 20190717          Qualify table names with database name in promoteAllTables.
    **     RFB 20190912          Added a new constructor which allows SymbolResolver to bypass
    **                           initializing symbol dictionaries. This increases efficiency.
    **                           Currently relex in schema.g is the only place using this.
    **     RFB 20190913          Added new method srchPropathFindFile which handles partially
    **                           qualified class names by prepending the PROPATH entries before
    **                           calling the original findFile.
    **     RFB 20190913          Enabled check in loadClass for preScanRefs to prevent recursive
    **                           death. Similarly, in addPackage, we prevent a call to
    **                           annotateClassRef if classes are "using" each other
    **     GES 20190823          Added support for direct Java OO references/usage from 4GL code.
    **     RFB 20191008          translateType receives null parm when there isn't any need to dive
    **                           deeper to determine the data type. We need to protect the jclass
    **                           check if null is passed.
    **     GES 20191010          Added dbtype() builtin function for expression evaluator use.
    **     GES 20191012          Check for LBRACKET after finding the correct node of the sub-expr
    **                           in readExtent().
    **     GES 20191015          Removed debugging code.
    ** 089 RFB 20200214          Allow for "qualified" annotation in pre-scan, since there are several
    **                           downstream scenarios when it's depended up for knowing the class name.
    **                           One warning is that we don't know whether it's a Java or Progress
    **                           class, so we are assuming it's Progress, and converting to lowercase.
    **                           In calculateParents we are giving up the ghost if this annotation is
    **                           not found, since it would lead to NPE.
    **                           Also made some log message formatting changes, with respect to the
    **                           recursion during pre-scan.
    **     CA  20200219          Fixed class type (OE, Java, .NET) resolution in resolveClassName.
    **     GES 20200301          Added NameNode support to some field-level lookups.
    **     GES 20200302          Changes to use findFieldInfo() and handle null returns.
    **     GES 20200108          Added support for direct usage of fully qualified Java class names
    **                           without requiring a USING statement first.
    **     CA  20200304          Added support for USING com.foo.Bar FROM JAVA (qualified class name).
    ** 090 CA  20200412          Added incremental conversion support.
    ** 091 CA  20200503          Added support for accumulators referenced by their default variables,
    **                           like TOTAL, COUNT.  This requires tracking the inner block scopes, and
    **                           these accum builtin variables has precedence over an outer defined
    **                           variable.
    **                           Lookup for chained object members must always be done at the class
    **                           members, and not local variables.
    **     GES 20200522          Removed class name map file name (dead code).
    **     CA  20200529          Fixed an issue for incremental conversion, when the AST for a skeleton class
    **                           (which was parsed in a previous conversion) does not exist.
    ** 092 CA  20200718          ConversionData must not be used in runtime conversion mode.
    ** 093 IAS 20200624          Added checks for <code>null</code>
    ** 094 CA  20200804          Emit bulk setter and getter for an extent property.
    **     CA  20200930          Use an exemplar instance to pre-populate the dictionaries at runtime conversion.
    **     CA  20210113          If a implemented skeleton's Java method name collides with one part of
    **                           NameConverter.isInternalMethodName's, fail immediately, as this means the
    **                           implemented skeleton method's Java name is not following NameConverter's rules.
    **     CA  20210203          Fixed conversion issues with OUTPUT/INPUT-OUTPUT for extent or scalar parameters,
    **                           involved in dynamic or static OO calls, functions or procedure calls.  This
    **                           includes mostly cases for OO properties/variables (static and non-static).
    **     OM  20210219          lookupWorkerExact returns -1 if null dictionary is provided instead of NPE.
    **     GES 20210610          Enabled lookup of .NET inner class names (and their associated filenames) which
    **                           have an embedded '+' character between the containing class and the inner class
    **                           name. For example, the qualified name some.package.ContainingClass+InnerClass
    **                           and its matching filename some/package/ContainingClass+InnerClass.cls are now
    **                           supported cases. Changed how variable instances are created. Added support for
    **                           the undocumented .NET enum value__ member.
    **     GES 20210715          Added loadConvertedClassByQname().
    **     CA  20210920          Added NPE protection for some registries, for runtime conversion.
    **     HC  20211001          Implementation of i18n support.
    **     OM  20211122          Added hints-based support for 'unloading' schema.
    **     CA  20220113          Widget lookup must not be performed (and return 'no match') if the closest
    **                           definition is a variable which can't be a widget (HANDLE, object, etc).
    **     CA  20220128          Added an abend protection and log for annotateObjectMethod(), seen in incremental
    **                           conversion, but with no local duplicate.
    **     ME  20211026          Added support for POLY data type.
    **     GES 20220309          Accumulator variables must be propagated to the parent/containing block. In the
    **                           4GL such implicit variables outlive their inner block scope.
    **     RFB 20220325          preferDefaultBuffer is passed through annotateField and lookupFieldInfo to
    **                           findFieldInfo so the default buffer can be considered. Ref #4791.
    **     GES 20220404          Reduced excessive logging.
    **     TJD 20220504          Java 11 compatibility minor changes
    **     CA  20220408          Ensure that the class hierarchy is reloaded properly when initial loading
    **                           was due to references in a procedure.
    **                           Fixed 'translateType' for TABLE parameters.
    **                           BUFFER definitions are tracked only in preScan or if they are not defined in a
    **                           method.
    **                           Annotate THIS-OBJECT and SUPER constructor call parameters.
    **     CA  20220429          Abort parsing if the parent class can't be found in 'parseHierarchy'.
    **     CA  20220513          Added isBuiltinFunction(String).
    **     CA  20220516          Fixed tracking of extent data members (variables or properties).
    **                           Indexed extent property setters have the index as int64 (and this is forced for
    **                           explicit indexed setters, too).
    **     GES 20220502          Added lookupTableInfo() to reduce the number of name node lookups. Reworked
    **                           translateType() to properly handle buffers, tables, datasets, table-handles,
    **                           dataset-handles and objects. Reworked method name processing and how we track
    **                           methods so that converted method name conflicts are resolved within the class
    **                           hierarchy. This minimizes the number of conflicts since previously the
    **                           disambiguation was done at a project level.
    **     CA  20220531          During prescan, always track the skeleton OO property or events.
    **                           Always track the default constructor, as it is not mandatory to be defined in the
    *                            class. Progress.Lang.Object must be loaded before any enum is loaded, otherwise
    **                           there will be circular references. OVERRIDE option at PROPERTY or METHOD is not
    **                           mandatory when overriding an interface method/property.
    **                           Fixed misc issues related to calculating the call signature or type translation.
    **     CA  20220727          Improved memory management for parsing; cleanup is done in two phases:
    **                           1. after each legacy class file has finished parsing, the SchemaDictionary will
    **                              keep only protected temp-tables (all private tables are removed, as these
    **                              can't be reached from sub-classes; all permanent tables are removed, as each
    **                              class has its own reference to the permanent tables).  Also, the class def
    **                              instance will reduce its own used memory, which is not required when parsing
    **                              sub-classes.
    **                           2. after parsing of the entire file set is finished, any SchemaDictionary or
    **                              other ASTs referenced by the ClassDefinition are released.
    **     CA  20220818          An abstract property can define only a getter or a setter, while the implementation
    **                           can define both.  For this reason, only the overridden accessor must have the
    **                           @Override annotation.
    **     TJD 20230220          Implement isExactMatchField to allow checking exact field names
    ** 095 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 096 OM  20230115          Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
    **                           versions, based on node types rather on string paths.
    ** 097 CA  202302426         For method arguments, mark those which are from an expression (i.e. using
    **                           operators).
    ** 098 CA  20231004          Fixed conversion for class events with parameters of type DATASET, TABLE,
    **                           DATASET-HANDE, TABLE-HANDLE and BUFFER.
    ** 099 CA  20230929          Method chain calls starting with SUPER keyword can not be morphed to dynamic
    **                           invoke, as the super calls require JVM to handle them.
    **     CA  20231007          Arguments for direct method calls which can't be emitted as direct java calls
    **                           (as they are considered 'dynamic poly') must be wrapped in a 'polyArg' to check
    **                           the type.
    ** 100 CA  20231019          Show linkage errors when resolving Java class names.
    ** 101 OM  20240305          Added [word-indexed] annotation on word indexed fields.
    ** 102 OM  20240606          Fixed [word-indexed] for dynamic case. Local optimisations.
    ** 103 GBB 20240826          Moving FileSystemDaemon to osresource package.
    ** 104 CA  20241026          Fixed .NET arrays property accessed via [].
    **                           Fixed an issue with circular references when parsing classes: if a parent is
    **                           already 'pre-scanned', avoid parsing the class and let it parse when it's reached
    **                           again.
    **                           The parent of .NET enums is System.Enum.
    **                           The '+' in .NET inner class names must be replaced with File.separatorChar.
    **                           Class hierarchy in 'processed' must be calculated when the parse finishes for the
    **                           file, and not when 'tryLoadClass' is called.
    **     CA  20241104          In 'tryLoadClass', force throwing an exception if the class can't be loaded.
    **     AOG 20241111          Changed addDataMember to look directly in varDict for class member.
    **     CA  20250225          'annotateNumericLiteral' must move a suffix '+' or '-' to the beginning, so it can
    **                           be parsed.
    ** 105 CA  20250319          An worker can be used before the 'qualified_oo_name' is set in annotation
    **                           (like in schema conversion phase), so ensure we can load the ClassDefinition.
    **     CA  20250319          A provisional class var found during pre-scan, which is referenced before the
    **                           definition statement and is referenced via a chain, must be resolved even if is
    **                           provisional - this is the only case allowed in 4GL, when a var can be defined
    **                           after it is referenced.
    ** 106 ES  20250327          Verify if a class definition is present in the conversion list, not just propath. And
    **                           add it automatically if it is not present in conversion list.
    **     ES  20250328          Added additional check to verify whether a file is present in the conversion list,
    **                           overrides a 4gl build in class. In such case remove it from conversion.
    **                           Modify the propath entries to store a list on sources.
    ** 107 ES  20250422          Check class definiton before adding to the conversion list.
    ** 108 CA  20250422          Normalize the file names when calculating the class list in 'getClassFileList'
    **                           and 'createClassMappings', so all are relative to project home.
    ** 109 ICP 20250407          Added getLegacyPackageName method that extracts the legacy package name from
    **                           the fully qualified class name.
    **                           Corrected calcAccessMode to also calculate the package private access mode.
    **                           Modified readExtent to resolve the extent in the case the node's type is
    **                           EXPRESSION and the extent annotation is present in the child.
    ** 110 CA  20231215          Protect against infinite recursion in loadClassDefinition.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.uast;

    import java.io.*;
    import java.lang.reflect.*;
    import java.util.*;
    import java.util.Stack;
    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.logging.*;

    import com.goldencode.ast.*;
    import com.goldencode.util.*;
    import antlr.*;
    import com.goldencode.p2j.cfg.Configuration;
    import com.goldencode.p2j.convert.*;
    import com.goldencode.p2j.convert.db.*;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    // ambiguous reference
    import com.goldencode.p2j.util.osresource.FileSystemDaemon;

    import org.reflections.*;

    /**
     * Contains a dictionary for each type of language namespace and provides
     * methods to maintain and lookup in the respective namespaces.
     * <p>
     * Provides support for the following namespaces:
     * <ul>
     *    <li> language keywords (optionally abbreviated)
     *    <li> global and local variables with multiple arbitrary levels of
     *         scoping
     *    <li> functions (both built-in and user-defined)
     *    <li> procedures
     *    <li> classes
     *    <li> package names
     *    <li> labels
     *    <li> schema names
     *    <ul>
     *       <li> databases
     *       <li> tables - qualified or not, optionally abbreviated
     *       <li> fields - qualified or not, optionally abbreviated
     *    </ul>
     *    <li> widgets (this namespace is artificially separated from the
     *         variable namespace though normally this is not the case in
     *         Progress)
     *    <li> frames
     *    <li> menus and sub-menus
     *    <li> menu-items
     *    <li> streams
     *    <li> DataSet support
     *    <ul>
     *       <li> datasets
     *       <li> data-sources
     *       <li> data-relations
     *    </ul>
     *    <li> object-oriented support
     *    <ul>
     *       <li> packages
     *       <li> class definitions
     *    </ul>
     * </ul>
     * <p>
     * All namespaces (except language keywords) provide a mapping between a
     * symbol and an integer token type as defined by the Lexer and Parser.  It
     * is this token type that is returned from most lookup operations.
     * <p>
     * This class is aware of the parser token types and where necessary it
     * converts other values into the proper types for use in the parser.
     * At this time this feature is only needed for schema name lookups as
     * all other types are already normalized properly in the respective
     * dictionary.  See {@link #lookupField}.
     * <p>
     * Only the variable, widget and label namespaces utilize multiple scoping
     * levels at this time.  All other namespaces are flat.
     */
    public class SymbolResolver
    implements ProgressParserTokenTypes
    {
       /** Possible access modes for object data members. */
       private static final Set<Integer> ACCESS_MODES = new HashSet<>();

       /** Available menu children types. */
       private static final Set<Integer> MENU_CHILDREN_TYPES = new HashSet<>();

       /** Available sub-menu children types. */
       private static final Set<Integer> SUBMENU_CHILDREN_TYPES = new HashSet<>();

       /** Class or interface definition file extension. */
       public static final String CLASS_EXT = ".cls";

       /** Class or interface definition file search specification. */
       public static final String CLASS_SPLAT = "*" + CLASS_EXT;

       /** Default parent of all OE classes if no explicit parent is specified. */
       public static final String ROOT_OBJ_NAME = "Progress.Lang.Object";

       /** Default parent of all .NET classes if no explicit parent is specified. */
       public static final String ROOT_DOTNET_OBJ_NAME = "System.Object";

       /** Default parent of all classes if no explicit parent is specified. */
       public static final String ROOT_CLASS_NAME = "Progress.Lang.Class";

       /** Relative location for the non-project class/interface 4GL code. */
       public static final String SKELETON_PATH = "./p2j/skeleton/";

       /** Relative location for the 4GL object-oriented base definitions. */
       public static final String OO4GL_PATH = "oo4gl/";

       /** Relative location for the .NET base definitions. */
       public static final String DOTNET_PATH = "dotnet/";

       /** Relative location for project-specific .NET definitions. */
       public static final String ASSEMBLY_PATH = "./assemblies/";

       /** Relative location for the non-project class/interface 4GL code. */
       public static String skeletonPath = SKELETON_PATH;

       /** Relative location for the 4GL object-oriented base definitions. */
       public static String oo4glPath = SKELETON_PATH + OO4GL_PATH;

       /** Relative location for the .NET base definitions. */
       public static String dotnetPath = SKELETON_PATH + DOTNET_PATH;

       /** Logger */
       private static final ConversionStatus LOG = ConversionStatus.get(SymbolResolver.class);

       /** Context-local data. */
       private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /**
        * Mock class to be able to parse OO references in pre-scan mode, without affecting the lookup,
        * as not all classes might be loaded.
        */
       private final ClassDefinition MOCK_CLASS_DEF =
       new ClassDefinition(null, null, null, false, OOType.CLASS, false, "")
       {
          @Override
          public void annotateMethodCall(String         mname,
                                         ParameterKey[] sig,
                                         int            access,
                                         boolean        isStatic,
                                         boolean        isSuper,
                                         boolean        internal,
                                         Aast           node)
          {
             // no-op
          }

          @Override
          public String getName()
          {
             return "MOCK_FWD_CLASS";
          };

          @Override
          public java.util.Set<String> getInterfaces()
          {
             return Collections.emptySet();
          };

          @Override
          public SchemaDictionary getSchemaDict()
          {
             return null;
          };

          @Override
          public void addDataSet(Variable var, Aast node, String name, int type, int access, boolean isStatic, boolean inMethod)
          {
             // no-op
          };

          @Override
          public void addDataSource(Variable var, Aast node, String name, int type, int access, boolean isStatic, boolean inMethod)
          {
             // no-op
          };

          @Override
          public void addMethod(String  name,
                                int     type,
                                int     access,
                                boolean isStatic,
                                Aast    node,
                                Aast    ret)
          {
             // no-op
          };

          @Override
          public void addQuery(String name, Aast node, int type, int access, boolean isStatic,
                               boolean inMethod)
          {
             // no-op
          };

          @Override
          public void addTable(Aast node, String name, int type, int access, boolean isStatic,
                               SchemaDictionary schemaDict, boolean inMethod)
          {
             // no-op
          };

          @Override
          public void addVariable(int type, String name, Variable var, Aast node, int access, boolean isStatic,
                                  String qname, boolean provisional)
          {
             // no-op
          }

          @Override
          public int lookupDataSet(String name, int access, boolean isStatic)
          {
             return DATA_SET;
          }

          @Override
          public int lookupDataSource(String name, int access, boolean isStatic)
          {
             return DATA_SOURCE;
          };

          @Override
          public int guessMethodType(String name, int access, boolean isStatic, boolean internal)
          {
             if (!currentCls.isEmpty())
             {
                int type = currentCls.peek().guessMethodType(name, access, isStatic, internal);
                if (type != -1)
                {
                   return type;
                }
             }

             int type = localLookup ? SymbolResolver.this.lookupVariable(name) : -1;
             return type != -1 ? -1 : OO_METH_CLASS;
          };

          @Override
          public int lookupQuery(String name, int access, boolean isStatic)
          {
             return QUERY;
          };

          @Override
          public int lookupTable(String name, int access, boolean isStatic)
          {
             return TABLE;
          };

          @Override
          public int lookupVariable(String name, int access, boolean isStatic)
          {
             if (!currentCls.isEmpty())
             {
                int type = currentCls.peek().lookupVariable(name, access, isStatic);
                if (type != -1)
                {
                   return type;
                }
             }

             int type = localLookup ? SymbolResolver.this.lookupVariable(name) : -1;
             return type != -1 ? -1 : VAR_CLASS;
          };

          @Override
          public String lookupVariableClassName(String name, int access, boolean isStatic)
          {
             if (!currentCls.isEmpty())
             {
                String clsName = currentCls.peek().lookupVariableClassName(name, access, isStatic);
                if (clsName != null)
                {
                   return clsName;
                }
             }

             return "MOCK_FWD_VAR";
          };

          /**
           * The method indicates whether this inctance represents a "mock" class definition
           * used to resolve symbols during class pre-scan mode in case the class file has not been
           * parsed.
           *
           * @return   <code>true</code> is this instance represents a "mock" class definition,
           *           <code>false</code> otherwise.
           */
          @Override
          public boolean isMock()
          {
             return true;
          }
       };

       /** Cache of resolved classes, per absolute directory path. */
       private static final Map<String, String[]> DIR_CLASSES = new ConcurrentHashMap<>();

       /**
        * An exemplar from which the dictionaries will be populated, when creating instances via
        * {@link #newRuntimeInstance()}.
        */
       protected static final SymbolResolver SYMBOL_RESOLVER_EXEMPLAR;

       static
       {
          // the following needs to be done only for runtime conversion

          SymbolResolver sym = null;
          if (Configuration.isRuntimeConfig())
          {
             sym = new SymbolResolver()
             {
                @Override
                protected boolean isRuntimeConfig()
                {
                   return false;
                }
             };
             StringReader in = new StringReader("");
             ProgressLexer lexer = new ProgressLexer(in, sym);
             ProgressParser parser = new ProgressParser(lexer, sym);
          }

          SYMBOL_RESOLVER_EXEMPLAR = sym;
       }

       /** Classes in the propath, mapped qualified classname to filename. */
       private Map<String, List<String>> propathCls = null;

       /** Stores class and interface definitions. */
       private ScopedSymbolDictionary classDict = null;

       /** Flag indicating if the data member or method is looked up without a qualifier. */
       private boolean localLookup = false;

       /** Flag indicating that variable lookup must be done only at class members. */
       private boolean classLookup = false;

       /** Stores keywords and handles abbreviation support. */
       private KeywordDictionary kwDict = null;

       /** Stores accum variables. */
       private ScopedSymbolDictionary accumDict = null;

       /** Stores variables, both local (scoped) and global. */
       private ScopedSymbolDictionary varDict = null;

       /** Cache of local variables used to duplicate function signatures. */
       private Map<String, Set<Map.Entry>> varDefCache = null;

       /** Stores function names and return types. */
       private ScopedSymbolDictionary funcDict = null;

       /** Stores procedure names. */
       private ScopedSymbolDictionary procDict = null;

       /** Stores explicit package names. */
       private Map<String, UsingSpec> explPkgDict = null;

       /** Stores package search specifications. */
       private Set<UsingSpec> searchPkgDict = null;

       /** Stores label names. */
       private ScopedSymbolDictionary labelDict = null;

       /** Stores database object names (databases, tables and fields). */
       private SchemaDictionary schemaDict = null;

       /** Stores widget names. */
       private ScopedSymbolDictionary widgetDict = null;

       /** Stores frame names. */
       private ScopedSymbolDictionary frameDict = null;

       /** Stores sets of database field widget ASTs, organized by their frame names */
       private ScopedSymbolDictionary<Set<Aast>> fieldWidgetDict = null;

       /** Stores the state of USE-DICT-EXPS flag for each frame. */
       private ScopedSymbolDictionary<Boolean> useDictExps = null;

       /** Stores menu and sub-menu names. */
       private ScopedSymbolDictionary menuDict = null;

       /** Stores menu-item names. */
       private ScopedSymbolDictionary menuItemDict = null;

       /** Stores stream names. */
       private ScopedSymbolDictionary streamDict = null;

       /** Stores query names. */
       private ScopedSymbolDictionary qryDict = null;

       /** Stores dataset names. */
       private ScopedSymbolDictionary dsetDict = null;

       /** Stores data-source names. */
       private ScopedSymbolDictionary dsrcDict = null;

       /** Stores data-relation names. */
       private ScopedSymbolDictionary drelDict = null;

       /** Maps keyword token types to the correct attribute and method type. */
       private Map<Integer, Integer> attributes = null;

       /** Provides access to ATTR_CLASS or METH_CLASS class names. */
       private Map<Integer, String> attrTypes = null;

       /** Current class or interface definition that is being created. */
       private Stack<ClassDefinition> currentCls = null;

       /** AST-specific variable definition index workaround. */
       private int tempIdx = 0;

       /** Current PROPATH. */
       private String[] propath = null;

       /** The legacy system's filesystem case-sensitivity. */
       private boolean caseSens = true;

       /** Used for recursive class or interface loading. */
       private AstGenerator generator = null;

       /** Flag indicating whether silent mode is required. */
       private boolean silent = false;

       /** Flag to globally enable/disable table promotion. */
       private boolean allow = true;

       /**
        * Flag indicating that an unique index was found in the current query - every subsequent
        * BY clause will be allowed to match garbage.
        */
       private boolean foundUniqueIndex = false;

       /**
        * Flag indicating that unique index matching using the BY clause's fields is possible; set via
        * {@link #startIndexFieldSearch()}, from {@link ProgressParser#each_first_last_spec}.  It will
        * be reset via {@link #endIndexFieldSearch()}, at the end of the current query or if a matching
        * is not possible (i.e. multiple tables, BREAK clause, not a prefix match for an unique
        * index).
        */
       private boolean indexFieldSearch = false;

       /**
        * Flag indicating that we can use the next field reference as an attempted match against an
        * index.  Set at the beginning of {@link ProgressParser#sort_order_clause()} and reset at the
        * end, via {@link #useIndexFieldSearch}.
        */
       private boolean useIndexFieldSearch = false;

       /**
        * The current query's single table, which will be used to disambiguate unqualified fields in
        * BY clauses.
        */
       private String indexTable = null;

       /**
        * The current query's single buffer, which will be used to disambiguate unqualified fields in
        * BY clauses.
        */
       private String indexBuffer = null;

       /**
        * The currently {@link #collectSortField(Aast) collected AST fields} from BY clauses.  All
        * these match as a prefix an unique index.
        */
       private List<Aast> sortFields = null;

       /**
        * The longest unique indexes (without any smaller unique index which can be used as a prefix)
        * in the {@link #indexTable}.
        */
       private Map<Aast, List<Aast>> indexes = null;

       /** Are we parsing code inside a method definition (includes constructors, destructors...). */
       private boolean inMethod = false;

       /** Flag indicating we are in a chained reference. */
       private boolean chainedReference = false;

       static
       {
          ACCESS_MODES.add(KW_PUBLIC);
          ACCESS_MODES.add(KW_PROTECTD);
          ACCESS_MODES.add(KW_PRIVATE);

          MENU_CHILDREN_TYPES.add(KW_NEW);
          MENU_CHILDREN_TYPES.add(KW_SHARED);
          MENU_CHILDREN_TYPES.add(KW_FGCOLOR);
          MENU_CHILDREN_TYPES.add(KW_BGCOLOR);
          MENU_CHILDREN_TYPES.add(KW_DCOLOR);
          MENU_CHILDREN_TYPES.add(KW_PFCOLOR);
          MENU_CHILDREN_TYPES.add(KW_FONT);
          MENU_CHILDREN_TYPES.add(KW_TITLE);
          MENU_CHILDREN_TYPES.add(KW_MENU_BAR);
          MENU_CHILDREN_TYPES.add(KW_MENU_ITM);
          MENU_CHILDREN_TYPES.add(KW_SUB_MENU);
          MENU_CHILDREN_TYPES.add(KW_SKIP);
          MENU_CHILDREN_TYPES.add(KW_RULE);

          SUBMENU_CHILDREN_TYPES.add(KW_FGCOLOR);
          SUBMENU_CHILDREN_TYPES.add(KW_BGCOLOR);
          SUBMENU_CHILDREN_TYPES.add(KW_DCOLOR);
          SUBMENU_CHILDREN_TYPES.add(KW_PFCOLOR);
          SUBMENU_CHILDREN_TYPES.add(KW_FONT);
          SUBMENU_CHILDREN_TYPES.add(KW_MENU_ITM);
          SUBMENU_CHILDREN_TYPES.add(KW_SUB_MENU);
          SUBMENU_CHILDREN_TYPES.add(KW_SKIP);
          SUBMENU_CHILDREN_TYPES.add(KW_RULE);
       }

       /**
        * Default constructor that creates an instance with schema lookups
        * <b>disabled</b>.  Each namespace is initialized.
        */
       public SymbolResolver()
       {
          this(false, true, null, true, true, null);
       }

       /**
        * Constructs an instance with control over disabling schema lookups.
        * This allows an instance to be created in the absence of any schema
        * support.  Each namespace is initialized.
        *
        * @param    schema
        *           <code>true</code> enables schema dictionary support.
        */
       public SymbolResolver(boolean schema)
       {
          this(schema, true, null, true, true, null);
       }

       /**
        * Constructs an instance with control over disabling schema lookups and
        * symbol dictionary. This allows an instance to be created in the absence
        * of any schema support and namespace is initialization
        *
        * @param    schema
        *           <code>true</code> enables schema dictionary support.
        * @param    caseSens
        *           <code>true</code> if the legacy system had a case-sensitive file-system.
        * @param    propath
        *           The current <code>PROPATH</code> to honor or <code>null</code> for the default
        *           of ".".
        * @param    silent
        *           Flag indicating if <code>Sytsem.out</code> output should be suppressed.
        */
       public SymbolResolver(boolean schema, boolean caseSens, String[] propath, boolean silent)
       {
          this(schema, caseSens, propath, silent, true, null);
       }

       /**
        * Constructs an instance with control over disabling schema lookups. This allows an instance
        * to be created in the absence of any schema support.  Each namespace is initialized.
        *
        * @param   schema
        *          {@code true} enables schema dictionary support.
        * @param   caseSens
        *          {@code true} if the legacy system had a case-sensitive file-system.
        * @param   propath
        *          The current {@code PROPATH} to honor or {@code null} for the default of {@code "."}.
        * @param   silent
        *          Flag indicating if {@code System.out} output should be suppressed.
        * @param   dict
        *          Flag indicating if symbol dictionary creations should proceed.
        * @param   bannedSchemas
        *          The set of banned schemas. The {@code Namespace} will not recognize element from these schemas
        *          even is they are loaded.
        */
       public SymbolResolver(boolean     schema,
                             boolean     caseSens,
                             String[]    propath,
                             boolean     silent,
                             boolean     dict,
                             Set<String> bannedSchemas)
       {
          // needed for minimal lexer use
          kwDict   = new KeywordDictionary();

          // needed for minimal expression evaluation usage
          varDict  = new ScopedSymbolDictionary<>(null, false);
          funcDict = new ScopedSymbolDictionary<>(null, false);

          // dictionaries that are only needed for full 4GL parsing
          if (dict)
          {
             accumDict       = new ScopedSymbolDictionary<>(null, false);
             varDefCache     = new HashMap<>();
             procDict        = new ScopedSymbolDictionary<>(null, false);
             explPkgDict     = new LinkedHashMap<>();
             searchPkgDict   = new LinkedHashSet<>();
             labelDict       = new ScopedSymbolDictionary<>(null, false);
             widgetDict      = new ScopedSymbolDictionary<>(null, false);
             frameDict       = new ScopedSymbolDictionary<>(null, false);
             fieldWidgetDict = new ScopedSymbolDictionary<>(null, false);
             useDictExps     = new ScopedSymbolDictionary<>(null, false);
             menuDict        = new ScopedSymbolDictionary<>(null, false);
             menuItemDict    = new ScopedSymbolDictionary<>(null, false);
             streamDict      = new ScopedSymbolDictionary<>(null, false);
             qryDict         = new ScopedSymbolDictionary<>(null, false);
             dsetDict        = new ScopedSymbolDictionary<>(null, false);
             dsrcDict        = new ScopedSymbolDictionary<>(null, false);
             drelDict        = new ScopedSymbolDictionary<>(null, false);
             attributes      = new HashMap<>();
             attrTypes       = new HashMap<>();
             currentCls      = new Stack<>();
          }

          if (schema)
          {
             // this load is dependent upon a configuration file being available
             try
             {
                schemaDict = new SchemaDictionary(bannedSchemas);

                // add a default scope that represents the scope of the external procedure
                addSchemaScope(false);
             }
             catch (SchemaException ex)
             {
                LOG.log(Level.WARNING,"The schema dictionary is invalid, however all other types of lookups " +
                   "will work fine --> if there is no schema name processing, then consuming this exception is" +
                   " safe; if there is schema name processing, then get ready for a null pointer exception!", ex);
             }
          }

          setPropath(propath);

          this.caseSens = caseSens;
          this.silent   = silent;
          this.propathCls = new HashMap<>();

          if (dict)
          {
             this.classDict = initPossibleClasses(this.propathCls, this.propath, this.caseSens);
             WorkArea wa = locate();
             if (!wa.legacyClassesLoaded)
             {
                wa.legacyClassesLoaded = true;
                loadLegacyClasses(this);
             }
          }
       }

       /**
        * Constructor to copy state for runtime conversion, from an exemplar instance.
        *
        * @param    sym
        *           The exemplar instance.
        */
       private SymbolResolver(SymbolResolver sym)
       {
          // initialize only the dictionaries which are used for runtime conversion
          kwDict          = sym.kwDict; // for runtime - this instance is immutable here
          accumDict       = null;
          varDict         = new ScopedSymbolDictionary<>(null, false); // for runtime
          varDict.copyAll(sym.varDict, false);
          funcDict        = new ScopedSymbolDictionary<>(null, false); // for runtime
          funcDict.copyAll(sym.funcDict, false);
          varDefCache     = new HashMap<>(); // TODO: is it used?
          procDict        = null;
          explPkgDict     = Collections.emptyMap();
          searchPkgDict   = Collections.emptySet();
          labelDict       = null;
          widgetDict      = null;
          frameDict       = null;
          fieldWidgetDict = null;
          useDictExps     = null;
          menuDict        = null;
          menuItemDict    = null;
          streamDict      = null;
          qryDict         = new ScopedSymbolDictionary<>(null, false); // used by query conversion
          dsetDict        = null;
          dsrcDict        = null;
          drelDict        = null;
          attributes      = sym.attributes; // for runtime - this instance is immutable here
          attrTypes       = sym.attrTypes;  // for runtime - this instance is immutable here
          currentCls      = null;
          this.propathCls = Collections.emptyMap();
          this.classDict  = null;

          this.caseSens = true;
          this.silent   = true;
          setPropath(null);
       }

       /**
        * Resolve the {@link ClassDefinition} for the specified AST, referencing it.  It is assumed that the
        * AST either has a <code>qualified_oo_name</code> annotation or otherwise, the AST is for a .cls file.
        *
        * @param    ast
        *           The ast for which we need the {@link ClassDefinition}.
        *
        * @return   See above.
        */
       public static ClassDefinition resolveClassDefinition(Aast ast)
       {
          if (Configuration.isRuntimeConfig())
          {
             // runtime conversion does not allow .cls usage
             return null;
          }
          ClassDefinition clsDef = null;

          String qname = (String) ast.getAnnotation("qualified_oo_name");
          if (qname == null)
          {
             // 'qualified_oo_name' may not have been set yet
             String fname = ast.getFilename();
             if (fname.toLowerCase().endsWith(".cls"))
             {
                try
                {
                   clsDef = SymbolResolver.loadConvertedClass(fname, false);
                }
                catch (ClassNotFoundException e)
                {
                   // ignore
                }
             }
          }
          else
          {
             // use the fast way to resolve it
             clsDef = SymbolResolver.loadClassDefinition(qname);
          }

          return clsDef;
       }

       /**
        * The parse for this entire file set has finished, and a final cleanup of used resources can be done for
        * each {@link ClassDefinition}.
        */
       public static void parseFinished()
       {
          WorkArea wa = locate();

          if (wa.classCache != null)
          {
             wa.classCache.values().forEach(cls -> cls.parseFinished(true));
          }

          for (ScopedSymbolDictionary dict : wa.propathClassDicts.values())
          {
             for (int i = 0; i < dict.size(); i++)
             {
                Map classes = (Map) dict.getDictionaryAtScope(i, false);
                if (classes != null)
                {
                   classes.values().forEach(cls -> ((ClassDefinition) cls).parseFinished(true));
                }
             }
          }
       }

       /**
        * Sets 4gl sources defined in file-cvt-list.txt.
        *
        * @param    fileList
        *           Set with the names of the sources.
        */
       public static void setCvtSources (Set<String> sources)
       {
          WorkArea wa = locate();
          if (wa.cvtSources != null)
          {
             return;
          }

          wa.cvtSources = sources;
       }

       /**
        * Create a new instance which has only dictionaries used by the runtime conversion, initialized from the
        * {@link #SYMBOL_RESOLVER_EXEMPLAR}.
        *
        * @return   See above.
        */
       public static SymbolResolver newRuntimeInstance()
       {
          return new SymbolResolver(SYMBOL_RESOLVER_EXEMPLAR);
       }

       /**
        * Override the default skeleton path if it is configured.
        */
       public static synchronized void initSkeletonPath()
       {
          skeletonPath = Configuration.getParameter("oo-skeleton-path");

          // only rework these from the defaults if a skeleton path is overridden
          if (skeletonPath != null)
          {
             if (!skeletonPath.endsWith(File.separator))
             {
                skeletonPath = skeletonPath + File.separator;
             }

             oo4glPath = skeletonPath + OO4GL_PATH;
             dotnetPath = skeletonPath + DOTNET_PATH;
          }
       }

       /**
        * Obtain the list of all "fakeout" class and interface definitions that
        * exist in the project.  These <code>.cls</code> files represent the
        * Progress built-ins and all .NET classes/interfaces that can be accessed.
        * <p>
        * These files will be listed in the directories represented by the
        * {@link #OO4GL_PATH}, {@link #DOTNET_PATH} and the {@link #ASSEMBLY_PATH}.
        *
        * @return   The list of <code>.cls</code> files, in the same case in which
        *           they exist in the filesystem.
        */
       public static String[] listFakeoutFiles()
       {
          String[] oo4gl    = getClassFileList(oo4glPath, false);
          String[] dotnet   = getClassFileList(dotnetPath, false);
          String[] assembly = getClassFileList(ASSEMBLY_PATH, false);

          ArrayList<String> list = new ArrayList<>();

          list.addAll(Arrays.asList(oo4gl));
          list.addAll(Arrays.asList(dotnet));
          list.addAll(Arrays.asList(assembly));

          return list.toArray(new String[0]);
       }

       /**
        * Process a method definition to extract the signature.
        *
        * @param    def
        *           The METHOD_DEF node which is used to extract the signature of the method.
        *
        * @return   An array of the parameter types in signature order from left to right.
        */
       public static ParameterKey[] calculateDefinitionSignature(Aast def)
       {
          List<ParameterKey> parms = new LinkedList<ParameterKey>();

          processDefinitionSignature(def, (Aast ast, ParameterKey key, int idx) -> parms.add(key));

          return parms.toArray(new ParameterKey[0]);
       }

       /**
        * Process a method definition signature (left to right) executing the given consumer for each parameter.
        *
        * @param    def
        *           The METHOD_DEF node whose signature is processed.
        * @param    handler
        *           The consumer of each parameter.
        */
       public static void processDefinitionSignature(Aast def, ParameterConsumer handler)
       {
          if (def != null)
          {
             Aast methkw  = null;
             Aast lparens = null;
             int defType = def.getType();

             if (defType == METHOD_DEF)
             {
                methkw = def.getImmediateChild(KW_METHOD, null);
             }
             else if (defType == CONSTRUCTOR)
             {
                methkw = def.getImmediateChild(KW_CONSTRUC, null);
             }
             else
             {
                methkw = def;

                if (defType != KW_METHOD && defType != KW_CONSTRUC && defType != EVENT_SIGNATURE)
                {
                   System.out.printf("BAD METHOD NODE %s\n", def.dumpTree());
                   return;
                }
             }

             int k = 0;

             lparens = methkw.getImmediateChild(LPARENS, null);

             if (lparens != null && lparens.getFirstChild() != null)
             {
                // iterate all parameters and extract their types
                Aast parm = (Aast) lparens.getFirstChild();

                while (parm != null)
                {
                   if (parm.getType() == PARAMETER)
                   {
                      String name = translateType((int) ((long) parm.getAnnotation("type")), parm, null);

                      name = formatExtentName(name, readExtent(parm));

                      if (name == null)
                      {
                         System.out.printf("BAD PARM NODE %s\n", parm.dumpTree());
                      }

                      Long ptype = (Long) parm.getAnnotation("parmtype");
                      if (name != null && name.startsWith("BUFFER "))
                      {
                         ptype = null;
                      }

                      // at definition time, the default mode (if missing) is INPUT
                      handler.accept(parm,
                                     new ParameterKey(ptype == null ? KW_INPUT : ptype.intValue(), name),
                                     k);
                   }
                   parm = (Aast) parm.getNextSibling();
                }
             }
          }
       }

       /**
        * Read the extent value for the given node if it is specified as an annotation or if there
        * is a KW_EXTENT child.
        *
        * @param    node
        *           The AST to read.
        *
        * @return   The extent value or 0 if this is a scalar node.
        */
       public static int readExtent(Aast node)
       {
          if (Boolean.TRUE.equals(node.getAnnotation("dotnet-cls")) &&
              Boolean.TRUE.equals(node.getAnnotation("dotnet-array")))
          {
             return -1;
          }

          int extent = 0;

          if (node.getType() == PARAMETER && !node.downPath(KW_AS))
          {
             node = (Aast) node.getFirstChild();

             int type = node.getType();

             while (type == KW_INPUT   ||
                    type == KW_IN_OUT  ||
                    type == KW_OUTPUT  ||
                    type == KW_APPEND  ||
                    type == KW_BIND    ||
                    type == KW_BY_REF  ||
                    type == KW_BY_VALUE)
             {
                node = (Aast) node.getNextSibling();
                type = node.getType();
             }
          }

          if (node.getType() == OBJECT_INVOCATION)
          {
             // the rightmost child of the topmost OBJECT_INVOCATION is the
             // rightmost node in the chain
             node = node.getChildAt(1);
          }

          // after finding the correct node above, check if this is a subscript ref
          if (node.downPath(LBRACKET))
          {
             // this is an indexed reference
             return 0;
          }

          if (node.isAnnotation("extent"))
          {
             extent = (int) ((long) node.getAnnotation("extent"));
          }
          else if (node.getType() == EXPRESSION && node.getNumChildren() > 0)
          {
             Aast child = node.getChildAt(0);
             if (child != null && child.isAnnotation("extent"))
             {
                extent = (int) ((long) child.getAnnotation("extent"));
             }
          }
          else
          {
             Aast child = node.getImmediateChild(KW_EXTENT, null);

             if (child != null)
             {
                // there is an extent, default to indeterminate
                extent = -1;

                Aast num = child.getChildAt(0);

                if (num != null)
                {
                   extent = ExpressionConversionWorker.literalToInt(num);
                }
             }
          }

          return extent;
       }

       /**
        * Process the extent value and augment the name specification with the extent value
        * if it is not scalar. Indeterminate extent (-1) will have an "[]" appended.  All other
        * non-scalar values (not 0) will be appended with the fixed extent "[num]" where num
        * is the value of the extent.
        *
        * @param    name
        *           The data type name to augment.
        * @param    extent
        *           The extent value.
        *
        * @return   The augmented name if this is a non-scalar value and the passed in name if
        *           scalar.
        */
       public static String formatExtentName(String name, int extent)
       {
          if (extent != 0 && extent != -2)
          {
             if (extent == -1)
             {
                // indeterminate
                name = String.format("%s[]", name);
             }
             else
             {
                // fixed
                name = String.format("%s[%d]", name, extent);
             }
          }

          return name;
       }

       /**
        * Compute the Java signature for a legacy method - this will include the converted method name and the
        * parameter representation in the converted Java code, but not the return type.
        *
        * @param    name
        *           The legacy name.
        * @param    sig
        *           The parameter definition.
        *
        * @return   The signature using Java representation for the parameters.
        */
       public static String calculateJavaSignature(String name, ParameterKey[] sig)
       {
          WorkArea wa = local.get();

          String jname = wa.nconvert.convert(name, MatchPhraseConstants.TYPE_METHOD);

          // in 4GL, the method names are case-insensitive - even if we want a Java signature here,
          // we still need to use a lowercased name, as otherwise overridden methods may not be resolved
          // properly (as 4GL has no restriction for an overridden method to have the same case as
          // the parent method).
          jname = jname.toLowerCase();

          return jname.toLowerCase() + "(" + calculateJavaParameters(sig) + ")";
       }

       /**
        * Compute the Java signature for the legacy method parameters (does not include the parenthesis).
        *
        * @param    sig
        *           The parameter definition.
        *
        * @return   The Java representation of the parameter list or "" if there are no parameters.
        */
       public static String calculateJavaParameters(ParameterKey[] sig)
       {
          String core = "";

          if (sig != null)
          {
             for (int i = 0; i < sig.length; i++)
             {
                core = core + (i == 0 ? "" : ", ") + sig[i].toJava();
             }
          }

          return core;
       }

       /**
        * Given a Java method with a {@link LegacySignature}, reverse-compute its 4GL signature.
        *
        * @param    mname
        *           The legacy method name.
        * @param    lsig
        *           The legacy signature annotation.
        */
       public static String calculateLegacySignature(String mname, LegacySignature lsig)
       {
          LegacyParameter[] lparms = lsig.parameters();
          ParameterKey[]    sig    = new ParameterKey[(lparms == null) ? 0 : lparms.length];

          for (int i = 0; i < lparms.length; i++)
          {
             sig[i] = new ParameterKey(lparms[i]);
          }

          return calculateLegacySignature(mname, sig);
       }

       /**
        * Compute the legacy signature for a legacy method - this will include the legacy name (lowercased) and
        * the parameter list, but not the return type.
        *
        * @param    name
        *           The legacy name.
        * @param    sig
        *           The parameter definition.
        *
        * @return   The legacy signature representation.
        */
       public static String calculateLegacySignature(String name, ParameterKey[] sig)
       {
          String s = name.toLowerCase() + "(";

          if (sig != null)
          {
             for (ParameterKey pk : sig)
             {
                s = s + (s.endsWith("(") ? "" : ", ") + pk.toString();
             }
          }

          return s + ")";
       }

       /**
        * Given a class property or a class event, track all its associated FWD methods, to allow
        * disambiguation during parsing.
        * <p>
        * This is used for incremental conversion, where we need to know the details of this method,
        * when they are defined in an already converted class.
        *
        * @param    stype
        *           Token type of the defining statement.
        * @param    cls
        *           The class in which the property or event exists.
        * @param    node
        *           The AST node being processed.
        * @param    var
        *           The associated variable for this member.
        * @param    name
        *           The legacy name.
        */
       public static void trackPropertyOrEventMethodSignatures(int             stype,
                                                               ClassDefinition cls,
                                                               Aast            node,
                                                               Variable        var,
                                                               String          name)
       {
          if (isPreScan() && !cls.isBuiltIn())
          {
             return;
          }

          boolean over = var.isOverride();
          boolean abst = var.isAbstract();

          if (stype == DEFINE_PROPERTY)
          {
             String ptype = translateType(var.getTokenType(), null, var.getClassName());

             if (var.isExtent())
             {
                String ext = "[" + (var.getExtent() > 0 ? Integer.toString(var.getExtent()) : "") +"]";

                cls.registerPropertyMethod(node, "bulk-get", name, over, abst, new ParameterKey[0]);
                cls.registerPropertyMethod(node,
                                           "get",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, "int64")
                                           });

                cls.registerPropertyMethod(node,
                                           "bulk-set",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, ptype + ext)
                                           });
                cls.registerPropertyMethod(node,
                                           "set",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, ptype),
                                              new ParameterKey(KW_INPUT, "int64")
                                           });

                cls.registerPropertyMethod(node,
                                           "set-all",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, ptype)
                                           });

                cls.registerPropertyMethod(node, "length-of", name, over, abst, new ParameterKey[0]);
                cls.registerPropertyMethod(node,
                                           "resize",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, "int64")
                                           });
             }
             else
             {
                cls.registerPropertyMethod(node, "get", name, over, abst, new ParameterKey[0]);
                cls.registerPropertyMethod(node,
                                           "set",
                                           name,
                                           over,
                                           abst,
                                           new ParameterKey[]
                                           {
                                              new ParameterKey(KW_INPUT, ptype)
                                           });
             }
          }
          else if (stype == DEFINE_EVENT)
          {
             cls.registerPropertyMethod(node, "subscribe", name, over, abst,
                                        new ParameterKey[]
                                        {
                                           new ParameterKey(KW_INPUT, "handle"),
                                           new ParameterKey(KW_INPUT, "character"),
                                        });
             cls.registerPropertyMethod(node, "unsubscribe", name, over, abst,
                                        new ParameterKey[]
                                        {
                                           new ParameterKey(KW_INPUT, "handle"),
                                           new ParameterKey(KW_INPUT, "character"),
                                        });
             cls.registerPropertyMethod(node, "subscribe", name, over, abst,
                                        new ParameterKey[]
                                        {
                                           new ParameterKey(KW_INPUT, "object<? extends progress.lang.object>"),
                                           new ParameterKey(KW_INPUT, "character"),
                                        });
             cls.registerPropertyMethod(node, "unsubscribe", name, over, abst,
                                        new ParameterKey[]
                                        {
                                           new ParameterKey(KW_INPUT, "object<? extends progress.lang.object>"),
                                           new ParameterKey(KW_INPUT, "character"),
                                        });

             // TODO: publish-[evtName] isn't tracked - not sure if even is required to do so
             // cls.registerPropertyMethod(node, "publish-", name, over, abst, pkey);
          }
       }

       /**
        * Convert the given legacy method name to Java and return it.
        *
        * @param    name
        *           The the 4GL method name.
        *
        * @return   The converted Java method name.
        */
       public static String convertMethodName(String name)
       {
          WorkArea wa = locate();
          return wa.nconvert.convert(name, MatchPhraseConstants.TYPE_METHOD);
       }

       /**
        * Translate a variable type into a wrapper type name for use in a signature specification.
        *
        * @param    type
        *           The token type representing a variable/parameter type.
        * @param    parm
        *           The PARAMETER node which is used to extract the type of this parameter.
        * @param    qualified
        *           Fully qualified object classname or {@code null} if no qualifier is needed.
        *
        * @return   The wrapper class name.
        */
       public static String translateType(int type, Aast parm, String qualified)
       {
          if (parm != null)
          {
             if (parm.downPath(KW_DSET_HND) || parm.getType() == KW_DSET_HND)
             {
                return "DATASET-HANDLE";
             }
             else if (parm.downPath(KW_TAB_HAND) || parm.getType() == KW_TAB_HAND)
             {
                return "TABLE-HANDLE";
             }
          }

          String cls = null;
          Aast ref = null;

          switch (type)
          {
             case VAR_CHAR:
                cls = "character";
                break;
             case VAR_COM_HANDLE:
                cls = "comhandle";
                break;
             case VAR_DATE:
                cls = "date";
                break;
             case VAR_DATETIME:
                cls = "datetime";
                break;
             case VAR_DATETIME_TZ:
                cls = "datetimetz";
                break;
             case VAR_DEC:
                cls = "decimal";
                break;
             case VAR_HANDLE:
                cls = "handle";
                break;
             case VAR_INT:
                cls = "integer";
                break;
             case VAR_INT64:
                cls = "int64";
                break;
             case VAR_LOGICAL:
                cls = "logical";
                break;
             case VAR_LONGCHAR:
                cls = "longchar";
                break;
             case VAR_MEMPTR:
                cls = "memptr";
                break;
             case VAR_RAW:
                cls = "raw";
                break;
             case VAR_RECID:
                cls = "recid";
                break;
             case VAR_ROWID:
                cls = "rowid";
                break;
             case VAR_POLY:
                cls = "Object";
                break;
             case VAR_CLASS:
                cls = "object";

                String qual = qualified;

                if (parm != null)
                {
                   if (parm.isAnnotation("is-java") && (Boolean) parm.getAnnotation("is-java"))
                   {
                      cls = "jobject";
                   }

                   if (qualified == null)
                   {
                      qual = (String) parm.getAnnotation("qualified");
                   }
                }

                cls = String.format("%s<? extends %s>", cls, qual);
                break;

             // TABLE and BUFFER parameters - these will have the TEMP-TABLE or BUFFER prefix,
             // followed by the schemaname or other details that make the table unambigous
             case KW_TABLE:
                ref = parm.getType() == KW_TABLE ? parm : parm.getImmediateChild(KW_TABLE, null);

                if (ref.downPath(TEMP_TABLE))
                {
                   ref = ref.getImmediateChild(TEMP_TABLE, null);
                }
                else if (ref.downPath(WORK_TABLE))
                {
                   ref = ref.getImmediateChild(WORK_TABLE, null);
                }
                else if (ref.downPath(BUFFER))
                {
                   ref = ref.getImmediateChild(BUFFER, null);
                }
                else
                {
                   // this is unexpected since permanent tables and work-tables should not be possible here
                   // and buffers are handled below
                   throw new RuntimeException(String.format("TABLE parameters should be temp-tables: %s",
                                                            parm.dumpTree()));
                }

                String signature = (String) ref.getAnnotation("db_signature");

                if (signature == null)
                {
                   throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
                                                            ref.dumpTree()));
                }

                // for temp-tables, the compile time mapping is done via the table's field signature
                cls = String.format("TEMP-TABLE <%s>", signature);
                break;
             case KW_BUFFER:
                ref = parm.getType() == KW_BUFFER ? parm : parm.getImmediateChild(KW_BUFFER, null);
                if (ref.downPath(KW_FOR))
                {
                   ref = ref.getImmediateChild(KW_FOR, null);
                }
                ref = (Aast) ref.getFirstChild();
                if (ref.getType() == TEMP_TABLE ||
                    (ref.getType() == BUFFER && "".equals(ref.getAnnotation("dbname"))))
                {
                   String tabname = (String) ref.getAnnotation("schemaname");
                   String tabsig  = (String) ref.getAnnotation("db_signature");

                   if (tabsig == null)
                   {
                      throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
                                                               ref.dumpTree()));
                   }

                   cls = String.format("BUFFER <temp-table %s <%s>>", tabname.toLowerCase(), tabsig);
                }
                else
                {
                   cls = String.format("BUFFER <%s>", ref.getAnnotation("schemaname"));
                }
                break;
             case KW_DATASET:
                ref = parm.getType() == KW_DATASET ? parm : parm.getImmediateChild(KW_DATASET, null);

                if (ref == null)
                {
                   throw new RuntimeException(String.format("Missing KW_DATASET in: %s", parm.dumpTree()));
                }

                if (!ref.downPath(DATA_SET))
                {
                   throw new RuntimeException(String.format("Missing DATA_SET in: %s", parm.dumpTree()));
                }

                ref = (Aast) ref.getImmediateChild(null, DATA_SET);

                String sig = (String) ref.getAnnotation("db_signature");

                if (sig == null)
                {
                   throw new RuntimeException(String.format("Missing 'db_signature' annotation in: %s",
                                                            ref.dumpTree()));
                }

                cls = String.format("DATASET %s", sig);
                break;
             case KW_DATA_SRC:
                // TODO: as far as I know, this is not possible; dump some data so we can generate an example
                //       with this as parameter and argument
                LOG.log(Level.WARNING, "UNEXPECTED DATA-SOURCE PARAMETER " + parm.dumpTree());
                cls = "DATA-SOURCE";
                break;
          }

          return cls;
       }

       /**
        * Process a method call to extract the signature.
        *
        * @param    call
        *           The OO_METH_* node which is used to extract the signature of the method.
        *
        * @return   An array of the parameter types in signature order from left to right.
        */
       public static ParameterKey[] calculateCallSignature(Aast call)
       {
          ParameterKey[] signature = new ParameterKey[0];

          if (call != null)
          {
             ArrayList<ParameterKey> list = new ArrayList<>();

             // iterate all parameters and extract their types
             Aast child = (Aast) call.getFirstChild();

             if ((call.isAnnotation("oldtype") && (long) call.getAnnotation("oldtype") == KW_NEW) ||
                 call.getType() == KW_THIS_OBJ                                                    ||
                 call.getType() == KW_SUPER)
             {
                child = (Aast) call.getImmediateChild(LPARENS, null);
                child = (Aast) child.getFirstChild();
             }

             Integer parmtype = null;
             int parIdx = 0;
             while (child != null)
             {
                int type = child.getType();
                if (type == CLASS_EVENT || type == KW_PUBLISH)
                {
                   child = (Aast) child.getNextSibling();
                   continue;
                }

                Aast parm = child;

                if (type == KW_INPUT || type == KW_IN_OUT || type == KW_OUTPUT)
                {
                   parmtype = type;
                }
                else if (type == PARAMETER)
                {
                   parm = (Aast) child.getFirstChild();
                   while (parm != null)
                   {
                      type = parm.getType();
                      if (type != KW_INPUT   &&
                          type != KW_IN_OUT  &&
                          type != KW_OUTPUT  &&
                          type != KW_APPEND  &&
                          type != KW_BIND    &&
                          type != KW_BY_REF  &&
                          type != KW_BY_VALUE)
                      {
                         break;
                      }

                      if (type == KW_INPUT || type == KW_IN_OUT || type == KW_OUTPUT)
                      {
                         parmtype = type;
                      }

                      parm = (Aast) parm.getNextSibling();
                   }
                }

                // avoid the optional mode or table parm modifiers
                if (type != KW_INPUT   &&
                    type != KW_IN_OUT  &&
                    type != KW_OUTPUT  &&
                    type != KW_APPEND  &&
                    type != KW_BIND    &&
                    type != KW_BY_REF  &&
                    type != KW_BY_VALUE)
                {
                   String cls = ExpressionConversionWorker.expressionType(parm, false, false);

                   // this is not just buffers, it is also any form of table reference except for table-handle
                   if ("Buffer".equalsIgnoreCase(cls) || "DATASET".equalsIgnoreCase(cls))
                   {
                      cls = translateType(parm.getType(), parm, null);
                   }
                   else
                   {
                      // non-buffers get checked for extent
                      cls = formatExtentName(cls, readExtent(parm));
                   }

                   Aast pref = parm.getType() == OBJECT_INVOCATION ? parm.getChildAt(1) : parm;
                   // constructors always return the expected type, so do not wrap the parameter
                   boolean isCtor = pref.getType() == ProgressParserTokenTypes.FUNC_CLASS &&
                                    ((Long) pref.getAnnotation("oldtype")) == ProgressParserTokenTypes.KW_NEW;
                   if (!isCtor && pref.isAnnotation("dynamic") && (Boolean) pref.getAnnotation("dynamic"))
                   {
                      pref.putAnnotation("wrap_parameter", true);
                      pref.putAnnotation("classname", cls);
                   }

                   if (parmtype == null                &&
                       parm.isAnnotation("is-literal") &&
                       (Boolean) parm.getAnnotation("is-literal"))
                   {
                      parmtype = KW_INPUT;
                   }

                   boolean fromExpression = ExpressionConversionWorker.hasOperators(parm);

                   // the parameter type is not defaulting to INPUT (as in func/proc call cases).
                   // OO method call resolution is done by checking the types first, and if more than
                   // one match found, the modes are checked second.
                   Aast mcall = parm;
                   if (mcall.getType() == OBJECT_INVOCATION)
                   {
                      mcall = parm.getChildAt(1);
                   }
                   boolean dynamicPolyArg = mcall.isAnnotation("dynamic-poly");
                   list.add(new ParameterKey(parmtype, cls, fromExpression, dynamicPolyArg));

                   // reset the parameter type here
                   parmtype = null;

                   parm.putAnnotation("param_index", (long) parIdx);
                   if (parm.getType() == PARAMETER)
                   {
                      Aast ref = parm.getChildAt(parm.getNumImmediateChildren() - 1);
                      ref.putAnnotation("param_index", (long) parIdx);
                   }
                   parIdx = parIdx + 1;
                }

                child = (Aast) child.getNextSibling();
             }

             signature = list.toArray(signature);
          }

          return signature;
       }

       /**
        * Save all the 4GL classes (builtin or business logic), .NET or Java classes encountered
        * during parsing.
        */
       public static void saveClasses()
       {
          WorkArea wa = local.get();
          for (String key : wa.propathClassDicts.keySet())
          {
             ScopedSymbolDictionary dict = wa.propathClassDicts.get(key);
             Iterator<String> iter = dict.symbolSet().iterator();
             while (iter.hasNext())
             {
                String clsName = iter.next();
                ClassDefinition clsDef = (ClassDefinition) dict.getSymbolAtScope(clsName, -1);
                ConversionData.saveClass(clsDef);
             }
          }
       }

       /**
        * Load all legacy (skeleton) classes which were previously parsed.
        *
        * @param    sym
        *           The symbol resolver.
        */
       public static void loadLegacyClasses(SymbolResolver sym)
       {
          if (Configuration.isRuntimeConfig())
          {
             // no-op in runtime conversion mode
             return;
          }

          List<String> classes = ConversionData.listLegacyClasses();
          for (String qname : classes)
          {
             loadClassDefinition(sym, qname);
          }
       }

       /**
        * Load the specified class definition.
        *
        * @param    qname
        *           The legacy OO class qualified name.
        *
        * @return   The found converted class.
        *
        * @throws   ClassNotFoundException
        *           If the class can't be resolved or loaded.
        */
       public static ClassDefinition loadConvertedClassByQname(String qname)
       throws ClassNotFoundException
       {
          String fname = ConversionData.findClassFilename(qname);

          if (fname == null)
          {
             throw new ClassNotFoundException("Can not find source filename for qualfied class " + qname);
          }

          return loadConvertedClass(fname, false);
       }

       /**
        * Load the specified class definition.  This will either create a {@link ClassDefinition}
        * structure for a new class (to be later populated with members by the parser), or will restore
        * an instance from an already-converted class.
        *
        * @param    filename
        *           The file name for the legacy OO class.
        * @param    isNew
        *           Flag indicating that this is a new class (now being parsed), or a previously
        *           converted class.
        *
        * @return   The class definition.
        *
        * @throws   ClassNotFoundException
        *           If the class can't be resolved or loaded.
        */
       public static ClassDefinition loadConvertedClass(String filename, boolean isNew)
       throws ClassNotFoundException
       {
          WorkArea wa = local.get();

          String qname = wa.fname2qname.get(filename);
          if (qname == null)
          {
             qname = ConversionData.findClassQName(filename);
             if (qname == null)
             {
                throw new ClassNotFoundException("Can not find class associated with " + filename);
             }

             wa.fname2qname.put(filename, qname);
          }

          ClassDefinition clsdef = null;

          if (isNew)
          {
             clsdef = getClassDefinition(qname);

             if (clsdef == null)
             {
                throw new ClassNotFoundException("Can not find class " + qname + " from " + filename);
             }
          }
          else
          {
             clsdef = loadClassDefinition(qname);
          }

          return clsdef;
       }

       /**
        * Load the specified class, given by its qualified name.  This will return either a
        * {@link ClassDefinition} instance being parsed in the current run, or load the definition
        * from an already parsed and converted .ast file.  If this file is not found, it will return
        * <code>null</code>
        *
        * @param    qname
        *           The qualified class name.
        *
        * @return   See above.
        */
       public static ClassDefinition loadClassDefinition(String qname)
       {
          return loadClassDefinition(null, qname);
       }

       /**
        * Load the specified class, given by its qualified name.  This will return either a
        * {@link ClassDefinition} instance being parsed in the current run, or load the definition
        * from an already parsed and converted .ast file.  If this file is not found, it will return
        * <code>null</code>
        *
        * @param    sym
        *           The symbol resolver.  Non-null only during parsing.
        * @param    qname
        *           The qualified class name.
        *
        * @return   See above.
        */
       public static ClassDefinition loadClassDefinition(SymbolResolver sym, String qname)
       {
          return loadClassDefinition(sym, qname, true);
       }

       /**
        * Load the specified class, given by its qualified name.  This will return either a
        * {@link ClassDefinition} instance being parsed in the current run, or load the definition
        * from an already parsed and converted .ast file.  If this file is not found, it will return
        * <code>null</code>
        *
        * @param    sym
        *           The symbol resolver.  Non-null only during parsing.
        * @param    qname
        *           The qualified class name.
        * @param    failOnPending
        *           When this flag is set, if the class is pending to be loaded, then throw an exception.
        *
        * @return   See above.
        */
       public static ClassDefinition loadClassDefinition(SymbolResolver sym, String qname, boolean failOnPending)
       {
          WorkArea wa = local.get();

          // this checks for classes converted in the current run
          ClassDefinition clsDef = getClassDefinition(qname);
          if (clsDef != null)
          {
             return clsDef;
          }

          String file = ConversionData.findClassFilename(qname);
          if (file == null || !new File(file + ".ast").exists())
          {
             return null;
          }

          String key = qname.toLowerCase();
          if (wa.pendingLoading.contains(key))
          {
             if (failOnPending)
             {
                throw new RuntimeException("Class " + qname + " is pending loading.");
             }
             return null;
          }

          try
          {
             wa.pendingLoading.add(key);
             clsDef = new ClassDefinition(sym, qname);
          }
          catch (SchemaException e)
          {
             throw new RuntimeException(e);
          }
          finally
          {
             wa.pendingLoading.remove(key);
          }
          wa.classCache.put(qname.toLowerCase(), clsDef);

          return clsDef;
       }

       /**
        * Given a qualified legacy class name, resolve its definition.
        *
        * @param    clsName
        *           The qualified class name.
        *
        * @return   The found definition, or <code>null</code> if it is not yet loaded.
        */
       public static ClassDefinition getClassDefinition(String clsName)
       {
          WorkArea wa = local.get();
          ClassDefinition clsDef = wa.classCache.get(clsName.toLowerCase());
          if (clsDef != null)
          {
             return clsDef;
          }

          for (String key : wa.propathClassDicts.keySet())
          {
             ScopedSymbolDictionary dict = wa.propathClassDicts.get(key);
             clsDef = (ClassDefinition) dict.getSymbolAtScope(clsName, -1);

             if (clsDef != null)
             {
                return clsDef;
             }
          }

          return null;
       }

       /**
        * List the 4GL class/interface files in a given path.
        *
        * @param    path
        *           The directory to search.
        * @param    caseSens
        *           <code>true</code> if the legacy system had a case-sensitive
        *           file-system.
        *
        * @return   The list of files that exist.  If there are no files, the
        *           array will have a 0 length.
        */
       public static String[] getClassFileList(String path, boolean caseSens)
       {
          File dir = new File(path);

          if (!dir.exists() || !dir.isDirectory())
          {
             return new String[0];
          }

          String absPath = null;

          try
          {
             absPath = dir.getCanonicalPath();
          }
          catch (IOException exc)
          {
             LOG.log(Level.SEVERE, "", exc);
          }

          String[] files = DIR_CLASSES.get(absPath);
          if (files == null)
          {
             FileList fl = new FileSpecList(dir, CLASS_SPLAT, true, caseSens);

             files = fl.listFilenames();
             for (int i = 0; i < files.length; i++)
             {
                String l = files[i];
                l = Configuration.normalizeFilename(l);
                files[i] = l;
             }
             DIR_CLASSES.put(absPath, files);
          }

          return files;
       }

       /**
        * Store each element of the list as a key in the map and create a filename
        * as the mapped value (using the path + the key). The keys will be
        * lowercased if case-insensitive matching is configured.
        *
        * @param    map
        *           The map to modify.
        * @param    list
        *           The elements to add.
        * @param    path
        *           The root path in which all elements reside.
        * @param    caseSens
        *           <code>true</code> if the legacy system had a case-sensitive
        *           file-system.
        */
       public static void createClassMappings(Map<String, List<String>> map,
                                              String[]                  list,
                                              String                    path,
                                              boolean                   caseSens)
       {
          path = Configuration.normalizeFilename(path);
          int begin = path.length();
          if (path.equals("."))
          {
             // remove the leading "/" or "\" char
             begin = 2;
          }
          else if (!path.endsWith("\\") && !path.endsWith("/"))
          {
             begin++;
          }

          for (int i = 0; i < list.length; i++)
          {
             String l = list[i];
             int end = l.length() - 4;
             String entry = l.substring(begin, end);

             // only the first entry is kept, in the propath - the others will not be seen
             String key = caseSens ? entry : entry.toLowerCase();

             List<String> propathList = map.get(key);
             if (propathList == null)
             {
                propathList = new ArrayList<>();
                map.put(key, propathList);
             }

             propathList.add(l);
          }
       }

       /**
        * Check if we are in pre-scan mode.
        *
        * @return   <code>true</code> if we are in pre-scan mode.
        */
       public static boolean isPreScan()
       {
          return locate().preScanPass > 0;
       }

       /**
        * Find the converted Java name for a legacy builtin class.
        * <p>
        * On first run, this will go through all FWD implementations of {@link _BaseObject_} in the
        * <code>com.goldencode.p2j.oo</code> package, and map the legacy class name to its definition
        * name.
        *
        * @param    name
        *           The legacy class name.
        *
        * @return   The converted Java name which must be used, or <code>null</code> if FWD does not
        *           provide yet a definition for it.
        */
       static String findConvertedLegacyClass(String name)
       {
          WorkArea wa = locate();
          if (wa.legacyToJava == null)
          {
             wa.legacyToJava = new HashMap<>();

             Reflections reflections = new Reflections("com.goldencode.p2j.oo");
             Set<Class<? extends _BaseObject_>> legacyClasses =
                reflections.getSubTypesOf(_BaseObject_.class);
             for (Class<? extends _BaseObject_> cls : legacyClasses)
             {
                LegacyResource lr = cls.getAnnotation(LegacyResource.class);
                if (lr == null)
                {
                   System.out.println("WARNING: class " + cls +
                                      " doesn't have a LegacyResource annotation!");
                }
                else
                {
                   String clsName = cls.getName();
                   if (cls == BaseObject.class)
                   {
                      clsName = _BaseObject_.class.getName();
                   }
                   wa.legacyToJava.put(lr.resource().toLowerCase(), clsName);

                   // check the method names against NameConverter
                   for (Method m : cls.getDeclaredMethods())
                   {
                      if (m.isAnnotationPresent(LegacySignature.class) &&
                          NameConverter.isInternalMethodName(m.getName()))
                      {
                         String msg = "Skeleton Java method name collides with FWD internal API: " +
                                      cls.getName() + "." + m.toString();
                         LOG.log(Level.SEVERE, msg);
                         System.exit(1);
                      }
                   }
                }
             }
          }

          return wa.legacyToJava.get(name.toLowerCase());
       }

       /**
        * Get the context-local {@link WorkArea} instance.
        *
        * @return   See above.
        */
       private static WorkArea locate()
       {
          return local.get();
       }

       /**
        * Get a brief descriptor of the database type with the specified logical
        * name or alias.  This is for CONVERSION TIME ONLY.  If schema support is
        * not loaded then we assume the database is OK.
        *
        * @param   name
        *          Logical name or alias of the target database.
        *
        * @return  The string "PROGRESS" or unknown value if <code>name</code>
        *          is not a recognized logical name or alias.  If the schema
        *          dictionary is not loaded, then this will simply return
        *          "PROGRESS".
        */
       public character dbtype(character name)
       {
          if (schemaDict != null && name != null)
          {
             boolean valid = (!name.isUnknown() && schemaDict.isDatabase(name.toStringMessage()));

             if (!valid)
             {
                return (new character());
             }
          }

          return new character("PROGRESS");
       }

       /**
        * Check if this numeric literal can fit 32 bits or 64 bits.
        *
        * @param    ast
        *           The NUM_LITERAL node.
        */
       public void annotateNumericLiteral(Aast ast)
       {
          String text = ast.getText();

          if (text.endsWith("+") || text.endsWith("-"))
          {
             ast.putAnnotation("original-text", text);
             if (text.endsWith("-"))
             {
                text = "-" + text;
             }
             // remove last
             text = text.substring(0, text.length() - 1);
             ast.setText(text);
          }

          // following approach is duplicated from cleanup.rules NUM_LITERAL rule
          Long val = null;
          if (ast.getType() == NUM_LITERAL)
          {
             val = ExpressionConversionWorker.parseLongQuiet(text);
          }
          else if (ast.getType() == HEX_LITERAL)
          {
             if (ExpressionConversionWorker.isHexLiteralValid(text))
             {
                decimal dec = decimal.fromUnsignedHexLiteral(text);
                if (CompareOps._isEqual(dec, dec.longValue()))
                {
                   val = dec.longValue();
                }
                else
                {
                   // this is actually a DEC_LITERAL
                }
             }
          }

          if (val != null)
          {
             if (val < -2147483648l || val > 2147483647l)
             {
                ast.putAnnotation("use64bit", true);
             }
          }
       }

       /**
        * Set the 'in-method body' state.
        *
        * @param    inMethod
        *           The new state for the {@link #inMethod} flag.
        */
       public void setInMethod(boolean inMethod)
       {
          this.inMethod = inMethod;
       }

       /**
        * Disable or enable unique field matches when abbreviations are used - if ambiguous, it will
        * return the first found field which matches the prefix.  Used by CAN-FIND statements.
        *
        * @param    unique
        *           When <code>true</code>, it forces an unique match; otherwise, first field is
        *           return, when abbreviations are used.
        *
        * @return   The previous flag state.
        */
       public boolean setUniqueFieldMatch(boolean unique)
       {
          return schemaDict.setUniqueFieldMatch(unique);
       }

       /**
        * Set the {@link #indexFieldSearch} flag, marking the beginning (and possibility) for BY fields
        * to be matched against an unique index.
        */
       public void startIndexFieldSearch()
       {
          indexFieldSearch = true;
       }

       /**
        * Terminate the index field search; this means that an {@link #foundUniqueIndex unique index}
        * was matched or that we encountered a condition which doesn't allow unique index matching
        * (i.e. currently collected {@link #sortFields fields} are not a prefix for any
        * {@link #indexes} in {@link #indexTable}, BREAK clause was used, multiple tables in the
        * query, etc.
        */
       public void endIndexFieldSearch()
       {
          if (!indexFieldSearch)
          {
             foundUniqueIndex = false;
             return;
          }

          indexFieldSearch = false;
          useIndexFieldSearch = false;
          foundUniqueIndex = false;
          indexTable = null;
          indexBuffer = null;
          sortFields = null;
          indexes = null;
       }

       /**
        * Save the {@link #indexTable table} used to match the BY sort order against unique indexes.
        * If there are no unique indexes in this table, then {@link #endIndexFieldSearch()}.
        * <p>
        * All indexes collected in {@link #indexes} will be unique and no other index will match as a
        * prefix for another index.
        *
        * @param    recordAst
        *           The table's AST reference.
        */
       public void setIndexFieldSearchTable(Aast recordAst)
       {
          if (!indexFieldSearch)
          {
             return;
          }

          Aast fieldRef = (Aast) recordAst.getFirstChild();
          this.indexTable = (String) fieldRef.getAnnotation("schemaname");
          this.indexBuffer = (String) fieldRef.getAnnotation("bufname");
          // load the unique indexes in this table
          Aast table = null;

          try
          {
             table = schemaDict.getTable(indexTable);
          }
          catch (SchemaException e)
          {
             // TODO: OO requires lookup of temp-table up the hierarchy
             endIndexFieldSearch();
             return;
          }

          indexes = schemaDict.getUniqueIndexes(table);

          if (indexes.isEmpty())
          {
             // no unique indexes, nothing to match, so disable this
             endIndexFieldSearch();
          }
          else
          {
             sortFields = new ArrayList<>();
          }
       }

       /**
        * Try to collect the expression associated with the BY clause, only if this is a field
        * reference and if,  together with {@link #sortFields}, match a prefix in {@link #indexes},
        * <p>
        * Otherwise, the {@link #endIndexFieldSearch() match is terminated}.  IF an unique index
        * is fully matched, then {@link #foundUniqueIndex} flag is set, to allow any subsequent BY
        * clauses to match garbage.
        *
        * @param    expr
        *           The expression associated with the BY clause.
        */
       public void collectSortField(Aast expr)
       {
          if (!indexFieldSearch || !useIndexFieldSearch)
          {
             return;
          }

          int etype = expr.getType();
          if (etype == EXPRESSION)
          {
             etype = expr.getFirstChild().getType();
          }
          if (!(etype >= BEGIN_FIELDTYPES && etype <= END_FIELDTYPES))
          {
             // complex expression, can't use, fallback to field search...
             endIndexFieldSearch();
          }
          else
          {
             // colect this for further analysis
             sortFields.add(expr);

             try
             {
                if (indexPrefixMatch(true))
                {
                   // found an unique index, end index matching.
                   // every other BY clause will be marked with 'drop=true'
                   endIndexFieldSearch();

                   // we can start matching garbage from now on
                   foundUniqueIndex = true;
                }
                else if (!indexPrefixMatch(false))
                {
                   // currently collected BY fields are not a prefix for any index, terminate search
                   endIndexFieldSearch();
                }
             }
             catch (SchemaException e)
             {
                LOG.log(Level.WARNING,"", e);
             }
          }
       }

       /**
        * Enable or disable field matching against {@link #indexes}, by setting the
        * {@link #useIndexFieldSearch} flag.  This is active only for the duration of
        * {@link ProgressParser#sort_order_clause() a BY clause}.
        *
        * @param    use
        *           Flag state.
        */
       public void useIndexFieldSearch(boolean use)
       {
          if (!indexFieldSearch)
          {
             return;
          }

          this.useIndexFieldSearch = use;
       }

       /**
        * Check if an unique index was found with the current BY clauses, or not.
        *
        * @return   The state of the {@link #foundUniqueIndex} flag.
        */
       public boolean isFoundUniqueIndex()
       {
          return foundUniqueIndex;
       }

       /**
        * Set the {@link #localLookup flag} to allow check for a data member or method which is
        * looked up without a qualifier.
        *
        * @param    state
        *           The new flag state.
        */
       public void setLocalLookup(boolean state)
       {
          this.localLookup = state;
       }

       /**
        * Set the {@link #classLookup} flag.  A <code>true</code> value will limit variable lookup
        * only to class members (as we are in a object chain).
        *
        * @param    state
        *           The new flag state.
        */
       public void setClassLookup(boolean state)
       {
          this.classLookup = state;
       }

       /**
        * Get the {@link #classLookup} flag.
        *
        * @return   See above.
        */
       public boolean isClassLookup()
       {
          return classLookup;
       }

       /**
        * Enables schema dictionary lookups when they are disabled at construction
        * and provides external control or sourcing over the schema dictionary.
        *
        * @param    schemaDict
        *           The schema dictionary to use.
        */
       public void setSchemaDictionary(SchemaDictionary schemaDict)
       {
          this.schemaDict = schemaDict;
       }

       /**
        * Gets the <code>PROPATH</code>.
        *
        * @return   The current <code>PROPATH</code> to honor.
        */
       public String[] getPropath()
       {
          return propath;
       }

       /**
        * Sets the <code>PROPATH</code>.
        *
        * @param    propath
        *           The current <code>PROPATH</code> to honor or <code>null</code>
        *           for the default of ".".
        */
       public void setPropath(String[] propath)
       {
          this.propath = (propath == null) ? new String[] { "." } : propath;
       }

       /**
        * Adds a <code>Keyword</code> to the keyword dictionary.
        *
        * @param   word
        *          <code>Keyword</code> to add.
        */
       public void addKeyword(Keyword word)
       {
          kwDict.addKeyword(word);
       }

       /**
        * Removes a <code>Keyword</code> from the keyword dictionary.
        *
        * @param   word
        *          <code>Keyword</code> to remove.
        */
       public void removeKeyword(Keyword word)
       {
          kwDict.deleteKeyword(word);
       }

       /**
        * Searches for a <code>Keyword</code> in the keyword dictionary, based
        * on a specific key.
        *
        * @param   key
        *          Text string to match.
        * @return  The <code>Keyword</code> that was found or null if no match
        *          was found.
        */
       public Keyword lookupKeyword(String key)
       {
          return kwDict.lookupKeyword(key);
       }

       /**
        * Increases the scoping level of the variable and widget dictionaries by
        * 1 level.  All symbols must be unique within a scoping level.
        */
       public void addScope()
       {
          varDict.addScope(null);
          widgetDict.addScope(null);
          fieldWidgetDict.addScope(null);
          useDictExps.addScope(null);
          dsrcDict.addScope(null);
          dsetDict.addScope(null);
       }

       /**
        * Decreases the scoping level of the variable and widget dictionaries by
        * 1 level.  All symbols previously defined in the top-most scoping level
        * are lost and will no longer be found during lookups.
        */
       public void deleteScope()
       {
          varDict.deleteScope();
          widgetDict.deleteScope();
          fieldWidgetDict.deleteScope();
          useDictExps.deleteScope();
          dsetDict.deleteScope();
          dsrcDict.deleteScope();
       }

       /**
        * Persist schema dictionary information which is specific to the source
        * file currently being parsed.
        *
        * @param    srcFile
        *           Name of the 4GL source code file containing the schema
        *           definitions being persisted.
        * @param    schemaFile
        *           Name of schema persistence file.
        *
        * @throws   AstException
        *           if any error occurs writing persistence file.
        */
       public void persistSchemaData(String srcFile, String schemaFile)
       throws AstException
       {
          if (schemaDict != null)
          {
             schemaDict.persist(srcFile, schemaFile);
          }
       }

       /**
        * Adds a variable name and associated token type to the appropriate scope
        * of the variable dictionary, based on the content of the passed in
        * <code>Variable</code> wrapper object.
        *
        * @param    global
        *           Add to global scope if <code>true</code>.
        * @param    var
        *           Variable to add.
        */
       public void addVariable(boolean global, Variable var)
       {
          addWrappedWorker(varDict, global, var.getName(), var);
       }

       /**
        * Adds a widget name and associated token type to the appropriate scope
        * of the widget dictionary, based on the content of the passed in
        * <code>Variable</code> wrapper object.
        *
        * @param    global
        *           Add to global scope if <code>true</code>.
        * @param    wid
        *           Widget as represented in a <code>Variable</code> object.
        */
       public void addWidget(boolean global, Variable wid)
       {
          addWrappedWorker(widgetDict, global, wid.getName(), wid);
       }

       /**
        * Helper to load variable, widget and frame dictionaries based on a list
        * of <code>Variable</code> objects. This processing will load all 3
        * dictionaries from the same list, however it must be noted that adding
        * a variable does NOT result in a matching widget of the same name in
        * the widget dictionary (even though this is essentially what happens
        * when the parser adds new variables to the variable dictionary). To
        * achieve this same effect, one must add multiple <code>Variable</code>
        * objects to the list as needed.
        * <p>
        * A field can be added to this lookup by using a field token type instead
        * of a variable token type. This will allow an ambiguous field reference
        * to be made unambiguous since variable lookups are processed
        * <b>before</b> any schema dictionary lookups.
        *
        * @param    varlist
        *           List of variables, widgets, frames or fields to add to the
        *           appropriate dictionary.
        */
       public void loadVariableSymbols(Variable[] varlist)
       {
          for ( int j = 0; j < varlist.length; j++ )
          {
             int type = varlist[j].getTokenType();

             if (
                  (type >= ProgressParser.BEGIN_VARTYPES   && type <= ProgressParser.END_VARTYPES) ||
                  (type >= ProgressParser.BEGIN_FIELDTYPES && type <= ProgressParser.END_FIELDTYPES)
                )
             {
                addVariable(true, varlist[j]);
             }
             else if (type == ProgressParser.WID_FRAME)
             {
                addFrame(varlist[j].getName(), ProgressParser.WID_FRAME);
             }
             else if (type >= ProgressParser.BEGIN_WIDGETS &&
                      type <= ProgressParser.END_WIDGETS )
             {
                addWidget(true, varlist[j]);
             }
          }
       }

       /**
        * Adds a variable name and associated token type to the global scope
        * of the variable dictionary.  Names in this scope can be hidden by
        * duplicate names added to scopes that are closer to the top of the
        * stack of scopes.
        *
        * @param    name
        *           Variable name to add.
        * @param    tokenType
        *           Lexer and Parser token type associated with the name.
        */
       public void addGlobalVariable(String name, int tokenType)
       {
          addGlobalVariable(name, tokenType, null);
       }

       /**
        * Adds a variable name and associated token type to the global scope
        * of the variable dictionary.  Names in this scope can be hidden by
        * duplicate names added to scopes that are closer to the top of the
        * stack of scopes.
        *
        * @param    name
        *           Variable name to add.
        * @param    tokenType
        *           Lexer and Parser token type associated with the name.
        * @param    cls
        *           Fully qualified class name of the object instance represented
        *           or <code>null</code> if this variable does not represent an
        *           object instance.
        */
       public void addGlobalVariable(String name, int tokenType, String cls)
       {
          Variable var = new Variable(name, tokenType, cls);
          var.setBuiltin(true);

          addWrappedWorker(varDict, true, name, var);
       }

       /**
        * Removes any global variable associated with this <code>Keyword</code> from the variable
        * dictionary.  This is only safe to call BEFORE real parsing begins.
        *
        * @param   word
        *          <code>Keyword</code> to remove (if it exists).
        */
       public void removeGlobalVariable(Keyword word)
       {
          varDict.deleteSymbol(word.getFullText());
       }

       /**
        * Add a new accum builtin variable, to the {@link #accumDict} dictionary.
        *
        * @param    name
        *           The variable name.
        * @param    tokenType
        *           The variable type.
        */
       public void addAccumVariable(String name, int tokenType)
       {
          if (accumDict.getSymbolAtScope(name, 0) == null)
          {
             addWrappedWorker(accumDict, false, name, new Variable(name, tokenType, null));
          }
       }

       /**
        * Add a new inner-block scope.
        */
       public void addInnerBlockScope()
       {
          accumDict.addScope(null);
       }

       /**
        * Pop the last inner-block scope and copy any definitions into the parent/containing scope.
        */
       public void deleteInnerBlockScope()
       {
          accumDict.deleteScope(true);
       }

       /**
        * Adds a variable name and associated token type to the current scope
        * of the variable dictionary.  This is the same as the top-most scope.
        * Names in this scope can hide duplicate names added to scopes that are
        * lower on the stack of scopes.
        *
        * @param    name
        *           Variable name to add.
        * @param    tokenType
        *           Lexer and Parser token type associated with the name.
        * @param    cls
        *           Fully qualified class name of the object instance represented
        *           or <code>null</code> if this variable does not represent an
        *           object instance.
        *
        * @return   The newly created instance.
        */
       public Variable addLocalVariable(String name, int tokenType, String cls)
       {
          Variable var = null;

          if (varDict.getSymbolAtScope(name, 0) == null)
          {
             var = new Variable(name, tokenType, cls);

             addWrappedWorker(varDict, false, name, var);
          }

          return var;
       }

       /**
        * Adds a menu name and associated token type/options to the menu dictionary
        * based on a referenced variable in a <code>LIKE</code> clause.
        *
        * @param    name
        *           Menu name to add.
        * @param    ast
        *           DEFINE_MENU or DEFINE_SUB_MENU AST node.
        */
       public void addMenuLike(String name, String oldname, Aast ast)
       {
          if (ast == null)
          {
             throw new NullPointerException("Root DEFINE_MENU or DEFINE_SUB_MENU statement is null");
          }

          int type = lookupMenu(oldname);
          Variable var   = new Variable(name, type, null);

          Variable like  = (Variable) lookupWrapped(menuDict, oldname);
          Aast     fopts = like.getOptions(0);

          if (fopts != null)
          {
             var.copyDefaultOptions(fopts);

             Collection<Integer> types;
             if (ast.getType() == DEFINE_MENU)
             {
                types = MENU_CHILDREN_TYPES;
             }
             else if (ast.getType() == DEFINE_SUB_MENU)
             {
                types = SUBMENU_CHILDREN_TYPES;
             }
             else
             {
                throw new IllegalArgumentException("Unsupported root ast type");
             }

             // copy children asts.
             for (Integer astType : types)
             {
                Variable.overrideIfNotPresent(fopts, ast, astType);
             }

             // copy annotations of root ast.
             for (Iterator<String> it = fopts.annotationKeys(); it.hasNext(); )
             {
                String key = it.next();
                if (ast.getAnnotation(key) == null)
                {
                   AnnotatedAst anast = (AnnotatedAst)ast;
                   Object ann = fopts.getAnnotation(key);
                   if (ann instanceof String)
                      anast.putAnnotation(key, (String)ann);
                   if (ann instanceof Double)
                      anast.putAnnotation(key, (Double)ann);
                   if (ann instanceof Long)
                      anast.putAnnotation(key, (Long)ann);
                   if (ann instanceof Boolean)
                      anast.putAnnotation(key, (Boolean)ann);
                }
             }
          }

          if (menuDict.getSymbolAtScope(name, 0) == null)
          {
             addWrappedWorker(menuDict, false, name, var);
          }
       }

       /**
        * Adds a variable name and associated token type/options to the current
        * scope of the variable dictionary based on a referenced variable or
        * schema field in a <code>LIKE</code> clause.  This is the same as the
        * top-most scope.  Names in this scope can hide duplicate names added to
        * scopes that are lower on the stack of scopes.
        *
        * @param    name
        *           Variable name to add.
        * @param    ast
        *           AST node containing the like clause.
        */
       public void addLocalVariableLike(String name, Aast ast)
       {
          int     ltype   = ast.getType();
          int     type    = ltype;
          String  oldname = ast.getText();
          int     extent  = getExtentSubscript(ast);
          boolean isFld   = ltype > BEGIN_FIELDTYPES && ltype < END_FIELDTYPES;
          String  cls     = null;

          // the local variable type must be converted to the proper
          // VAR_ variant *if* this was a like FIELD_...
          switch (ltype)
          {
             case FIELD_CHAR:
                type = VAR_CHAR;
                break;
             case FIELD_CLASS:
                type = VAR_CLASS;
                // class fields must propagate the fully qualified class name
                cls  = (String) ast.getAnnotation("qualified");
                break;
             case FIELD_COM_HANDLE:
                type = VAR_COM_HANDLE;
                break;
             case FIELD_DATE:
                type = VAR_DATE;
                break;
             case FIELD_DATETIME:
                type = VAR_DATETIME;
                break;
             case FIELD_DATETIME_TZ:
                type = VAR_DATETIME_TZ;
                break;
             case FIELD_DEC:
                type = VAR_DEC;
                break;
             case FIELD_HANDLE:
                type = VAR_HANDLE;
                break;
             case FIELD_INT:
                type = VAR_INT;
                break;
             case FIELD_INT64:
                type = VAR_INT64;
                break;
             case FIELD_LOGICAL:
                type = VAR_LOGICAL;
                break;
             case FIELD_RAW:
                type = VAR_RAW;
                break;
             case FIELD_RECID:
                type = VAR_RECID;
                break;
             case FIELD_ROWID:
                type = VAR_ROWID;
                break;
             default:
                // this MUST be at the end of the list of fields, it will match
                // BLOB and CLOB which can't be variable types
                if (isFld)
                {
                   String err = String.format("Unexpected field type %s!",
                                              ast.getDescriptiveTokenText());
                   throw new RuntimeException(err);
                }
                else
                {
                   // TODO: OM: is this correct when creating LIKE variable (copy its true type)?
                   type = lookupVariable(oldname);
                }
                break;
          }

          // class variables must propagate the fully qualified class name
          if (ltype == VAR_CLASS)
             cls = (String) ast.getAnnotation("qualified");

          Variable var   = new Variable(name, type, cls);
          Aast     fopts = null;

          if (isFld)
          {
             // we must pattern the new variable off a referenced field
             // obtain the field options AST from the schema
             try
             {
                SchemaHelper<Aast> fcopy = (SchemaDictionary dict) ->
                {
                   Aast fld = null;

                   // getFieldCopy() is not safe to call if the field doesn't exist
                   if (dict.isField(oldname))
                   {
                      fld = dict.getFieldCopy(oldname, extent);
                   }

                   return fld;
                };

                Predicate<Aast> notNull = (Aast val) -> { return (val == null); };

                fopts = processHierarchy(fcopy, notNull);
             }
             catch (SchemaException exc)
             {
                LOG.log(Level.WARNING,"", exc);
             }
          }
          else
          {
             // we must pattern the variable off another variable
             Variable like = (Variable) lookupWrapped(varDict, oldname);
             fopts = like.getOptions(extent);
          }

          if (fopts != null)
          {
             var.copyDefaultOptions(fopts);
          }

          if (varDict.getSymbolAtScope(name, 0) == null)
          {
             addWrappedWorker(varDict, false, name, var);
          }
       }

       /**
        * Save off the variable dictionary definitions for the current scope into a cache.
        *
        * @param    name
        *           The name by which the saved definitions can be restored.
        */
       public void saveLocalVariables(String name)
       {
          varDefCache.put(name.toUpperCase(), varDict.entrySet(0));
       }

       /**
        * Restore any previously cached variable dictionary definitions into the current scope.
        * <p>
        * Any cached definitions will be removed from the cache.
        *
        * @param    name
        *           The name by which the saved definitions can be located.
        *
        * @return   <code>true</code> if there were variable definitions restored.
        */
       public boolean restoreLocalVariables(String name)
       {
          boolean nonEmpty = false;

          Set<Map.Entry> locals = varDefCache.remove(name.toUpperCase());

          if (locals != null)
          {
             Iterator<Map.Entry> iter = locals.iterator();

             while (iter.hasNext())
             {
                Map.Entry entry = iter.next();
                varDict.addEntry(false, entry.getKey(), entry.getValue());
                nonEmpty = true;
             }
          }

          return nonEmpty;
       }

       /**
        * Remove any previously cached variable dictionary definitions without restoring them
        * to the current scope.
        *
        * @param    name
        *           The name by which the saved definitions can be located.
        */
       public void removeLocalVariables(String name)
       {
          varDefCache.remove(name.toUpperCase());
       }

       /**
        * Inspect the variable definition and write any non-default options into
        * the variable object.
        *
        * @param    name
        *           The variable name which must have its options set.
        * @param    ast
        *           The tree to inspect which defines a Progress variable.
        */
       public void setVariableOptions(String name, Aast ast)
       {
          Variable var = lookupLocalDef(name);

          if (var == null || ast == null)
          {
             // nothing to do
             return;
          }

          // the KW_LIKE should already have been processed by now (in the
          // addLocalVariableLike() so that defaults are set from that
          // variable/field and then can be overridden by explicit options
          // so now we process the explicit overrides

          var.setOptions(ast);
       }

       /**
        * Stores all non-standard options as annotations to the AST passed as
        * a parameter if this is the original definition, otherwise it stores
        * a subset of values, most important of which is the cross-reference
        * index to allow the original definition to be easily identified and
        * accessed from a reference.  If an option has the default value, it is
        * not saved as an annotation.
        *
        * @param    name
        *           The variable name which must have its options annotated.
        * @param    ast
        *           The AST to annotate.
        * @param    original
        *           <code>true</code> if this is to annotate the AST of the
        *           original definition, <code>false</code> if the AST is only
        *           a reference.
        */
       public void annotateVariableOptions(String name, Aast ast, boolean original)
       {
          annotateVariableOptions(null, false, name, ast, original);
       }

       /**
        * Stores all non-standard options as annotations to the AST passed as
        * a parameter if this is the original definition, otherwise it stores
        * a subset of values, most important of which is the cross-reference
        * index to allow the original definition to be easily identified and
        * accessed from a reference.  If an option has the default value, it is
        * not saved as an annotation.
        *
        * @param    cname
        *           The class in which to lookup the variable or <code>null</code> for the current
        *           class def.
        * @param    isStatic
        *           <code>true</code> to force a static lookup.
        * @param    name
        *           The variable name which must have its options annotated.
        * @param    ast
        *           The AST to annotate.
        * @param    original
        *           <code>true</code> if this is to annotate the AST of the
        *           original definition, <code>false</code> if the AST is only
        *           a reference.
        */
       public void annotateVariableOptions(String  cname,
                                           boolean isStatic,
                                           String  name,
                                           Aast    ast,
                                           boolean original)
       {
          java.util.function.Function<String, Variable> lookup = (clsName) ->
          {
             ClassDefinition cls = null;
             int access = KW_PRIVATE;
             if (clsName != null)
             {
                access = calcAccessMode(clsName);
                cls = lookupClass(clsName);
             }
             else
             {
                cls = getCurrentClassDef();
             }
             return cls.lookupVariableWrapper(name, access, isStatic);
          };

          annotateVariableOptions(cname, isStatic, name, ast, original,
                                  lookupLocalDef(name), lookup);
       }

       /**
        * Stores all non-standard options as annotations to the AST passed as
        * a parameter if this is the original definition, otherwise it stores
        * a subset of values, most important of which is the cross-reference
        * index to allow the original definition to be easily identified and
        * accessed from a reference.  If an option has the default value, it is
        * not saved as an annotation.
        *
        * @param    cname
        *           The class in which to lookup the variable or <code>null</code> for the current
        *           class def.
        * @param    isStatic
        *           <code>true</code> to force a static lookup.
        * @param    name
        *           The variable name which must have its options annotated.
        * @param    ast
        *           The AST to annotate.
        * @param    original
        *           <code>true</code> if this is to annotate the AST of the
        *           original definition, <code>false</code> if the AST is only
        *           a reference.
        * @param    localVar
        *           The local variable definition.
        * @param    clsLookup
        *           IF set, a function to lookup this member in a legacy class definition.
        */
       public void annotateVariableOptions(String  cname,
                                           boolean isStatic,
                                           String  name,
                                           Aast    ast,
                                           boolean original,
                                           Variable localVar,
                                           java.util.function.Function<String, Variable> clsLookup)
       {
          Variable var = null;
          ClassDefinition cls = cname == null ? getCurrentClassDef() : lookupClass(cname);

          // look for local definitions (not class members) first, but only if we are in the same
          // class as being (pre)scanned
          ClassDefinition current = getCurrentClassDef();
          boolean sameClass = !classLookup    &&
                              cls != null     &&
                              current != null &&
                              cls.getName().equals(current.getName());
          if (sameClass)
          {
             var = localVar;
          }

          if (var == null && cls != null)
          {
             // look for a class member, otherwise
             var = clsLookup.apply(cname);
          }

          if (var == null && !sameClass)
          {
             // fallback to local variables
             var = localVar;
          }

          // if called from the ProgressExpressionEvaluator (to evaluate an
          // expression) the normal variable dictionary processing is bypassed
          // we annotate
          if (var != null)
          {
             var.annotateOptions(ast, original, inMethod, this);
          }
       }

       /**
        * Creates a new field AST node based on a qualified or unqualified
        * field name (must be unambiguous) and sets the token type, text and
        * all annotations as needed.  This centralizes a service which is
        * normally handled by the parser directly.  It uses the
        * {@link #annotateField} method for the core of the lookup and
        * annotation logic.
        * <p>
        * No table promotion will occur based on this field name, even if it
        * is a qualified name.
        *
        * @param    name
        *           The field name from which to obtain options and type.
        *
        * @return   The created AST node.
        */
       public Aast createField(String name)
       {
          ProgressAst field = new ProgressAst();

          field.setText(name);
          annotateField(field, null, true, false);

          return field;
       }

       /**
        * Stores common data (schemaname, bufname, recordtype) as well as
        * all non-standard options as annotations to the AST passed as
        * a parameter.  If an option has the default value, it is not saved as
        * an annotation.  Optionally sets the type of the given AST based on
        * the field found in the schema dictionary.  The field will be looked
        * up in the dictionary based on the text of the AST node and there
        * must be no ambiguity.
        * <p>
        * If the field cannot be found due to ambiguity (more than 1 match), the
        * method will return with no resulting changes.
        * <p>
        * If no such field exists, some default annotations will be set to
        * "unknown".
        *
        * @param    field
        *           The AST to annotate.
        * @param    table
        *           If the field's table is known, this is the name-node for that table.
        *           Passing this will narrow the lookup just to that table. Pass <code>null</code>
        *           to do a normal schema namespace lookup (with all the ambiguity and other
        *           problems that could occur).
        * @param    setType
        *           If <code>true</code>, the AST node will have its type set
        *           based on the field found in the schema dictionary.
        * @param    promote
        *           <code>true</code> to force the promotion (in the schema
        *           dictionary) of the table if the field's name is qualified.
        */
       public void annotateField(Aast field, Object table, boolean setType, boolean promote)
       {
           annotateField(field, table, setType, promote, false);
       }

       /**
        * Stores common data (schemaname, bufname, recordtype) as well as
        * all non-standard options as annotations to the AST passed as
        * a parameter.  If an option has the default value, it is not saved as
        * an annotation.  Optionally sets the type of the given AST based on
        * the field found in the schema dictionary.  The field will be looked
        * up in the dictionary based on the text of the AST node and there
        * must be no ambiguity.
        * <p>
        * If the field cannot be found due to ambiguity (more than 1 match), the
        * method will return with no resulting changes.
        * <p>
        * If no such field exists, some default annotations will be set to
        * "unknown".
        *
        * @param    field
        *           The AST to annotate.
        * @param    table
        *           If the field's table is known, this is the name-node for that table.
        *           Passing this will narrow the lookup just to that table. Pass <code>null</code>
        *           to do a normal schema namespace lookup (with all the ambiguity and other
        *           problems that could occur).
        * @param    setType
        *           If <code>true</code>, the AST node will have its type set
        *           based on the field found in the schema dictionary.
        * @param    promote
        *           <code>true</code> to force the promotion (in the schema
        *           dictionary) of the table if the field's name is qualified.
        * @param    preferDefaultBuffer
        *           <code>true</code> allow unqualified fields to use default buffer.
        */
       public void annotateField(Aast field,
                                 Object table,
                                 boolean setType,
                                 boolean promote,
                                 boolean preferDefaultBuffer)
       {
          String ftext = field.getText();
          String fbuffer = null;
          if (indexFieldSearch && useIndexFieldSearch && ftext.indexOf('.') == -1)
          {
             // we know for sure the field is part of the 'indexTable' (see isField(String))
             ftext = indexTable + "." + ftext;
             fbuffer = indexBuffer;
          }
          NameNode tableNode = (NameNode) table;
          FieldInfo finfo = lookupFieldInfo(tableNode, ftext, preferDefaultBuffer);
          Aast      fback = finfo.getAst();

          // make sure we found the backing field
          if (fback != null)
          {
             if (setType)
             {
                field.setType(fback.getType());
             }

             // now store an annotation with the fully qualified name and
             // the shorted unabbreviated unique buffer name
             String fullname = finfo.getQualified();
             String bufname  = fbuffer == null ? finfo.getBuffer() : fbuffer;
             String dbname   = finfo.getDatabase();

             // was the original reference qualified?
             if (promote && ftext.indexOf('.') != -1)
             {
                // qualified references cause table promotion
                promoteTableName(bufname, false, false);
             }

             if (fullname != null)
             {
                field.putAnnotation("schemaname", fullname);
             }

             if (bufname != null)
             {
                field.putAnnotation("bufname", bufname);
             }

             if (dbname != null)
             {
                field.putAnnotation("dbname", dbname);
             }

             if (tableNode != null)
             {
                Aast tableAst = tableNode.getAst();
                if (tableAst.getType() == BUFFER)
                {
                   tableAst = tableAst.getParent();
                }
                Aast index = tableAst.getImmediateChild(INDEX, null);
                indexLoop: while (index != null)
                {
                   Aast idxProps = index.getImmediateChild(PROPERTIES, null);
                   if (idxProps != null && idxProps.getImmediateChild(KW_WORD_IDX, null) != null)
                   {
                      Aast prop = index.getImmediateChild(INDEX_FIELD, null);
                      while (prop != null)
                      {
                         if (ftext.equalsIgnoreCase(prop.getText()) ||
                             ftext.toLowerCase().endsWith("." + prop.getText().toLowerCase()))
                         {
                            // found the WORD component
                            field.putAnnotation("word-indexed", true);
                            break indexLoop; // skip the other indices, the field is marked from now on
                         }

                         prop = index.getImmediateChild(INDEX_FIELD, prop); // next property
                      }
                   }

                   index = tableAst.getImmediateChild(INDEX, index); // process next index
                }
             }

             // store the backing construct (this will be TABLE, TEMP_TABLE or WORK_TABLE)
             int backing = finfo.getRecordType();
             if (backing != -1)
             {
                field.putAnnotation("recordtype", Long.valueOf(backing));
             }

             int    newtype = field.getType();
             String cls     = (newtype == FIELD_CLASS) ? ROOT_OBJ_NAME : null;

             // temporary variable object to be our worker to annotate
             Variable var = new Variable(ftext, newtype, cls);

             // we must pattern the new variable off a referenced field
             var.copyDefaultOptions(fback);
             var.annotateOptions(field, true, inMethod, this);

             // link field with the backing node
             Long defidLow = (Long) fback.getAnnotation("fdefid-low");
             Long defidHigh = (Long) fback.getAnnotation("fdefid-high");

             if (defidLow != null && defidHigh != null)
             {
                field.putAnnotation("frefid-low", defidLow);
                field.putAnnotation("frefid-high", defidHigh);
             }
          }
          else
          {
             // if the FIELD_ type was set via a hint or some other
             // unexpected manner, then this will get triggered...
             field.putAnnotation("schemaname", "unknown field");
             field.putAnnotation("bufname", "unknown buffer");
             field.putAnnotation("recordtype", Long.valueOf(-1));
          }
       }

       /**
        * Searches the variable dictionary for a symbol matching the passed
        * key or matching a keyword's full text if the symbol matches a language
        * keyword.  This lookup starts at the top-most scoping level and descends
        * through each scope level on the stack until a match is found. The
        * token type of the first match found is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   actual
        *          Actual text string to match which may be a language keyword
        *          abbreviation.
        *
        * @return  The token type that was found or -1 if no match was found.
        */
       public int lookupVariable(String actual)
       {
          int varRef = lookupWrappedType(lookupWrapped(accumDict, actual));

          if (varRef == -1)
          {
             varRef = lookupWrappedType(lookupWrapped(varDict, actual));
          }

          return varRef;
       }

       /**
        * Searches a dictionary containing {@link TokenDataWrapper} objects for a
        * symbol matching the passed key or matching a keyword's full text if the
        * symbol matches a language keyword.  This lookup starts at the top-most
        * scoping level and descends through each scope level on the stack until
        * a match is found. The wrapped object of the first match found is
        * returned.
        *
        * @param   ssd
        *          The dictionary to search.
        * @param   actual
        *          Actual text string to match which may be a language keyword
        *          abbreviation.
        *
        * @return  The wrapped object that was found or <code>null</code> if no
        *          match was found.
        */
       public TokenDataWrapper lookupWrapped(ScopedSymbolDictionary ssd, String actual)
       {
          if (ssd == null)
          {
             // this can be only for runtime conversion
             return null;
          }

          Keyword          kw      = lookupKeyword(actual);
          TokenDataWrapper wrapper = null;

          if (kw != null)
          {
             // the actual text matches a keyword
             if (kw.isReserved())
             {
                // since this is a reserved keyword, this must be a
                // built-in variable/function
                wrapper = (TokenDataWrapper) ssd.lookupSymbol(kw.getFullText());
             }
             else
             {
                // this is an unreserved keyword, this MAY be a
                // built-in variable/function BUT we must allow a user-defined
                // name to take precedence (note that if the actual text is
                // is the same as the full keyword text, then we may still
                // get a match here that is the built-in, assuming no
                // user-defined name exists --> that is OK too)
                wrapper = (TokenDataWrapper) ssd.lookupSymbol(actual);

                // check if there was a match to the actual text
                if (wrapper == null)
                {
                   // match the full text of the keyword with the built-in
                   wrapper = (TokenDataWrapper) ssd.lookupSymbol(kw.getFullText());
                }
             }
          }

          if (wrapper == null)
          {
             // there is no keyword or the keyword is not a built-in var/function,
             // so this must be a user-defined name
             wrapper = (TokenDataWrapper) ssd.lookupSymbol(actual);
          }

          return wrapper;
       }

       /**
        * Searches a dictionaries containing {@link TokenDataWrapper} objects for a
        * symbol matching the passed key or matching a keyword's full text if the
        * symbol matches a language keyword.  This lookup starts at the top-most
        * scoping level and descends through each scope level on the stack until
        * a match is found. The wrapped object of the first match found is
        * returned.
        *
        * @param   actual
        *          Actual text string to match which may be a language keyword
        *          abbreviation.
        * @param   ssds
        *          An array of dictionaries to search.
        *
        * @return  The wrapped object that was found or <code>null</code> if no
        *          match was found.
        */
       public TokenDataWrapper lookupWrapped(String actual, ScopedSymbolDictionary<?> ... ssds)
       {
          for (ScopedSymbolDictionary<?> ssd : ssds)
          {
             TokenDataWrapper wrapped = lookupWrapped(ssd, actual);

             if (wrapped != null)
             {
                return wrapped;
             }
          }

          return null;
       }

       /**
        * Adds a user-defined function and its return type to the dictionary
        * of functions. This is a flat dictionary (there is no scoping).  The
        * function name is used as the key and a wrapped {@link Function} object
        * is the value.  Since the dictionary is flat, all function names must be
        * unique.
        * <p>
        * Due to the Progress forward definition capability, this function only
        * adds to the dictionary the first time this method is called for a
        * given name.
        *
        * @param    name
        *           Function name.
        * @param    tokenType
        *           The token type of the function's return value as defined by
        *           the Lexer and Parser.
        * @param    qname
        *           Fully qualified class name for functions that return an
        *           object instance, otherwise <code>null</code>.
        */
       public void addFunction(String name, int tokenType, String qname)
       {
          // a forward declaration may have already occurred for this function
          // try a lookup
          Function func = (Function) funcDict.lookupSymbol(name);

          // is this a new function def OR an override of a builtin function?
          if (func == null || func.isBuiltin())
          {
             func = new Function(name, tokenType, false, qname);

             addWrappedWorker(funcDict, false, name, func);
          }
       }

       /**
        * Adds a built-in Progress function and its return type to the dictionary
        * of functions. This is a flat dictionary (there is no scoping).  The
        * function name is used as the key and a wrapped {@link Function} object
        * is the value.  Since the dictionary is flat, all function names must be
        * unique.
        *
        * @param    name
        *           Function name.
        * @param    tokenType
        *           The token type of the function's return value as defined by
        *           the Lexer and Parser.
        * @param    returnsUnknown
        *           Specifies if this built-in function can possibly return
        *           the unknown value.
        * @param    qname
        *           Fully qualified class name for functions that return an
        *           object instance, otherwise <code>null</code>.
        */
       public void addBuiltinFunction(String  name,
                                      int     tokenType,
                                      boolean returnsUnknown,
                                      String  qname)
       {
          Function func = new Function(name, tokenType, true, qname);
          func.setReturnsUnknown(returnsUnknown ? Boolean.TRUE : Boolean.FALSE);

          addWrappedWorker(funcDict, false, name, func);
       }

       /**
        * Adds a built-in Progress function and its return type to the dictionary
        * of functions. This is a flat dictionary (there is no scoping).  The
        * function name is used as the key and a wrapped {@link Function} object
        * is the value.  Since the dictionary is flat, all function names must be
        * unique.
        *
        * @param   name
        *          Function name.
        * @param   tokenType
        *          The token type of the function's return value as defined by
        *          the Lexer and Parser.
        * @param   returnsUnknown
        *          Specifies if this built-in function can possibly return
        *          the unknown value.
        */
       public void addBuiltinFunction(String  name,
                                      int     tokenType,
                                      boolean returnsUnknown)
       {
          addBuiltinFunction(name, tokenType, returnsUnknown, null);
       }

       /**
        * Searches the function dictionary for a symbol matching the passed
        * key or matching a keyword's full text if the symbol matches a language
        * keyword.  This lookup starts at the top-most scoping level and descends
        * through each scope level on the stack until a match is found. The
        * token type of the first match found is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param    actual
        *           Actual text string to match which may be a language keyword
        *           abbreviation.
        *
        * @return   The token type that was found or -1 if no match was found.
        */
       public int lookupFunction(String actual)
       {
          return lookupWrappedType(lookupWrapped(funcDict, actual));
       }

       /**
        * Searches the function dictionary for a symbol matching the passed
        * key or matching a keyword's full text if the symbol matches a language
        * keyword.  This lookup starts at the top-most scoping level and descends
        * through each scope level on the stack until a match is found. The
        * definition of the first match found is returned.
        *
        * @param    actual
        *           Actual text string to match which may be a language keyword
        *           abbreviation.
        *
        * @return   The function definition or <code>null</code> if no match was
        *           found.
        */
       public Function lookupFunctionDef(String actual)
       {
          return (Function) lookupWrapped(funcDict, actual);
       }

       /**
        * Removes any function associated with this <code>Keyword</code> from the builtin
        * function dictionary.  This is only safe to call BEFORE real parsing begins.
        *
        * @param   word
        *          <code>Keyword</code> to remove (if it exists).
        */
       public void removeBuiltinFunction(Keyword word)
       {
          funcDict.deleteSymbol(word.getFullText());
       }

       /**
        * Stores all non-standard options as annotations to the AST passed as
        * a parameter if this is the original definition, otherwise it stores
        * a subset of values, most important of which is the cross-reference
        * index to allow the original definition to be easily identified and
        * accessed from a reference.  If an option has the default value, it is
        * not saved as an annotation.
        *
        * @param    name
        *           The function name which must have its options annotated.
        * @param    ast
        *           The AST to annotate.
        * @param    original
        *           <code>true</code> if this is to annotate the AST of the
        *           original definition, <code>false</code> if the AST is only
        *           a reference.
        */
       public void annotateFunction(String name, Aast ast, boolean original)
       {
          Function func = (Function) lookupWrapped(funcDict, name);

          // special case: preprocessor expression evaluation can resolve to
          // function token types without any function being registered since
          // the CallbackResolver registers the function token types inside
          // the KeywordDictionary; this means that a null pointer can occur
          // here, so we just return without doing any annotations in that
          // case
          if (func == null)
             return;

          func.annotateOptions(ast, original, this);
       }

       /**
        * Adds a procedure and a token type to the dictionary of procedures.
        * This is a flat dictionary (there is no scoping).  The procedure name
        * is used as the key and the token type is the value.  Since the
        * dictionary is flat, all procedure names must be unique.
        *
        * @param   name
        *          Procedure name.
        * @param   tokenType
        *          The procedure's token type as defined by the Lexer and Parser.
        */
       public void addProcedure(String name, int tokenType)
       {
          procDict.addSymbol(false, name, Integer.valueOf(tokenType));
       }

       /**
        * Searches the procedure dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The procedure's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupProcedure(String name)
       {
          return lookupWorkerExact(procDict, name);
       }

       /**
        * Adds a class name and a token type to the dictionary of classes.
        * This is a flat dictionary (there is no scoping).  The class name
        * is used as the key and the token type is the value.  Since the
        * dictionary is flat, all class names must be unique.
        *
        * @param    symbol
        *           Node defining the fully qualified (including package) class
        *           name.
        * @param    inherits
        *           Node defining the fully qualified name of the superclass or
        *           <code>null</code> for none.
        * @param    imple
        *           Node which has the list of interfaces that are implemented
        *           or <code>null</code> if none exist.
        * @param    file
        *           The filename of the .cls that defined this class.
        */
       public void addClass(Aast symbol, Aast inherits, Aast imple, String file)
       {
          String name = symbol.getText();

          WorkArea wa = locate();
          if (wa.preScanPass == 0)
          {
             loadClass(name, true);
          }

          SearchResult found = resolveClassName(name);
          ClassDefinition[] parents = calculateParents(name, inherits, found.dotnet);
          ClassDefinition[] ifaces  = null;

          if (imple != null)
          {
             ifaces = calculateParents(name, imple, found.dotnet);
          }

          addClass(name, parents, ifaces, found.builtin, found.dotnet, file);
       }

       /**
        * Adds a class name and a token type to the dictionary of classes.
        * This is a flat dictionary (there is no scoping).  The class name
        * is used as the key and the token type is the value.  Since the
        * dictionary is flat, all class names must be unique.
        *
        * @param    name
        *           The fully qualified (including package) class name.
        * @param    parents
        *           The superclass or <code>null</code> for none.
        * @param    ifaces
        *           The list of interfaces that are implemented or
        *           <code>null</code> if none exist.
        * @param    builtin
        *           <code>true</code> if this definition is built-in to Progress.
        * @param    dotnet
        *           <code>true</code> if this definition is a .NET class.
        * @param    file
        *           The filename of the .cls that defined this class.
        */
       public void addClass(String            name,
                            ClassDefinition[] parents,
                            ClassDefinition[] ifaces,
                            boolean           builtin,
                            boolean           dotnet,
                            String            file)
       {
          WorkArea wa = locate();

          ClassDefinition cls = lookupClass(name);
          boolean loaded = (cls != null);// && (!isPreScan() || !wa.preScanRefs.contains(name)));

          if (!loaded)
          {
             cls = new ClassDefinition(name, parents, ifaces, builtin, OOType.CLASS, dotnet, file);
          }

          while (parents != null)
          {
             SchemaWorker.loadFromPersistence(schemaDict, parents[0].getFilename());

             // classes can only inherit from 1 parent and it must also be a class
             // so we can hard code to the first array element
             parents = parents[0].getParents();
          }

          // remember this is the class definition being updated (with methods,
          // members and properties)
          currentCls.push(cls);

          // add the default constructor
          addObjectMethod(cls.getSimpleName(), OO_METH_VOID, KW_PUBLIC, false, null, null);

          if (loaded)
          {
             return;
          }

          classDict.addSymbol(false, name, cls);
          wa.preScanRefs.add(name);
       }

       /**
        * Adds an interface name and a token type to the dictionary of classes.
        * This is a flat dictionary (there is no scoping).  The interface name
        * is used as the key and the token type is the value.  Since the
        * dictionary is flat, all interface names must be unique.
        *
        * @param    symbol
        *           Node defining the fully qualified (including package) interface
        *           name.
        * @param    inherits
        *           Optional inherits clause.
        * @param    file
        *           The filename of the .cls that defined this interface.
        */
       public void addInterface(Aast symbol, Aast inherits, String file)
       {
          String          name   = symbol.getText();
          SearchResult    found  = resolveClassName(name);
          // all interfaces inherit methods from Progress.Lang.Object
          ClassDefinition[] parents = calculateParents(name, inherits, found.dotnet);

          addInterface(name, parents, found.builtin, found.dotnet, file);
       }

       /**
        * Adds an interface name and a token type to the dictionary of classes.
        * This is a flat dictionary (there is no scoping).  The interface name
        * is used as the key and the token type is the value.  Since the
        * dictionary is flat, all interface names must be unique.
        *
        * @param    name
        *           The fully qualified (including package) interface name.
        * @param    parents
        *           Optional parent class definition(s) for an inherits clause.
        * @param    builtin
        *           <code>true</code> if this definition is built-in to Progress.
        * @param    dotnet
        *           <code>true</code> if this definition is a .NET class.
        * @param    file
        *           The filename of the .cls that defined this interface.
        */
       public void addInterface(String            name,
                                ClassDefinition[] parents,
                                boolean           builtin,
                                boolean           dotnet,
                                String            file)
       {
          WorkArea wa = locate();
          if (wa.preScanPass == 0)
          {
             loadClass(name, true);
          }

          ClassDefinition iface = lookupClass(name);
          boolean loaded = (iface != null);

          if (!loaded)
          {
             iface = new ClassDefinition(name,
                                         parents,
                                         null,
                                         builtin,
                                         OOType.INTERFACE,
                                         dotnet,
                                         file);
          }

          // remember this is the interface definition being updated (with
          // methods, members and properties)
          currentCls.push(iface);

          if (loaded)
          {
             return;
          }

          classDict.addSymbol(false, name, iface);

          wa.preScanRefs.add(name);
       }

       /**
        * Adds an enum definition to the dictionary of classes.
        *
        * @param    symbol
        *           Node defining the fully qualified (including package) enum
        *           name.
        * @param    flags
        *           {@code true} if this is a flags enum.
        * @param    file
        *           The filename of the .cls that defined this interface.
        */
       public void addEnum(Aast symbol, boolean flags, String file)
       {
          String          name   = symbol.getText();
          SearchResult    found  = resolveClassName(name);

          WorkArea wa = locate();
          if (wa.preScanPass == 0)
          {
             loadClass(name, true);
          }

          ClassDefinition edef = lookupClass(name);
          boolean loaded = (edef != null);

          if (!loaded)
          {
             // ensure progress.lang.object is loaded before loading the enum class (as in FWD, this inherits
             // from PLO, and will trigger a circular dependency
             loadClass(ROOT_OBJ_NAME);

             String pname = found.dotnet
                               ? "System.Enum"
                               : flags ? "Progress.Lang.FlagsEnum" : "Progress.Lang.Enum";

             // regular enums use parent Progress.Lang.Enum, flag enums use Progress.Lang.FlagsEnum
             loadClass(pname);
             ClassDefinition parent = lookupClass(pname);

             if (parent == null)
             {
                if (wa.preScanRefs.contains(pname))
                {
                   String err = String.format("Parent class already in pre-scan - postponed: %s!", pname);
                   throw new ParentUnavailableException(err);
                }

                String err = String.format("Missing enum parent class definition for %s!", pname);
                throw new RuntimeException(err);
             }

             ClassDefinition[] pars = new ClassDefinition[] { parent };
             edef = new ClassDefinition(name,
                                        pars,
                                        null,
                                        found.builtin,
                                        OOType.ENUM,
                                        found.dotnet,
                                        file);

             // the 4GL has an undocumented member of all .NET enum instances that returns the integer value of
             // that instance; this implicit member does NOT exist for 4GL enums; references will appear in the
             // source code in this syntax: System.Windows.Forms.TabAlignment:TOP:value__
             if (edef.isDotNet())
             {
                Variable value__ = new Variable("value__", VAR_INT, null);
                addVariable(true, value__);
                edef.addVariable(DEFINE_ENUM, "value__", value__, null, KW_PUBLIC, false, null, false);
                value__.setAnnotationHelper((Variable var, Aast ast, boolean ori, boolean meth, SymbolResolver sym) ->
                {
                   ast.putAnnotation("dotnet-enum-implicit-value", true);
                });
             }
          }

          // remember this is the enum definition being updated (with members)
          currentCls.push(edef);

          if (loaded)
          {
             return;
          }

          classDict.addSymbol(false, name, edef);

          wa.preScanRefs.add(name);
       }

       /**
        * Adds a member to the current enum definition.
        *
        * @param    symbol
        *           Node defining the new enum member.
        */
       public void addEnumMember(Aast symbol)
       {
          ClassDefinition cls = getCurrentClassDef();

          if (cls != null)
          {
             String name  = symbol.getText();
             String qname = cls.getName();

             Variable var = addLocalVariable(name, VAR_CLASS, qname);

             // we mark this as a non-static var so that annotations will not mark it as static
             var.setProperty(false);
             var.setStatic(false);
             var.annotateOptions(symbol, true, false, this);

             // we register it as static because it is implicitly static in its lookup
             cls.addVariable(DEFINE_ENUM, name, var, symbol, KW_PUBLIC, true, qname, false);
          }
       }

       /**
        * Obtain the currently active class or interface definition.
        *
        * @return   The active class or interface definition or <code>null</code>
        *           if there is no active definition.
        */
       public ClassDefinition getCurrentClassDef()
       {
          return currentCls == null || currentCls.empty() ? null : currentCls.peek();
       }

       /**
        * Obtain the name of the currently active class definition.
        *
        * @return   The class name or <code>null</code> if there is no active
        *           class definition.
        */
       public String getCurrentClassName()
       {
          ClassDefinition cls = getCurrentClassDef();
          return (cls == null) ? null : cls.getName();
       }

       /**
        * Obtain the parent of the currently active class definition.
        *
        * @return   The parent class definition or <code>null</code> if there is
        *           no explicit parent.
        */
       public ClassDefinition[] getCurrentParentsDef()
       {
          ClassDefinition cls = getCurrentClassDef();
          return (cls == null) ? null : cls.getParents();
       }

       /**
        * Obtain the name of the parent of the currently active class definition. This can only
        * return a single name and MUST NEVER be called for an interface since interfaces can
        * have multiple inheritance.
        *
        * @return   The parent class name.
        */
       public String getCurrentParentName()
       {
          ClassDefinition[] cls = getCurrentParentsDef();
          return (cls == null) ? ROOT_OBJ_NAME : cls[0].getName();
       }

       /**
        * Notify the symbol resolver that the current class or interface
        * definition is complete.
        */
       public void classDefinitionComplete()
       {
          currentCls.pop();
       }

       /**
        * Attempt to load the class or interface name given. This "eats" any
        * failures since it is not guaranteed that the input does represent a
        * valid class name.
        *
        * @param    name
        *           A potential class or interface name.
        *
        * @return   The fully qualified class name if it can be loaded or <code>null</code> if the
        *           class or interface cannot be loaded.
        */
       public String tryLoadClass(String name)
       {
          String full = null;

          WorkArea wa = locate();

          // do not interfere with the currently processed classes - the hierarchy will be processed when the
          // 'caller' finishes.
          List<String> save = new ArrayList<>();
          save.addAll(wa.processed);
          try
          {
             wa.processed.clear();

             // if the class can't be loaded, this will force throwing an exception
             wa.forceLoadFailure = true;
             full = loadClass(name);
          }

          catch (Throwable exp)
          {
             // TODO: uncomment after TransformDriver supports setting up of cmd line logging level
             //LOG.log(Level.FINE, "FAILURE in tryLoadClass()", exp);
          }
          finally
          {
             wa.forceLoadFailure = false;
             wa.processed.clear();
             wa.processed.addAll(save);
          }

          return full;
       }

       /**
        * Attempt to load the class or interface name given. This "eats" any
        * failures since it is not guaranteed that the input does represent a
        * valid class name.
        *
        * @param    name
        *           A potential class or interface name.
        *
        * @return   <code>true</code> if the class or interface can be loaded.
        */
       public boolean canLoadClass(String name)
       {
          return (tryLoadClass(name) != null);
       }

       /**
        * Load (using recursion if needed) the hierarchy of class definitions (or
        * a single interface definition) referenced by the given name. This should
        * be called when a reference to a class or interface name occurs (except
        * in the case of the <code>CLASS</code> or <code>INTERFACE</code> language
        * statements). The name can be unqualified (requiring a call to
        * {@link #resolveClassName} to determine the fully qualified name) or it
        * can be either partially or fully qualified (partially qualified names were
        * added in v11). Once resolved, if the class or interface definition
        * is already loaded, then this will return immediately.  Otherwise the
        * class or interface file will be found via the <code>PROPATH</code> and
        * that file's AST will be loaded.  If the AST does not exist, then the
        * file will first be parsed. During parsing, the <code>CLASS</code> or
        * <code>INTERFACE</code> language statements will call {@link #addClass}
        * or {@link #addInterface} respectively. This will instantiate the proper
        * {@link ClassDefinition} objects and the contained definitions for
        * methods, member variables and properties will be added by the rules
        * that match those language features. Once parsing is complete, the
        * definition will be fully formed and loaded. Any references to other
        * classes or interfaces in that file will cause recursive parsing/loading
        * using this method, until the entire object hierarchy is loaded. At that
        * point, this chain of methods will return.
        *
        * @param    name
        *           Unqualified or partially/fully qualified class or interface name to
        *           load.
        *
        * @return   The fully qualified class or interface name that was loaded.
        */
       public String loadClass(String name)
       {
          return loadClass(name, false);
       }

       /**
        * Load (using recursion if needed) the hierarchy of class definitions (or
        * a single interface definition) referenced by the given name. This should
        * be called when a reference to a class or interface name occurs (except
        * in the case of the <code>CLASS</code> or <code>INTERFACE</code> language
        * statements). The name can be unqualified (requiring a call to
        * {@link #resolveClassName} to determine the fully qualified name) or it
        * can be either partially or fully qualified (partially qualified names were
        * added in v11). Once resolved, if the class or interface definition
        * is already loaded, then this will return immediately.  Otherwise the
        * class or interface file will be found via the <code>PROPATH</code> and
        * that file's AST will be loaded.  If the AST does not exist, then the
        * file will first be parsed. During parsing, the <code>CLASS</code> or
        * <code>INTERFACE</code> language statements will call {@link #addClass}
        * or {@link #addInterface} respectively. This will instantiate the proper
        * {@link ClassDefinition} objects and the contained definitions for
        * methods, member variables and properties will be added by the rules
        * that match those language features. Once parsing is complete, the
        * definition will be fully formed and loaded. Any references to other
        * classes or interfaces in that file will cause recursive parsing/loading
        * using this method, until the entire object hierarchy is loaded. At that
        * point, this chain of methods will return.
        *
        * @param    name
        *           Unqualified or partially/fully qualified class or interface name to
        *           load.
        * @param    topLevel
        *           {@code true} if the named class is already being parsed in the caller.
        *
        * @return   The fully qualified class or interface name that was loaded.
        */
       public String loadClass(String name, boolean topLevel)
       {
          SearchResult found = resolveClassName(name);

          ClassDefinition cls = lookupClass(found.clsname);
          WorkArea wa = locate();

          if (cls == null)
          {
             cls = loadClassDefinition(this, found.clsname);
             if (cls != null)
             {
                return found.clsname;
             }

             if (found.java)
             {
                if (loadJavaClass(found.clsname) == null)
                {
                   return null;
                }

                return found.clsname;
             }

             if (wa.preScanRefs.contains(found.clsname))
             {
                // in process of being loaded
                return found.clsname;
             }

             // not loaded yet

             // do we have a cached file name?
             if (found.filename == null)
             {
                // find the class file name in the propath
                if (!srchPropathFindFile(found.clsname, found.type, found))
                {
                   if (wa.preScanPass >= 1 && !wa.forceLoadFailure)
                   {
                      classDict.addSymbol(false, found.clsname, MOCK_CLASS_DEF);
                      wa.preScanRefs.add(name);

                      // this class is not found, but it must not break parsing - show a warning and
                      // 'sink' all references to it
                      String err = reportClassNotFound(found.clsname, false);
                      System.out.printf("WARNING: Lvl%02d parse: %s\n", wa.preScanPass, err);

                      return found.clsname;
                   }
                   else
                   {
                      // we fail only if we are referencing this class from the currently parsed
                      // file (we are not in pre-scan-mode)
                      reportClassNotFound(found.clsname, true);
                   }
                }

                // at this point found.filename must be valid
             }

             if (generator == null)
             {
                generator = new AstGenerator();
                generator.setSilent(silent);
                generator.setPreprocess(true);
                generator.setCache(true);
                generator.setDumpLexer(true);
                generator.setDumpParser(true);
                generator.setAstPersist(true);
             }

             String relative = Configuration.normalizeFilename(found.filename);

             if (wa.preScanPass == 0)
             {
                wa.preScanRefs.clear();
             }

             wa.preScanRefs.add(found.clsname);

             wa.preScanPass++;

             boolean inPreScanParent = false;

             try
             {
                if (!silent)
                {
                   String str = String.join("|", Collections.nCopies(wa.preScanPass, " "));
                   System.out.printf("   Lvl%02d parse:%s%s\n",
                                     wa.preScanPass,
                                     str,
                                     relative);
                }

                // the PROPATH is inherited from the caller
                generator.preScanClass(relative, found.builtin, found.dotnet, this.propath);

                // save all referenced classes/interfaces except for the genesis object
                if (wa.preScanPass > 1)
                {
                   wa.processed.add(found.clsname);
                }
             }

             catch (ParentUnavailableException exc)
             {
                System.err.println("Postpone loading class from " + name + ": " + exc.getMessage());
                inPreScanParent = true;
             }

             catch (AstException   |
                    ANTLRException |
                    IOException e)
             {
                // if we reach here, do not abort.  4GL is lenient if there are parse errors in
                // classes referenced via i.e. var types; what matters is that the file can be found,
                // but it doesn't matter if it can compile or not.  Only explicit compile of that
                // class will report it.

                // although FWD attempts to fully compile the class, this is just a warning that there
                // might be problems in the application's legacy 4GL code, which the user needs to
                // fix/address.  In any case, if we reach this step, explicit compile of this class
                // will always fail in both 4GL and FWD, and we are fine with it.
                String msg = String.format("Failed Lvl%02d parse of %s: %s",
                                           wa.preScanPass,
                                           found.clsname,
                                           e.getMessage());
                LOG.log(Level.WARNING, msg);
                LOG.log(Level.WARNING, "", e);
             }
             finally
             {
                wa.preScanPass--;

                if (wa.preScanPass == 0)
                {
                   wa.preScanRefs.clear();
                   System.out.println("   Lvl01 DONE:  " + relative);
                }
             }

             if (!inPreScanParent)
             {
                cls = lookupClass(found.clsname);

                if (cls == null)
                {
                   String spec = "Cannot load class/interface %s from file %s!";
                   String err  = String.format(spec, found.clsname, relative);
                   throw new RuntimeException(err);
                }

                if (!isPreScan())
                {
                   // we cannot just process the genesis object that started the graph; we must process all
                   // referenced classes/interfaces including those not in our parent hierarchy AND we need
                   // to process them in the same order they were processed previously
                   while (!wa.processed.isEmpty())
                   {
                      String fname = wa.processed.remove();

                      // ensure all super-classes are loaded for this object
                      parseHierarchy(fname, wa);
                   }

                   // do NOT handle the 2nd level parsing of the genesis object here if that parsing is already
                   // happening in the caller
                   if (!topLevel)
                   {
                      parseHierarchy(cls.getName(), wa);
                   }
                }
             }
          }

          return found.clsname;
       }

       /**
        * Searches the class dictionary for a definition matching the passed key.
        *
        * @param   name
        *          Fully qualified class/interface name to match.
        *
        * @return  The class that was found or <code>null</code> if no match was found.
        */
       public ClassDefinition lookupClass(String name)
       {
          if (classDict == null)
          {
             return null;
          }

          ClassDefinition cls = null;

          cls = (ClassDefinition) classDict.lookupSymbol(name);

          if (cls == null)
          {
             cls = locate().classCache.get(name.toLowerCase());
          }

          if (cls != null && cls.isMock() && locate().preScanPass == 0)
          {
             // in parsing (non-prescan) mode, if we are resolving the MOCK_CLASS_DEF, then do not
             // allow it as the name is really missing.
             cls = null;
          }

          return cls;
       }

       /**
        * Annotate a published class event, with the details of the parameter signature.
        *
        * @param    evt
        *           The event node, annotated with its class definition.
        * @param    call
        *           The event publish node.
        * @param    isStatic
        *           <code>true</code> if the call is from a static context.
        */
       public void annotateObjectEvent(Aast evt, Aast call, boolean isStatic)
       {
          String cname = (String) evt.getAnnotation("found-in-cls");
          ClassDefinition cls = lookupClass(cname);
          cls.annotateObjectEvent(evt, call, isStatic);
       }

       /**
        * Annotate the given method invocation reference with details for the specified method
        * in this class.  This is a no-operation if the method doesn't exist or if the node is
        * <code>null</code>.
        *
        * @param    cname
        *           Class name, may be <code>null</code>.
        * @param    mname
        *           Method name, <code>null</code> is a c'tor invocation.
        * @param    isStatic
        *           <code>true</code> if the method is static.
        * @param    node
        *           The AST to be annotated.
        */
       public void annotateObjectMethod(String cname, String mname, boolean isStatic, Aast node)
       {
          annotateObjectMethod(cname, mname, isStatic, false, node);
       }

       /**
        * Annotate the given method invocation reference with details for the specified method
        * in this class.  This is a no-operation if the method doesn't exist or if the node is
        * <code>null</code>.
        *
        * @param    cname
        *           Class name, may be <code>null</code>.
        * @param    mname
        *           Method name, <code>null</code> is a c'tor invocation.
        * @param    isStatic
        *           <code>true</code> if the method is static.
        * @param    isSuper
        *           <code>true</code> if the rvalue refnode is the SUPER keyword.
        * @param    node
        *           The AST to be annotated.
        */
       public void annotateObjectMethod(String cname, String mname, boolean isStatic, boolean isSuper, Aast node)
       {
          ClassDefinition current  = getCurrentClassDef();
          ClassDefinition cls      = (cname == null) ? current : lookupClass(cname);
          boolean         internal = (cls == current);
          int             access   = calcAccessMode(cname);

          if (cls != null)
          {
             ParameterKey[] sig = calculateCallSignature(node);

             try
             {
                cls.annotateMethodCall(mname, sig, access, isStatic, isSuper, internal, node);
             }
             catch (Throwable t)
             {
                throw new RuntimeException("Could not annotate method call at\n" + node.dumpTree(true), t);
             }

             if (node.getType() == OO_METH_CLASS)
             {
                String rcls = (String) node.getAnnotation("qualified");

                // this leaves different annotations than above; the annotations are about
                // the returned reference NOT about the called method
                if (rcls == null)
                {
                   System.out.println("WARNING: No annotateClassRef due to missing 'qualified' annotation\n" + node.dumpTree(true));
                }
                else
                {
                   annotateClassRef(rcls, node);
                }
             }
          }
       }

       /**
        * Annotate the given node which is a reference to the named class.
        *
        * @param   qname
        *          Fully qualified class name being referenced.
        * @param   node
        *          The node to annotate.
        */
       public void annotateClassRef(String qname, Aast node)
       {
          // Note that this should no longer happen, since we have added checks prior to invoking
          // annotateClassRef, but left here in case there are other situations added.
          if (qname == null)
          {
             System.out.println("FAILURE: Returning from annotateClassRef because of null qname " +
                                node.dumpTree(true));
             return;
          }
          ClassDefinition cls = lookupClass(qname);

          if (cls == null)
          {
             // try to load the class
             qname = loadClass(qname);
             cls = lookupClass(qname);

             if (cls == null)
             {
                WorkArea wa = locate();
                if (wa.preScanRefs.contains(qname) && wa.preScanPass > 0)
                {
                   // the pre-scan is still in progress, do not annotate
                   // except for one important annotation that is utilized
                   // downstream during pre-scan itself for class name
                   // "lookahead", for lack of a better term.
                   // WARNING: since we don't have a class definition, we are
                   // making the assumption that this is not a Java class and
                   // converting to lowercase
                   node.putAnnotation("qualified", qname.toLowerCase());
                   return;
                }
                else
                {
                   reportClassNotFound(qname, true);
                }
             }
          }

          node.putAnnotation("qualified", cls.isJava() ? qname : qname.toLowerCase());
          node.putAnnotation("source-file", cls.getFilename());
          node.putAnnotation("builtin-cls", cls.isBuiltIn());
          node.putAnnotation("dotnet-cls", cls.isDotNet());
          node.putAnnotation("indirect-dotnet", cls.isIndirectDotNetReference());
          node.putAnnotation("is-class", cls.isClass());
          node.putAnnotation("is-interface", cls.isInterface());
          node.putAnnotation("is-enum", cls.isEnum());
          node.putAnnotation("is-java", cls.isJava());

          if (cls.isDotNet() && node.getText().endsWith("]"))
          {
             node.putAnnotation("dotnet-array", true);
          }

          ClassDefinition current = getCurrentClassDef();

          if (current != null && current != cls)
          {
             current.markDotNetUsage(cls);
          }
       }

       /**
        * Converts any unqualified or partially qualified class or interface names into the fully
        * qualified name (which has a package included). Before v11 there was no such thing as a
        * partially qualified class or interface name in Progress, but now they exists and are
        * supported here.  When any "." character is found in the source name, that name is assumed
        * be qualified in some manner (fully or partially).
        * <p>
        * The following name resolution actions are attempted:
        * <p>
        * <ol>
        *    <li> If the name matches the unqualified name of any of the
        *         explicitly imported classes (via the USING statement), that
        *         fully qualified class name is returned.
        *    <li> The "*" in each of the imported package specifications is
        *         replaced with the unqualified name (in order) and:
        *    <ol>
        *       <li> If the resulting name matches a class that is already loaded,
        *            that class name is set into the search result, but the other
        *            search result members will NOT be set.
        *       <li> The following paths are searched depending on the USING type:
        *          <ul>
        *             <li> <code>PROPATH</code> for the project's 4GL cls files
        *             <li> <code>OO4GL_PATH</code> for built-in 4GL cls files
        *             <li> <code>ASSEMBLY_PATH</code> for the project's .NET cls
        *                  files
        *             <li> <code>DOTNET_PATH</code> for built-in .NET cls files
        *          </ul>
        *       <li> If a match is found via the search, the members of the search
        *            result will be set as noted below.
        *    </ol>
        * </ol>
        * <p>
        * If an unqualified name is not matched to the above criteria, the name
        * is assumed to be the complete type name and is returned unchanged. This
        * means that no search has been successful.
        * <p>
        * If the filesystem search has been successful, the returned search
        * result will have a non-null <code>filename</code> member.  In addition,
        * the <code>builtin</code> and <code>dotnet</code> members will have been
        * set and can be relied upon.
        *
        * @param    uname
        *           Unqualified or fully qualified class or interface name.
        *
        * @return   An object containing data about the search. If the
        *           <code>filename</code> member is <code>null</code> then only
        *           the <code>name</code> member has been set, though it may not
        *           be valid. If the <code>filename</code> member is NOT
        *           <code>null</code>, then all members of the object are already
        *           set and can be relied upon.
        */
       public SearchResult resolveClassName(String uname)
       {
          return resolveClassName(uname, UsingType.UNSPECIFIED);
       }

       /**
        * Converts any unqualified or partially qualified class or interface names into the fully
        * qualified name (which has a package included). Before v11 there was no such thing as a
        * partially qualified class or interface name in Progress, but now they exists and are
        * supported here.  When any "." character is found in the source name, that name is assumed
        * be qualified in some manner (fully or partially).
        * <p>
        * The following name resolution actions are attempted:
        * <p>
        * <ol>
        *    <li> If the name matches the unqualified name of any of the
        *         explicitly imported classes (via the USING statement), that
        *         fully qualified class name is returned.
        *    <li> The "*" in each of the imported package specifications is
        *         replaced with the unqualified name (in order) and:
        *    <ol>
        *       <li> If the resulting name matches a class that is already loaded,
        *            that class name is set into the search result, but the other
        *            search result members will NOT be set.
        *       <li> The following paths are searched depending on the USING type:
        *          <ul>
        *             <li> <code>PROPATH</code> for the project's 4GL cls files
        *             <li> <code>OO4GL_PATH</code> for built-in 4GL cls files
        *             <li> <code>ASSEMBLY_PATH</code> for the project's .NET cls
        *                  files
        *             <li> <code>DOTNET_PATH</code> for built-in .NET cls files
        *          </ul>
        *       <li> If a match is found via the search, the members of the search
        *            result will be set as noted below.
        *    </ol>
        * </ol>
        * <p>
        * If an unqualified name is not matched to the above criteria, the name
        * is assumed to be the complete type name and is returned unchanged. This
        * means that no search has been successful.
        * <p>
        * If the filesystem search has been successful, the returned search
        * result will have a non-null <code>filename</code> member.  In addition,
        * the <code>builtin</code> and <code>dotnet</code> members will have been
        * set and can be relied upon.
        *
        * @param    uname
        *           Unqualified or fully qualified class or interface name.
        * @param    utype
        *           The type of tje class to search.
        *
        * @return   An object containing data about the search. If the
        *           <code>filename</code> member is <code>null</code> then only
        *           the <code>name</code> member has been set, though it may not
        *           be valid. If the <code>filename</code> member is NOT
        *           <code>null</code>, then all members of the object are already
        *           set and can be relied upon.
        */
       public SearchResult resolveClassName(String uname, UsingType utype)
       {
          String[]        qualified = new String[1];
          UsingSpec[]     uspec     = new UsingSpec[1];
          SearchResult    result    = new SearchResult();

          // remove array brackets if they exist (this assumes they are always
          // at the end AND that there is no other valid content after the
          // opening square bracket); note that brackets can define multi-
          // dimensional arrays like MyClass[,,] for a 3-dimensional array
          int sqBrackets = uname.indexOf("[");
          String name = sqBrackets >= 0 ? uname.substring(0, sqBrackets) : uname;

          // lambda for searching the imported packages (USING blah.blah.*)
          Runnable pkgLookup = () ->
          {
             Iterator<UsingSpec> iter = searchPkgDict.iterator();

             // try each of the search specifications in order
             while (iter.hasNext() && qualified[0] == null)
             {
                uspec[0] = iter.next();

                // build the next possible qualified class name
                String next = String.format("%s%s", uspec[0].spec, name);

                // short circuit the search logic in the case that we already
                // have loaded this class definition
                ClassDefinition cls = lookupClass(next);

                if (cls != null)
                {
                   qualified[0] = next;
                   result.builtin = cls.isBuiltIn();
                   result.dotnet  = cls.isDotNet();
                   result.java    = cls.isJava();
                }
                else
                {
                   // search propath for the specific class file name
                   if (findFile(next, uspec[0].type, result))
                   {
                      qualified[0] = next;
                   }
                }
             }
          };

          if (name.indexOf('.') == -1)
          {
             // check the explicit list of class names
             uspec[0]     = explPkgDict.get(name.toLowerCase());
             qualified[0] = (uspec[0] != null) ? uspec[0].spec : null;

             if (qualified[0] == null)
             {
                // fallback to search imported packages
                pkgLookup.run();
             }

             if (qualified[0] == null)
             {
                // the unqualified name must be the full name
                qualified[0] = name;
             }
          }
          else
          {
             // given the assumption that a basepath.something.qualifier.Gadget.cls class exists,
             // the following is allowed:
             //
             //    USING basepath.something.*.
             //    DEFINE VARIABLE ref AS qualifier.Gadget.
             //
             // This partially qualified case is resolved by:
             //
             // 1. check if the name exists as-is
             // 2. if not, do a search as above using searchPkgDict
             if (findFile(name, utype, result))
             {
                // fully qualified, found
                qualified[0] = name;
                if (utype != UsingType.UNSPECIFIED)
                {
                   result.type   = utype;
                   result.java   = (utype == UsingType.JAVA);
                   result.dotnet = (utype == UsingType.DOTNET);
                }
             }
             else
             {
                // fallback to search imported packages
                pkgLookup.run();

                if (qualified[0] == null)
                {
                   // last chance, this may be a fully qualified Java class reference that has no
                   // associated USING statement so there was no previous way to look it up
                   if (isJavaClass(name, result))
                   {
                      // the edits have already been made to the result structure
                      return result;
                   }

                   // not found, fallback to the name
                   qualified[0] = name;
                }
             }
          }

          if (uspec[0] != null)
          {
             utype = uspec[0].type;
             result.type     = utype;
             result.java     = (utype == UsingType.JAVA);
             result.dotnet   = (utype == UsingType.DOTNET);
          }

          result.clsname  = qualified[0];

          return result;
       }

       /**
        * Adds a package search specification ("package.*") or a fully qualified
        * class name ("package.class") to the list of known packages. These are
        * used to match unqualified class names with their fully qualified
        * equivalents.
        *
        * @param   filename
        *          The file where this USING statement appears.
        * @param   using
        *          The USING node.
        * @param   name
        *          Package search specification or explicit fully qualified
        *          class name.
        * @param   fromClause
        *          The nodes parsed as the <code>FROM (ASSEMBLY | PROPATH | JAVA)</code>
        *          clause or <code>null</code> if it wasn't present.  A Java package will
        *          only be processed if explicitly specified (non-null FROM JAVA clause).
        */
       public void addPackage(String filename, Aast using, String name, Aast fromClause)
       {
          UsingType type = UsingType.UNSPECIFIED;

          if (fromClause != null)
          {
             Aast child = fromClause.getChildAt(0);

             if (child != null)
             {
                switch (child.getType())
                {
                   case KW_ASSEMBLY:
                      type = UsingType.DOTNET;
                      break;
                   case KW_PROPATH:
                      type = UsingType.PROPATH;
                      break;
                   case KW_JAVA:
                      type = UsingType.JAVA;
                      break;
                   default:
                      LOG.log(Level.SEVERE, String.format("Unrecognized FROM %s!", child.getText()));
                }
             }
          }

          if (name.endsWith(".*"))
          {
             // remove the asterisk since we will no longer need it (it only
             // serves to denote that this is a search spec versus an explicit
             // fully qualified class name
             name = name.substring(0, name.length() - 1);

             // one cannot easily check for Java package existence; using
             // Package.getPackage(name) or Package.getPackages() only return data for
             // packages in which a class was already loaded; we can't assume that is the
             // case here; using ClassLoader.getResource() or ClassLoader.getResources()
             // need specific resource names (instead of a package name); instead as long as
             // the programmer has specfied Java we must assume it is

             // if not explicitly Java, then assume it is 4GL or .NET
             if (type != UsingType.JAVA)
             {
                // error out if the directory can't be found in the PROPATH, this does not check Java
                String path = pkgExists(name);

                if (path == null)
                {
                   // 4GL doesn't abend if a package (with asterisk) doesn't exist.
                   LOG.log(Level.SEVERE, String.format("Non-existent 4GL or .NET package %s!", name));
                   using.setHidden(true);
                   return;
                }
                else
                {
                   if (path.startsWith("." + File.separator))
                   {
                      path = path.substring(2);
                   }
                   path = path.replace(File.separatorChar, '.');
                   if ((caseSens && !path.equals(name)) || (!caseSens && !path.equalsIgnoreCase(name)))
                   {
                      using.putAnnotation("normalized-path", path);
                   }
                }
             }

             searchPkgDict.add(new UsingSpec(name, type));
          }
          else
          {
             // find the base name
             int idx = name.lastIndexOf(".");

             if (idx < 0)
             {
                throw new RuntimeException("Invalid fully qualified class name!");
             }

             String base = name.substring(idx + 1).toLowerCase();

             // if more than one USING clause has the same base name, the 4GL only honors the first
             if (!explPkgDict.containsKey(base))
             {
                // map the lowercased base name to the fully qualified name (allows
                // case-insensitive lookup)
                explPkgDict.put(base, new UsingSpec(name, type));
             }

             // prescan this class (if needed) and save its source file; this needs to be done only
             // if we are not 'using' the same class as the current class being defined
             SearchResult found = resolveClassName(name, type);

             String relative = (found == null || found.filename == null || found.java)
                                  ? null
                                  : Configuration.normalizeFilename(found.filename);

             WorkArea wa = locate();

             // take a peek at the preScanRefs searching a class we are already parsing
             if ((relative == null || !filename.equalsIgnoreCase(relative)) &&
                 !(wa.preScanRefs.contains(found.clsname)))
             {
                if (name == null)
                {
                   System.out.println("\nWARNING: unable to annotate Class Ref (classname==null)");
                }
                else
                {
                   annotateClassRef(name, using);
                }
             }
             else
             {
                using.putAnnotation("qualified", found.java ? name : name.toLowerCase());
                using.putAnnotation("source-file", filename);
                using.putAnnotation("builtin-cls", found.builtin);
                using.putAnnotation("dotnet-cls", found.dotnet);
                using.putAnnotation("is-java", found.java);
             }
          }
       }

       /**
        * Add the named method to the currently active class definition.
        *
        * @param    name
        *           Method name.
        * @param    mtype
        *           Return type for the method.
        * @param    access
        *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
        *           or <code>KW_PRIVATE</code>).
        * @param    isStatic
        *           <code>true</code> if the variable/property is static.
        * @param    node
        *           The AST to be annotated.
        * @param    ret
        *           The AST for the method's return type.  This is used to copy OO annotations back to
        *           the method node.
        */
       public void addObjectMethod(String  name,
                                   int     mtype,
                                   int     access,
                                   boolean isStatic,
                                   Aast    node,
                                   Aast    ret)
       {
          // must be non-null since methods can't be defined outside of classes
          getCurrentClassDef().addMethod(name, mtype, access, isStatic, node, ret);
       }

       /**
        * Add the named variable, property or class event as a data member of the currently active
        * class definition. If no class definition is active, this method does nothing.
        *
        * @param    stype
        *           Defining language statement type.
        * @param    name
        *           Member name.
        * @param    node
        *           The definition node for this member.
        * @param    access
        *           Node of type <code>KW_PUBLIC</code>, <code>KW_PROTECTD</code>
        *           or <code>KW_PRIVATE</code>.
        * @param    stic
        *           Node of type <code>KW_STATIC</code> if the variable/property is
        *           static.
        * @param    ov
        *           Override node if specified.
        * @param    ab
        *           Abstract node if specified.
        * @param    qname
        *           Fully qualified class name for members that are references to a given class
        *           or <code>null</code> if this member is not an object instance.
        * @param    extent
        *           The extent value or 0 if there is no extent.  The AST cannot be inspected at this point
        *           because all the child notes are not linked into the root node yet.  For this reason, the
        *           explicit value must be passed.
        */
       public void addDataMember(int    stype,
                                 String name,
                                 Aast   node,
                                 Aast   access,
                                 Aast   stic,
                                 Aast   ov,
                                 Aast   ab,
                                 String qname,
                                 int    extent)
       {
          // this is called both in pre-scan and normal parse phases:
          // 1. in pre-scan, it will pickup and collect the DEF VAR scoped with methods/c'tor/etc
          // 2. in normal parse, it will not pickup these (as the body will be ignored)

          ClassDefinition cls = getCurrentClassDef();

          if (cls != null)
          {
             // we are deliberately bypassing keyword lookup that happens in lookupWrapped(); otherwise it
             // conflicts with using reserved keywords as class data member names
             Variable var = (Variable) varDict.lookupSymbol(name);

             // properties and class events default to public, def var defaults to private
             int dmode = (stype == DEFINE_VARIABLE) ? KW_PRIVATE : KW_PUBLIC;
             int amode = (access == null) ? dmode : access.getType();

             boolean isStatic = (stic != null);
             boolean isProp = (stype == DEFINE_PROPERTY);

             // if the previous definition is a property, and we are in pre-scan mode, then ignore it; this is
             // just a dirty fix for pre-scan mode, it's not complete
             if (var.isProp() && !isProp && isPreScan())
             {
                return;
             }

             var.setProperty(isProp);
             var.setStatic(isStatic);
             var.setExtent(extent);
             var.setOverride(ov != null);
             var.setAbstract(ab != null);

             // check if any interface parents has this property defined, as re-defining it as abstract or
             // implementing it in a sub-class doesn't require the OVERRIDE option
             if (var.isProp() && !var.isOverride())
             {
                Variable found = cls.lookupVariableWrapper(var.getName(), KW_PROTECTD, var.isStatic());
                if (found != null)
                {
                   var.setOverride(found.isOverride() || found.getClassDefinition() != var.getClassDefinition());
                }
             }

             trackPropertyOrEventMethodSignatures(stype, cls, node, var, name);

             // a variable is provisional during prescan, if and only if we have no explicit access modifier set
             boolean provisional = isPreScan() && stype == DEFINE_VARIABLE && access == null;

             cls.addVariable(stype, name, var, node, amode, isStatic, qname, provisional);
          }
       }

       /**
        * Set the proper flags at the property's {@link Variable} instance, specifying if the getter or setter
        * are defined at this property definition.
        *
        * @param    name
        *           The property name.
        * @param    getter
        *           <code>true</code> if a getter is defined.
        * @param    setter
        *           <code>true</code> if a setter is defined.
        */
       public void postProcessProperty(String name, boolean getter, boolean setter)
       {
          ClassDefinition cls = getCurrentClassDef();
          if (cls != null)
          {
             Variable var = (Variable) lookupWrapped(varDict, name);

             var.setWithGetter(getter);
             var.setWithSetter(setter);
          }
       }

       /**
        * Check if the given class is built-in.
        *
        * @param    name
        *           The qualified class name.
        *
        * @return   See above.
        */
       public boolean isBuiltInClass(String name)
       {
          SearchResult found = resolveClassName(name);

          return found != null && found.builtin;
       }

       /**
        * Check if the given name is for a builtin function.
        *
        * @param    name
        *           The function's name.
        *
        * @return   See above.
        */
       public boolean isBuiltinFunction(String name)
       {
          Function def = lookupFunctionDef(name);

          return def != null && def.isBuiltin();
       }

       /**
        * Check if the given class is a .NET class.
        *
        * @param    name
        *           The qualified class name.
        *
        * @return   See above.
        */
       public boolean isDotNetClass(String name)
       {
          SearchResult found = resolveClassName(name);

          return found != null && found.dotnet;
       }

       /**
        * Reports if the given data member is a static member of the given class.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a member within the local class definition.
        * @param    name
        *           Member name to lookup.
        *
        * @return   <code>true</code> if the member is static.  <code>false</code> otherwise
        *           including the case where the member does not exist at all.
        */
       public boolean isStaticDataMember(String cname, String name)
       {
          ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);
          return cls != null && cls.isStaticDataMember(name, calcAccessMode(cname));
       }

       /**
        * Searches the named class' hierarchy for the named data member. This
        * will match both static and instance members.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a member within the local class definition.
        * @param    name
        *           Member name to lookup.
        *
        * @return   The token type of the member or -1 if no match was found.
        */
       public int lookupDataMember(String cname, String name)
       {
          int type = lookupDataMember(cname, name, false);

          return type == -1 ? lookupDataMember(cname, name, true) : type;
       }

       /**
        * Searches the named class' hierarchy for the named data member.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a member within the local class definition.
        * @param    name
        *           Member name to lookup.
        * @param    isStatic
        *           If <code>true</code>, only static methods will be looked up.
        *           Otherwise any method can be returned.
        *
        * @return   The token type of the member or -1 if no match was found.
        */
       public int lookupDataMember(String cname, String name, boolean isStatic)
       {
          return lookupDataMemberWorker(cname, name, isStatic);
       }

       /**
        * Searches the named class' hierarchy for the named data member and
        * returns the fully qualified class name if that member represents an
        * object instance.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a member within the local class definition.
        * @param    name
        *           Member name to lookup.
        * @param    isStatic
        *           <code>true</code> if this is a static reference.
        *
        * @return   Fully qualified class name of the the object instance
        *           represented by the named variable, or <code>null</code> if no
        *           such variable exists.
        */
       public String lookupDataMemberClass(String  cname, String  name, boolean isStatic)
       {
          ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);

          int access = calcAccessMode(cname);

          return (cls == null) ? null : cls.lookupVariableClassName(name, access, isStatic);
       }

       /**
        * Searches the named class' hierarchy for the named method. This will match any method
        * (static or instance) of the given name.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is a method call
        *           within the local class definition.
        * @param    method
        *           Method name to lookup.
        * @param    isStatic
        *           If <code>true</code>, only static methods will be looked up. Otherwise any
        *           method can be returned.
        *
        * @return   {@code true} if a match was found.
        */
       public boolean isObjectMethod(String cname, String method, boolean isStatic)
       {
          return lookupObjectMethod(cname, method, isStatic) != -1;
       }

       /**
        * Searches the named class' hierarchy for the named method.
        * <p>
        * <b>If a positive value is returned, the caller can be assured that there is an accessible
        * method with that name BUT the actual type of the return value cannot be known until the
        * signature is known. This is due to the fact that the same method name can be present with
        * differing return values and only the parameter signature matching can differentiate the
        * exact method that will be called.</b>
        * <p>
        * <b>Do not call this from any location where the type must be known authoritatively.</b>
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a method call within the local class definition.
        * @param    method
        *           Method name to lookup.
        * @param    isStatic
        *           If <code>true</code>, only static methods will be looked up.
        *           Otherwise any method can be returned.
        *
        * @return   The return type of the method or -1 if no match was found.
        */
       public int lookupObjectMethod(String cname, String method, boolean isStatic)
       {
          ClassDefinition current  = getCurrentClassDef();
          ClassDefinition cls      = (cname == null) ? current : lookupClass(cname);
          boolean         internal = (cls == current);
          int             access   = calcAccessMode(cname);

          return (cls == null) ? -1 : cls.guessMethodType(method, access, isStatic, internal);
       }

       /**
        * Add all specified keyword token types to the correct attribute and method type mappings.
        *
        * @param    attrsAndMethods
        *           The attribute/method mappings.
        */
       public void addAllAttributesAndMethods(Map<Integer, Integer> attrsAndMethods)
       {
          if (attributes != null)
          {
             attributes.putAll(attrsAndMethods);
          }
       }

       /**
        * Set the attribute or method's OO type, in the {@link #attrTypes} map.
        *
        * @param    ktype
        *           Keyword token type to be used as the key in the dictionary.
        * @param    cls
        *           Fully qualified name of the class (user defined type) that
        *           is returned from this method. Must be <code>null</code> if
        *           the resultType is not <code>ATTR_CLASS</code> or
        *           <code>METH_CLASS</code>.
        */
       public void setAttributeOrMethodType(int ktype, String cls)
       {
          if (cls != null && attrTypes != null)
          {
             attrTypes.put(ktype, cls);
          }
       }

       /**
        * Adds an attribute or method keyword token type and the associated token
        * type of the data type (for attributes) or return type (for methods) to
        * the dictionary of attributes/methods.
        * <p>
        * This is a flat dictionary (there is no scoping).  The keyword's token
        * type is used as the key and the result token type is the value.
        * <p>
        * This form of the method is NOT suitable for adding attributes or
        * methods with <code>ATTR_CLASS</code> or <code>METH_CLASS</code> result
        * types. This is due to the fact that there is no mechanism to
        * associate a class name with the resulting attribute or method.
        *
        * @param    ktype
        *           Keyword token type to be used as the key in the dictionary.
        * @param    rtype
        *           The token type representing the attribute data type or the
        *           method return type as defined by the Lexer and Parser.
        */
       public void addAttributeOrMethod(int ktype, int rtype)
       {
          addAttributeOrMethod(ktype, rtype, null);
       }

       /**
        * Adds an attribute or method keyword token type and the associated token
        * type of the data type (for attributes) or return type (for methods) to
        * the dictionary of attributes/methods.
        * <p>
        * This is a flat dictionary (there is no scoping).  The keyword's token
        * type is used as the key and the result token type is the value.
        *
        * @param    ktype
        *           Keyword token type to be used as the key in the dictionary.
        * @param    rtype
        *           The token type representing the attribute data type or the
        *           method return type as defined by the Lexer and Parser.
        * @param    cls
        *           Fully qualified name of the class (user defined type) that
        *           is returned from this method. Must be <code>null</code> if
        *           the resultType is not <code>ATTR_CLASS</code> or
        *           <code>METH_CLASS</code>.
        */
       public void addAttributeOrMethod(int ktype, int rtype, String cls)
       {
          attributes.put(ktype, rtype);

          if (cls != null)
             attrTypes.put(ktype, cls);
       }

       /**
        * Searches the attribute and method dictionary for a token type matching
        * the passed keyword.  The token type of the method return type or the
        * attribute data type is returned, if a match is found.  A token type
        * of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param    ktype
        *           Token type of the keyword to lookup.
        *
        * @return   The attribute or method token type that was found or -1 if no
        *           match was found.
        */
       public int lookupAttributeOrMethod(int ktype)
       {
          Integer result = attributes.get(ktype);

          return result == null ? -1 : result.intValue();
       }

       /**
        * Searches the attribute and method dictionary for the fully qualified
        * class name that is the actual type of an <code>ATTR_CLASS</code> or
        * <code>METH_CLASS</code> matching the given keyword.
        *
        * @param    ktype
        *           Token type of the keyword to lookup.
        *
        * @return   The fully qualified class name that is the actual type
        *           of an <code>ATTR_CLASS</code> or <code>METH_CLASS</code>.
        *           <code>null</code> if the given keyword is not a valid class
        *           attribute or method.
        */
       public String lookupAttributeOrMethodClass(int ktype)
       {
          int ttype = lookupAttributeOrMethod(ktype);

          String result = null;

          if (ttype == ATTR_CLASS || ttype == METH_CLASS)
          {
             result = attrTypes.get(ktype);
          }

          return result;
       }

       /**
        * Adds a label and a token type to the dictionary of labels.  This is
        * a special form of a scoped dictionary where there is only one label name
        * per scope (Progress does not allow lable aliases). The label name is
        * used as the key and the token type is the value.
        *
        * @param   name
        *          Label name.
        * @param   tokenType
        *          The label's token type as defined by the Lexer and Parser.
        */
       public void addLabel(String name, int tokenType)
       {
          labelDict.addScope(null);
          labelDict.addSymbol(false, name, Integer.valueOf(tokenType));
       }

       /**
        * Searches the label dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The label's token type that was found or -1 if no match was
        *          found.
        */
       public int lookupLabel(String name)
       {
          return lookupWorkerExact(labelDict, name);
       }

       /**
        * Deletes the current label from the dictionary of labels.  This is
        * a special form of a scoped dictionary where there is only one label name
        * per scope (Progress does not allow lable aliases). So, deleting the
        * current label is deleting the current scope.
        */
       public void deleteLabel()
       {
          labelDict.deleteScope();
       }

       /**
        * Adds a widget and a token type to the dictionary of widgets.
        * This dictionary supports scoping that matches the variable namespace.
        * The widget name is used as the key and the token type is the value.
        * <p>
        * Note that this is artificially separated from the variable namespace
        * for implementation simplicity, but in Progress there is only one
        * namespace that handles both variables and widgets.
        *
        * @param   name
        *          Widget name.
        * @param   tokenType
        *          The procedure's token type as defined by the Lexer and Parser.
        */
       public void addWidget(String name, int tokenType)
       {
          Variable var = new Variable(name, tokenType, null);

          addWrappedWorker(widgetDict, false, name, var);
       }

       /**
        * Searches the widget dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        * <p>
        * Note that this is artificially separated from the variable namespace
        * for implementation simplicity, but in Progress there is only one
        * namespace that handles both variables and widgets.
        *
        * @param   name
        *          Text string to match.
        * @return  The widget's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupWidget(String name)
       {
          Variable var = lookupLocalDef(name);
          if (var != null)
          {
             int ttype = var.getTokenType();
             // any lookup for a variable which can not be a possible widget will return 'no match'
             switch (ttype)
             {
                case VAR_RAW:
                case VAR_MEMPTR:
                case VAR_CLASS:
                case VAR_HANDLE:
                case VAR_COM_HANDLE:
                   return -1;
             }
          }
          return lookupWrappedType(lookupWrapped(widgetDict, name));
       }

       /**
        * Adds a frame and a token type to the dictionary of frames. The frame
        * name is used as the key and the token type is the value.
        *
        * @param   name
        *          Frame name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        */
       public void addFrame(String name, int tokenType)
       {
          frameDict.addSymbol(false, name, Integer.valueOf(tokenType));
       }

       /**
        * Get the set of field reference ASTs associated with a particular frame.
        *
        * @param   frame
        *          Name of the frame for which field references should be retrieved.
        *
        * @return  Set of field references associated with the given frame.
        */
       public Set<Aast> getFrameFields(String frame)
       {
          Set<Aast> fieldSet = fieldWidgetDict.lookup(frame);
          if (fieldSet == null)
          {
             fieldSet = new HashSet<>();
             fieldWidgetDict.addSymbol(false, frame, fieldSet);
          }

          return fieldSet;
       }

       /**
        * Map a collection of field reference ASTs to the name of the frame in which they reside as
        * widgets.
        *
        * @param   frame
        *          Name of the frame contaning the field widgets.
        * @param   fields
        *          Collection of field ASTs to be stored.
        */
       public void addFrameFields(String frame, Collection<Aast> fields)
       {
          Set<Aast> fieldSet = getFrameFields(frame);
          for (Aast fld : fields)
          {
             String key1 = fld.getAnnotation("schemaname") + "$" + fld.getAnnotation("bufname");
             boolean found = false;

             // a field is unique if schemaName$bufname are the same...
             for (Aast sfld : fieldSet)
             {
                String key2 = sfld.getAnnotation("schemaname") + "$" + sfld.getAnnotation("bufname");
                if (key1.equals(key2))
                {
                   found = true;
                   break;
                }
             }
             if (!found)
             {
                fieldSet.add(fld);
             }
          }
       }

       /**
        * Map a field reference ASTs to the name of the frame in which it resides as a widget.
        *
        * @param   frame
        *          Name of the frame contaning the field widgets.
        * @param   field
        *          Field AST to be stored.
        */
       public void addFrameField(String frame, Aast field)
       {
          addFrameFields(frame, Collections.singleton(field));
       }

       /**
        * Check if the USE-DICT-EXPS option is active for the given frame.
        *
        * @param    frame
        *           The frame name.
        *
        * @return   <code>true</code> if USE-DICT-EXPS is active.
        */
       public boolean isUseDictExps(String frame)
       {
          Boolean state = useDictExps.lookup(frame);

          return state != null && state;
       }

       /**
        * Set the USE-DICT-EXPS for this frame, if not already set.
        *
        * @param    frame
        *           The frame name.
        */
       public void markUseDictExps(String frame)
       {
          Boolean state = useDictExps.lookup(frame);

          if (state == null)
          {
             useDictExps.addSymbol(false, frame, true);
          }
       }

       /**
        * Searches the frame dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The frame's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupFrame(String name)
       {
          return lookupWorkerExact(frameDict, name);
       }

       /**
        * Adds a menu or sub-menu and a token type to the dictionary of menus.
        * The menu or sub-menu name is used as the key and the token type is the
        * value.
        *
        * @param   name
        *          Menu or sub-menu name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        */
       public void addMenu(String name, int tokenType)
       {
          Variable var = new Variable(name, tokenType, null);

          addWrappedWorker(menuDict, false, name, var);
       }

       /**
        * Searches the menu dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        * <p>
        * This supports both menus and sub-menus.
        *
        * @param   name
        *          Text string to match.
        * @return  The menu's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupMenu(String name)
       {
          return lookupWrappedType(lookupWrapped(menuDict, name));
       }

       /**
        * Adds a menu-item and a token type to the dictionary of menu-items.
        * The menu-item name is used as the key and the token type is the value.
        *
        * @param   name
        *          Menu-item name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        */
       public void addMenuItem(String name, int tokenType)
       {
          Variable var = new Variable(name, tokenType, null);

          addWrappedWorker(menuItemDict, false, name, var);
       }

       /**
        * Searches the menu-item dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The menu-item's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupMenuItem(String name)
       {
          return lookupWrappedType(lookupWrapped(menuItemDict, name));
       }

       /**
        * Adds a stream and a token type to the dictionary of streams.
        * The stream name is used as the key and the token type is the value.
        *
        * @param   name
        *          Stream name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        */
       public void addStream(String name, int tokenType)
       {
          streamDict.addSymbol(false, name, tokenType);
       }

       /**
        * Searches the stream dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The stream's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupStream(String name)
       {
          return lookupWorkerExact(streamDict, name);
       }

       /**
        * Adds a query and a token type to the dictionary of queries.
        * The query name is used as the key and the token type is the value.
        *
        * @param    name
        *           Query name.
        * @param    node
        *           The definition node for this member.
        * @param    tokenType
        *           The token type as defined by the Lexer and Parser.
        * @param    am
        *           Access modifiers if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        * @param    st
        *           Static specifier if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        */
       public void addQuery(String name, Aast node, int tokenType, Aast am, Aast st)
       {
          qryDict.addSymbol(false, name, tokenType);

          ClassDefinition def = getCurrentClassDef();

          if (def != null)
          {
             def.addQuery(name, node, tokenType, decodeAccessMode(am), decodeStatic(st), inMethod);
          }
       }

       /**
        * Searches the query dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The query's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupQuery(String name)
       {
          int result = lookupWorkerExact(qryDict, name);

          if (result == -1)
          {
             ClassDefinition[] parents = getCurrentParentsDef();

             if (parents != null)
             {
                for (ClassDefinition par : parents)
                {
                   // TODO: we need to know the context of the calling code here to know whether
                   //       this is a static or instance lookup; for now we try both
                   result = par.lookupQuery(name, KW_PROTECTD, false);

                   if (result == -1)
                   {
                      result = par.lookupQuery(name, KW_PROTECTD, true);
                   }

                   if (result != -1)
                      break;
                }
             }
          }

          return result;
       }

       /**
        * Adds a dataset and a token type to the dictionary of datasets. The dataset name is used as
        * the key and the token type is the value.
        *
        * @param   name
        *          Dataset name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        * @param   node
        *          The definition AST.
        * @param   am
        *          Access modifiers if not null. Only will be non-null if this is a resource
        *          defined as a member of a class definition.
        * @param   st
        *          Static specifier if not null. Only will be non-null if this is a resource
        *          defined as a member of a class definition.
        */
       public void addDataSet(String name, int tokenType, Aast node, Aast am, Aast st)
       {
          ClassDefinition def = getCurrentClassDef();
          boolean isStatic = decodeStatic(st);

          Variable dataset = createVariable(name, tokenType, node, isStatic);
          if (def != null)
          {
             def.addDataSet(dataset, node, name, tokenType, decodeAccessMode(am), isStatic, inMethod);
          }

          addWrappedWorker(dsetDict, false, name, dataset);
       }

       /**
        * Searches the dataset dictionary for a symbol matching the passed key.  The token type of the
        * match is returned.  Note that a token type of -1 is invalid in the Lexer and Parser, so this
        * value is used to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        *
        * @return  The dataset's token type that was found or -1 if no match was found.
        */
       public int lookupDataSet(String name)
       {
          int result = -1;
          Variable ds = (Variable) dsetDict.lookupSymbol(name);

          if (ds != null)
          {
             result  = ds.getTokenType();
          }
          else
          {
             ClassDefinition[] parents = getCurrentParentsDef();

             if (parents != null)
             {
                for (ClassDefinition par : parents)
                {
                   // TODO: we need to know the context of the calling code here to know whether
                   //       this is a static or instance lookup; for now we try both
                   result = par.lookupDataSet(name, KW_PROTECTD, false);

                   if (result == -1)
                   {
                      result = par.lookupDataSet(name, KW_PROTECTD, true);
                   }

                   if (result != -1)
                      break;
                }
             }
          }

          return result;
       }

       /**
        * Annotates a DATASET or DATA-SOURCE node with the information needed to relate the static
        * defined variable references to their definitions.
        *
        * @param  cname
        *         The class in which to lookup the dataset or <code>null</code> for the current
        *         class def.
        * @param  isStatic
        *         Flag indicating that we are in a static context.
        * @param  dsname
        *         The legacy dataset name.
        * @param  root
        *         The Ast node that will be annotated.
        * @param  type
        *         The type of the token. May be DATASET or DATA-SOURCE when processing a reference
        *         but also DEFINE_DATASET or DEFINE_DATA_SOURCE when processing their definitions.
        * @param  original
        *         Flag indicating this is the original definition.
        */
       public void setDatasetOptions(String  cname,
                                     boolean isStatic,
                                     String  dsname,
                                     Aast    root,
                                     long    type,
                                     boolean original)
       {
          Variable ds = null;
          if (type == DEFINE_DATASET || type == DATA_SET)
          {
             ds = (Variable) dsetDict.lookupSymbol(dsname);
             root.putAnnotation("type", (long) DATA_SET);
          }
          else if (type == DEFINE_DATA_SOURCE || type == DATA_SOURCE)
          {
             ds = (Variable) dsrcDict.lookupSymbol(dsname);
             root.putAnnotation("type", (long) DATA_SOURCE);
          }

          java.util.function.Function<String, Variable> lookup = (clsName) ->
          {
             ClassDefinition cls = null;
             int access = KW_PRIVATE;
             if (clsName != null)
             {
                access = calcAccessMode(clsName);
                cls = lookupClass(clsName);
             }
             else
             {
                cls = getCurrentClassDef();
             }

             if (type == DEFINE_DATASET || type == DATA_SET)
             {
                return cls.lookupDataSetWrapper(dsname, access, isStatic);
             }
             else if (type == DEFINE_DATA_SOURCE || type == DATA_SOURCE)
             {
                return cls.lookupDataSourceWrapper(dsname, access, isStatic);
             }

             return null;
          };
          annotateVariableOptions(cname, isStatic, dsname, root, original, ds, lookup);
       }

       /**
        * Adds a data-source and a token type to the dictionary of data-sources.
        * The data-source name is used as the key and the token type is the value.
        *
        * @param    name
        *           Data-source name.
        * @param    tokenType
        *           The token type as defined by the Lexer and Parser.
        * @param    node
        *           The definition AST.
        * @param    am
        *           Access modifiers if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        * @param    st
        *           Static specifier if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        */
       public void addDataSource(String name, int tokenType, Aast node, Aast am, Aast st)
       {
          ClassDefinition def = getCurrentClassDef();
          boolean isStatic = decodeStatic(st);
          Variable ds = createVariable(name, tokenType, node, isStatic);

          if (def != null)
          {
             def.addDataSource(ds, node, name, tokenType, decodeAccessMode(am), isStatic, inMethod);
          }

          addWrappedWorker(dsrcDict, false, name, ds);
       }

       /**
        * Searches the data-source dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The data-source's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupDataSource(String name)
       {
          int result = -1;
          Variable ds = (Variable) dsrcDict.lookupSymbol(name);

          if (ds != null)
          {
             result  = ds.getTokenType();
          }
          else
          {
             ClassDefinition[] parents = getCurrentParentsDef();

             if (parents != null)
             {
                for (ClassDefinition par : parents)
                {
                   // TODO: we need to know the context of the calling code here to know whether
                   //       this is a static or instance lookup; for now we try both
                   result = par.lookupDataSource(name, KW_PROTECTD, false);

                   if (result == -1)
                   {
                      result = par.lookupDataSource(name, KW_PROTECTD, true);
                   }

                   if (result != -1)
                      break;
                }
             }
          }

          return result;
       }

       /**
        * Adds a data-relation and a token type to the dictionary of data-relations. The data-relation
        * name is used as the key and the token type is the value.
        *
        * @param   name
        *          Data-relation name.
        * @param   tokenType
        *          The token type as defined by the Lexer and Parser.
        */
       public void addDataRelation(String name, int tokenType)
       {
          drelDict.addSymbol(false, name, tokenType);
       }

       /**
        * Searches the data-relation dictionary for a symbol matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match.
        *
        * @param   name
        *          Text string to match.
        * @return  The data-relation's token type that was found or -1 if no match
        *          was found.
        */
       public int lookupDataRelation(String name)
       {
          return lookupWorkerExact(drelDict, name);
       }

       /**
        * Loads a set of databases into the schema dictionary.  This is the way that non-default databases are
        * 'connected' or added into the known namespace.
        *
        * @param    dblist
        *           The {@code Set} of database names to load.
        */
       public void loadSchemaDatabases(Set<String> dblist)
       {
          if (dblist == null || schemaDict == null)
          {
             return;
          }

          try
          {
             for (String db : dblist)
             {
                schemaDict.loadSchema(db);
             }
          }
          catch (SchemaException excpt)
          {
             LOG.log(Level.WARNING, "", excpt);
          }
       }

       /**
        * Adds a list of aliases (each to an associated database name) to the
        * schema dictionary.
        *
        * @param    list
        *           A list of string arrays, each representing an alias and
        *           associated database to add to the schema dictionary.
        */
       public void loadAliases(List<String[]> list)
       {
          if (schemaDict != null)
          {
             int len = list.size();

             for (int i = 0; i < len; i++)
             {
                String[] pair   = list.get(i);
                boolean  result = schemaDict.createAlias(pair[0], pair[1]);

                if (!result)
                {
                   // todo: yes, more logging here too
                   LOG.log(Level.WARNING, "createAlias() failed on " + pair[0] + " and database " + pair[1]);
                }
             }
          }
       }

       /**
        * Returns the shortest unique non-abbreviated buffer name for a field
        * or table reference.  This allows one to calculate the identical buffer
        * name from each reference, such that all references that use the same
        * buffer will share the same buffer name, even if the references are
        * different (due to abbreviations, field vs. table, qualified vs
        * unqualified) in the original source code.
        *
        * @param   name
        *          Text string to match, exactly as found in the original source
        *          code (abbreviated, qualified or inqualified).
        * @param   field
        *          <code>true</code> if this is a field reference,
        *          <code>false</code> if this is a table reference.
        *
        * @return  The shortest unabbreviated unique buffer name or
        *          <code>null</code> if no match was found.
        */
       public String lookupBufferName(String name, boolean field)
       {
          return lookupBufferName(name, field, false, false);
       }

       /**
        * Returns the shortest unique non-abbreviated buffer name for a field
        * or table reference.  This allows one to calculate the identical buffer
        * name from each reference, such that all references that use the same
        * buffer will share the same buffer name, even if the references are
        * different (due to abbreviations, field vs. table, qualified vs
        * unqualified) in the original source code.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Text string to match, exactly as found in the original source
        *          code (abbreviated, qualified or inqualified).
        * @param   field
        *          <code>true</code> if this is a field reference,
        *          <code>false</code> if this is a table reference.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The shortest unabbreviated unique buffer name or
        *          <code>null</code> if no match was found.
        */
       public String lookupBufferName(String name, boolean field, boolean forceTemp, boolean forcePersistent)
       {
          String result = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<String> bff = (SchemaDictionary dict) ->
                {
                   return dict.getBufferForField(name, forceTemp, forcePersistent);
                };

                SchemaHelper<String> bft = (SchemaDictionary dict) ->
                {
                   return dict.getBufferForTable(name, forceTemp, forcePersistent);
                };

                Predicate<String> notNull = (String val) -> { return (val == null); };

                result = processHierarchy(field ? bff : bft, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return result;
       }

       /**
        * Returns the logical database name being used for a given table
        * reference.  The resulting name may be different from the database
        * portion of the fully qualified schema name as returned by the
        * {@link #lookupFullTableName} since the same schema can be referenced
        * as multiple database instances.
        *
        * @param   name
        *          Text string to match, exactly as found in the original source
        *          code (abbreviated, qualified or inqualified).
        *
        * @return  The logical database name or <code>null</code> if no match was
        *          found.
        */
       public String lookupDatabaseName(String name)
       {
          return lookupDatabaseName(name, false, false);
       }

       /**
        * Returns the logical database name being used for a given table
        * reference.  The resulting name may be different from the database
        * portion of the fully qualified schema name as returned by the
        * {@link #lookupFullTableName} since the same schema can be referenced
        * as multiple database instances.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Text string to match, exactly as found in the original source
        *          code (abbreviated, qualified or inqualified).
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The logical database name or <code>null</code> if no match was
        *          found.
        */
       public String lookupDatabaseName(String name, boolean forceTemp, boolean forcePersistent)
       {
          String result = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<String> ldft = (SchemaDictionary dict) ->
                {
                   String dbname = null;

                   // getLogicalDatabaseForTable() assumes that the node will be found, so we can
                   // only call it if we know the node exists in the current dictionary
                   if (dict.isTable(name, forceTemp, forcePersistent))
                   {
                      dbname = dict.getLogicalDatabaseForTable(name, forceTemp, forcePersistent);
                   }

                   return dbname;
                };

                Predicate<String> notNull = (String val) -> { return (val == null); };

                result = processHierarchy(ldft, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return result;
       }

       /**
        * Returns the fully qualified name for a field matching the passed
        * key.
        * <p>
        * Schema field names can come in 3 forms:
        * <ul>
        *    <li> unqualified (e.g. address1)
        *    <li> partially qualified with a tablename (e.g. customer.address1)
        *    <li> fully qualified with database and table (e.g.
        *         salesapp.customer.address1)
        * </ul>
        * <p>
        * All 3 forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of field name portion. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table name is the smallest possible
        * abbreviation.  As such, no ambiguity is allowed in the name passed as
        * an argument.  There must be only one match that is ever found in the
        * schema namespace.
        *
        * @param   name
        *          Text string to match, in either of the 3 possible forms.
        *
        * @return  The field's fully qualified schema name or <code>null</code>
        *          if no match was found.
        */
       public String lookupFullFieldName(String name)
       {
          String fqn = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<String> ffn =
                   (SchemaDictionary dict) -> { return dict.getFullFieldName(name); };

                Predicate<String> notNull = (String val) -> { return (val == null); };

                fqn = processHierarchy(ffn, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return fqn;
       }

       /**
        * Searches the schema dictionary for a field matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous field name or due to some other problem with the schema
        * dictionary.
        * <p>
        * Schema field names can come in 3 forms:
        * <ul>
        *    <li> unqualified (e.g. address1)
        *    <li> partially qualified with a tablename (e.g. customer.address1)
        *    <li> fully qualified with database and table (e.g.
        *         salesapp.customer.address1)
        * </ul>
        * <p>
        * All 3 forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        *
        * @param    table
        *           The table being processed or {@code null} if no specific table is being
        *           processed.
        * @param    name
        *           Text string to match, in any of the supported 3 forms.
        *
        * @return   The label's token type that was found or -1 if no match was
        *           found.
        */
       public int lookupField(NameNode table, String name)
       {
          int type = -1;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<Integer> fdt = (SchemaDictionary dict) ->
                {
                   int ftype = -1;
                   String fldName = name;

                   if (indexFieldSearch && useIndexFieldSearch && name.indexOf(".") == -1)
                   {
                      fldName = String.format("%s.%s", indexTable, name);

                      if (!dict.isField(fldName))
                      {
                         // fallback to the original name and disable index matching
                         fldName = name;
                         endIndexFieldSearch();
                      }
                   }

                   ftype = dict.getFieldDataType(table, fldName);

                   return ftype;
                };

                Predicate<Integer> notNull =
                   (Integer val) -> { return (val == null || val.intValue() == -1) ; };

                type = processHierarchy(fdt, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return type;
       }

       /**
        * Searches the schema dictionary for a field matching the passed
        * key and returns a boolean reporting if it is a valid field or not.
        * <p>
        * Schema field names can come in 3 forms:
        * <ul>
        *    <li> unqualified (e.g. address1)
        *    <li> partially qualified with a tablename (e.g. customer.address1)
        *    <li> fully qualified with database and table (e.g.
        *         salesapp.customer.address1)
        * </ul>
        * <p>
        * All 3 forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        * <p>
        * <b>This method is safe to use inside a semantic predicate since it
        * will not throw an exception if the name is not found or if there is
        * ambiguity detected!</b>
        *
        * @param   name
        *          Text string to match, in any of the supported 3 forms.
        *
        * @return  true if the name is a valid field, false otherwise.
        */
       public boolean isField(String name)
       {
          boolean result = false;

          if (schemaDict != null)
          {
             try
             {
                SchemaHelper<Boolean> isFld = (SchemaDictionary dict) ->
                {
                   String fldName = name;

                   if (indexFieldSearch && useIndexFieldSearch && name.indexOf('.') == -1)
                   {
                      fldName = String.format("%s.%s", indexTable, name);
                      if (dict.isField(fldName))
                      {
                         return true;
                      }
                      else
                      {
                         // fallback to the original name and disable index matching
                         fldName = name;
                         endIndexFieldSearch();
                      }
                   }

                   return dict.isField(fldName);
                };

                Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };

                result = processHierarchy(isFld, whileFalse);
             }
             catch (SchemaException exc)
             {
                // this should not happen
                LOG.log(Level.SEVERE, "", exc);
             }
          }

          return result;
       }

       /**
        * Searches the schema dictionary for a field matching the passed
        * key and returns a boolean reporting if it is a valid field or not.
        * Exact name matching version of <code>isField</code>
        * @param   name
        *          Text string to match, in any of the supported 3 forms, exact name match required
        *
        * @return  true if the name is a valid field, false otherwise.
        */
       public boolean isFieldExactName(String name)
       {
          boolean result = false;

          if (schemaDict != null)
          {
             try
             {
                SchemaHelper<Boolean> isFld = (SchemaDictionary dict) ->
                {
                   String fldName = name;

                   if (indexFieldSearch && useIndexFieldSearch && name.indexOf('.') == -1)
                   {
                      fldName = String.format("%s.%s", indexTable, name);
                      if (dict.isExactMatchField(fldName) )
                      {
                         return true;
                      }
                      else
                      {
                         // fallback to the original name and disable index matching
                         fldName = name;
                         endIndexFieldSearch();
                      }
                   }

                   return dict.isExactMatchField(fldName);
                };

                Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };

                result = processHierarchy(isFld, whileFalse);
             }
             catch (SchemaException exc)
             {
                // this should not happen

                LOG.log(Level.SEVERE, "", exc);
             }
          }

          return result;
       }

       /**
        * Returns the fully qualified name for a table matching the passed
        * key.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms of input are accepted.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames portion. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table name is the smallest possible
        * abbreviation.  As such, no ambiguity is allowed in the name passed as
        * an argument.  There must be only one match that is ever found in the
        * schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        *
        * @param   name
        *          Text string to match, in either of the 2 possible forms.
        *
        * @return  The table's fully qualified schema name or <code>null</code>
        *          if no match was found.
        */
       public String lookupFullTableName(String name)
       {
          return lookupFullTableName(name, false, false);
       }

       /**
        * Returns the fully qualified name for a table matching the passed
        * key.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms of input are accepted.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames portion. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table name is the smallest possible
        * abbreviation.  As such, no ambiguity is allowed in the name passed as
        * an argument.  There must be only one match that is ever found in the
        * schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Text string to match, in either of the 2 possible forms.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The table's fully qualified schema name or <code>null</code>
        *          if no match was found.
        */
       public String lookupFullTableName(String name, boolean forceTemp, boolean forcePersistent)
       {
          String fqn = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<String> ftn = (SchemaDictionary dict) ->
                {
                   return dict.getFullTableName(name, forceTemp, forcePersistent);
                };

                Predicate<String> notNull = (String val) -> { return (val == null); };

                fqn = processHierarchy(ftn, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return fqn;
       }

       /**
        * Searches the schema dictionary for a table matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous table name or due to some other problem with the schema
        * dictionary.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms of input are accepted.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames portion. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table name is the smallest possible
        * abbreviation.  As such, no ambiguity is allowed in the name passed as
        * an argument.  There must be only one match that is ever found in the
        * schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        *
        * @param    name
        *           Text string to match, in either of the 2 possible forms.
        *
        * @return   The table's token type that was found or -1 if no match was
        *           found.
        */
       public int lookupTable(String name)
       {
          NameNode table = lookupTableNode(name, false, false);

          return (table == null) ? -1 : table.getType();
       }

       /**
        * Searches the schema dictionary for a table matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous table name or due to some other problem with the schema
        * dictionary.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms of input are accepted.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames portion. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table name is the smallest possible
        * abbreviation.  As such, no ambiguity is allowed in the name passed as
        * an argument.  There must be only one match that is ever found in the
        * schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param    name
        *           Text string to match, in either of the 2 possible forms.
        * @param    forceTemp
        *           If <code>true</code>, force a temp-table lookup.
        * @param    forcePersistent
        *           If <code>true</code>, force a persistent table lookup.
        *
        * @return   The table's name node if found or {@code null} if no match was found.
        */
       public NameNode lookupTableNode(String name, boolean forceTemp, boolean forcePersistent)
       {
          NameNode found = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<NameNode> ftt = (SchemaDictionary dict) ->
                {
                   return dict.findTableNode(name, forceTemp, forcePersistent);
                };

                Predicate<NameNode> notNull = (NameNode val) -> { return val == null; };

                found = processHierarchy(ftt, notNull);
             }
          }
          catch (AmbiguousSchemaNameException exc)
          {
             // if not found, let the caller decide what to do (look again without forcing a specific
             // db or fail).
             if (!(forceTemp || forcePersistent))
             {
                LOG.log(Level.WARNING,"", exc);
             }

             return null;
          }
          catch (IllegalArgumentException | SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return found;
       }

       /**
        * Searches the schema dictionary for a table matching the passed
        * key and returns a boolean reporting if it is a valid table or not.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        * <p>
        * <b>This method is safe to use inside a semantic predicate since it
        * will not throw an exception if the name is not found or if there is
        * ambiguity detected!</b>
        *
        * @param   name
        *          Text string to match, in either of the supported 2 forms.
        *
        * @return  true if the name is a valid field, false otherwise.
        */
       public boolean isTable(String name)
       {
          boolean result = false;

          if (schemaDict != null)
          {
             try
             {
                SchemaHelper<Boolean> isTbl =
                   (SchemaDictionary dict) -> { return dict.isTable(name); };

                Predicate<Boolean> whileFalse = (Boolean val) -> { return (val == Boolean.FALSE); };

                result = processHierarchy(isTbl, whileFalse);
             }
             catch (SchemaException exc)
             {
                // this should not happen
                LOG.log(Level.SEVERE, "", exc);
             }
          }

          return result;
       }

       /**
        * Searches the schema dictionary for a field matching the passed
        * name and then will return the token type of the backing construct
        * (table, temp-table or work-table) to which this record belongs.  Note
        * that a token type of -1 is invalid in the Lexer and Parser, so this
        * value is used to denote a search that found no match, or one that
        * failed due to an ambiguous table name or due to some other problem with
        * the schema dictionary.
        * <p>
        * Schema field names can come in 3 forms:
        * <ul>
        *    <li> unqualified (e.g. address1)
        *    <li> partially qualified with a tablename (e.g. customer.address1)
        *    <li> fully qualified with database and table (e.g.
        *         salesapp.customer.address1)
        * </ul>
        * <p>
        * All 3 forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        *
        * @param   name
        *          Text string to match, in any of the supported 3 forms.
        *
        * @return  The backing record's token type that was found or -1 if no
        *          match was found.
        */
       public int lookupFieldRecordType(String name)
       {
          int type = -1;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<Integer> frt =
                   (SchemaDictionary dict) -> { return dict.getFieldRecordType(name); };

                Predicate<Integer> notNull =
                   (Integer val) -> { return (val == null || val.intValue() == -1) ; };

                type = processHierarchy(frt, notNull);
             }
          }
          catch (IllegalArgumentException | SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return type;
       }

       /**
        * Searches the schema dictionary for a record matching the passed
        * name and then will return the token type of the backing construct
        * (table, temp-table or work-table) to which this record belongs. By
        * definition, if one passes a name that references a table, temp-table
        * or work-table directly, then this method returns the same token type
        * value as can be obtained via {@link #lookupTable}. However, if the
        * name references a buffer, then this method will return the type of
        * the backing construct to which the buffer refers.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous table name or due to some other problem with schema
        * dictionary.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        *
        * @param   name
        *          Text string to match, in either of the supported 2 forms.
        *
        * @return  The backing record's token type that was found or -1 if no
        *          match was found.
        */
       public int lookupTableRecordType(String name)
       {
          return lookupTableRecordType(name, false, false);
       }

       /**
        * Searches the schema dictionary for a record matching the passed
        * name and then will return the token type of the backing construct
        * (table, temp-table or work-table) to which this record belongs. By
        * definition, if one passes a name that references a table, temp-table
        * or work-table directly, then this method returns the same token type
        * value as can be obtained via {@link #lookupTable}. However, if the
        * name references a buffer, then this method will return the type of
        * the backing construct to which the buffer refers.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous table name or due to some other problem with schema
        * dictionary.
        * <p>
        * Schema table names can come in 2 forms:
        * <ul>
        *    <li> unqualified, table name only (e.g. customer)
        *    <li> fully qualified with database and table (e.g. salesapp.customer)
        * </ul>
        * <p>
        * Both forms are accepted as input.  All searches are done on
        * a case-insensitive basis.  In addition, the Progress 4GL schema
        * supports optional abbreviation of tablenames and fieldnames. These
        * abbreviations are valid as long as they represent a unique match. The
        * smallest unique match to a table or field name is the smallest
        * possible abbreviation.  As such, no ambiguity is allowed in the
        * name passed as an argument.  There must be only one match that is
        * ever found in the schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create buffers, temp-tables and work-tables that all
        * are interchangeable with table names.  This represents a dynamic
        * table namespace which must appear as a single unified space.
        * <p>
        * If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
        * with no preference for temp-table or persistent table.
        *
        * @param   name
        *          Text string to match, in either of the supported 2 forms.
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The backing record's token type that was found or -1 if no
        *          match was found.
        */
       public int lookupTableRecordType(String name, boolean forceTemp, boolean forcePersistent)
       {
          int type = -1;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<Integer> trt = (SchemaDictionary dict) ->
                {
                   return dict.getTableRecordType(name, forceTemp, forcePersistent);
                };

                Predicate<Integer> notNull =
                   (Integer val) -> { return (val == null || val.intValue() == -1) ; };

                type = processHierarchy(trt, notNull);
             }
          }
          catch (IllegalArgumentException | SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return type;
       }

       /**
        * Searches the schema dictionary for a database matching the passed
        * key.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used
        * to denote a search that found no match, or one that failed due to an
        * ambiguous database name or due to some other problem with the schema
        * dictionary.
        * <p>
        * Schema database names are always unqualified (by '.' characters) and
        * cannot be abbreviated.  All searches are done on a case-insensitive
        * basis.  No ambiguity is allowed in the name passed as an argument.
        * There must be only one match that is ever found in the schema namespace.
        * <p>
        * In addition to the predefined table names from the schema, the user
        * may define or create database name aliases that are interchangeable
        * with database names.  This allows a procedure written to use a specific
        * database name to use (at runtime) a database of a different name that
        * has an identical or at least compatible schema.  This represents a
        * dynamic namespace which must appear as a single unified space.
        * <p>
        * <b>At this time, this method does not support the lookup of aliases
        * since the dictionary does not provide such support.</b>
        * <p>
        * <b>This method is safe to use inside a semantic predicate since it
        * will not throw an exception if the name is not found or if there is
        * ambiguity detected!</b>
        *
        * @param    name
        *           Text string to match.
        *
        * @return   The database's token type that was found or -1 if no match
        *           was found.
        */
       public int lookupDatabase(String name)
       {
          int type = -1;

          // lookup in our local schema dictionary
          if (schemaDict != null && schemaDict.isDatabase(name))
          {
             type = DATABASE;
          }

          return type;
       }

       /**
        * Look up and return the field info instance which describes a database field in a schema,
        * given the field's name and an optional table name-node.
        *
        * @param   table
        *          Name node associated with the field reference. If not {@code null}, only the
        *          private namespace of the given {@code table} is searched (for an exact match)
        *          of {@code fieldName}. If {@code null}, a lenient search (i.e., abbreviations
        *          permitted) is performed across all scopes in the schema dictionary.
        * @param   name
        *          Field's name. May be qualified or not, but must be unambiguous in the current
        *          parsing context.
        *
        * @return  Field's info instance, or <code>null</code> if <code>name</code> cannot be
        *          resolved to a field.
        */
       public FieldInfo lookupFieldInfo(NameNode table, String name)
       {
           return lookupFieldInfo(table, name, false);
       }

       /**
        * Look up and return the field info instance which describes a database field in a schema,
        * given the field's name and an optional table name-node.
        *
        * @param   table
        *          Name node asssociated with the field reference. If not {@code null}, only the
        *          private namespace of the given {@code table} is searched (for an exact match)
        *          of {@code fieldName}. If {@code null}, a lenient search (i.e., abbreviations
        *          permitted) is performed across all scopes in the schema dictionary.
        * @param   name
        *          Field's name. May be qualified or not, but must be unambiguous in the current
        *          parsing context.
        * @param   preferDefaultBuffer
        *          Allow unqualified fields to scope to the default buffer.
        *
        * @return  Field's info instance, or <code>null</code> if <code>name</code> cannot be
        *          resolved to a field.
        */
       public FieldInfo lookupFieldInfo(NameNode table, String name, boolean preferDefaultBuffer)
       {
          FieldInfo finfo = null;

          try
          {
             SchemaHelper<FieldInfo> fget = (SchemaDictionary dict) ->
             {
                return dict.findFieldInfo(table, name, preferDefaultBuffer);
             };

             Predicate<FieldInfo> whileNull = (FieldInfo val) -> { return (val == null); };

             finfo = processHierarchy(fget, whileNull);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return finfo;
       }

       /**
        * Look up and return the table info instance which describes a database table in a schema.
        *
        * @param   name
        *          Text string to match, exactly as found in the original source code
        *          (abbreviated, qualified or unqualified).
        * @param   forceTemp
        *          If <code>true</code>, force a temp-table lookup.
        * @param   forcePersistent
        *          If <code>true</code>, force a persistent table lookup.
        *
        * @return  The table info instance or <code>null</code> if no match was found.
        */
       public TableInfo lookupTableInfo(String name, boolean forceTemp, boolean forcePersistent)
       {
          TableInfo result = null;

          try
          {
             if (schemaDict != null)
             {
                SchemaHelper<TableInfo> info = (SchemaDictionary dict) ->
                {
                   return dict.findTableInfo(name, forceTemp, forcePersistent);
                };

                Predicate<TableInfo> notNull = (TableInfo val) -> { return (val == null); };

                result = processHierarchy(info, notNull);
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.SEVERE, "", exc);
          }

          return result;
       }

       /**
        * Look up and return the AST which represents a database field in a schema, given the field's
        * name.
        *
        * @param   name
        *          Field's name. May be qualified or not, but must be unambiguous in the current
        *          parsing context.
        *
        * @return  Field's schema AST, or <code>null</code> if <code>name</code> cannot be resolved
        *          to a field.
        */
       public Aast lookupFieldAst(String name)
       {
          Aast field = null;

          try
          {
             SchemaHelper<Aast> fget = (SchemaDictionary dict) ->
             {
                Aast fld = null;

                // getField() assumes that the node will be found, so we can
                // only call it if we know the node exists in the current dictionary
                if (dict.isField(name))
                {
                   fld = dict.getField(name);
                }

                return fld;
             };

             Predicate<Aast> notNull = (Aast val) -> { return (val == null); };

             field = processHierarchy(fget, notNull);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return field;
       }

       /**
        * Look up and return the AST which represents a database table in a schema, given the table's
        * name.
        *
        * @param   name
        *          Table's name. May be qualified or not, but must be unambiguous in the current
        *          parsing context.
        *
        * @return  Table's schema AST, or <code>null</code> if <code>name</code> cannot be resolved
        *          to a table.
        */
       public Aast lookupTableAst(String name)
       {
          Aast table = null;

          try
          {
             SchemaHelper<Aast> fget = (SchemaDictionary dict) ->
             {
                Aast tbl = null;

                // getTable() is not safe to call if the table doesn't exist in the
                // current schema
                if (dict.isTable(name))
                {
                   tbl = dict.getTable(name);
                }

                return tbl;
             };

             Predicate<Aast> notNull = (Aast val) -> { return (val == null); };

             table = processHierarchy(fget, notNull);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return table;
       }

       /**
        * Get an iterator on all of the field ASTs defined for a given table. The fields are iterated
        * in the order in which they were added to the schema dictionary.
        *
        * @param   table
        *          Target table AST.
        *
        * @return  An iterator of fields for the specified table. May be empty, but will not be
        *          <code>null</code>.
        */
       public Iterator<Aast> fields(String table)
       {
          Iterator<Aast> iter = null;

          try
          {
             SchemaHelper<Iterator<Aast>> fget = (SchemaDictionary dict) ->
             {
                Aast tableAst = null;

                // getTable() is not safe to call if the table doesn't exist in the
                // current schema
                if (dict.isTable(table))
                {
                   tableAst = dict.getTable(table);
                }

                return (tableAst == null) ? null : dict.fields(tableAst);
             };

             Predicate<Iterator<Aast>> notNull = (Iterator<Aast> val) -> { return (val == null); };

             iter = processHierarchy(fget, notNull);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return (iter == null) ? EmptyIterator.get() : iter;
       }

       /**
        * Allows the addition of a new table of any type (table, buffer,
        * temp-table or work-table) to the schema namespace.  Additions are
        * made either to the global namespace (which is the same as the external
        * procedure scope) or to the current internal procedure or function
        * scope.  This is user controlled by a parameter to this method call.
        * <p>
        * The database to which these are added is <code>null</code> and is
        * picked up by context in the schema dictionary or the table is
        * simply not associated with a specific database (true for temp-tables
        * and work-tables at least).
        * <p>
        * Any of these token types are accepted as the 2nd parameter:
        * <p>
        * <ul>
        *    <li> TABLE (this should never really occur)
        *    <li> BUFFER
        *    <li> TEMP_TABLE
        *    <li> WORK_TABLE
        * </ul>
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table (for the further addition of fields), is returned as a
        * type <code>Object</code>.  Field additions will require this instance
        * reference to be passed as a parameter.  See {@link #addField}.
        *
        * @param    tablename
        *           The name of the table to add.
        * @param    type
        *           The integer token type representing the type of table.
        * @param    global
        *           Specifies if the table should be added to the global scope
        *           or to the current procedure/function scope.
        * @param    node
        *           The node that triggered the table creation.
        * @param    am
        *           Access modifiers if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        * @param    st
        *           Static specifier if not null. Only will be non-null if this is a resource
        *           defined as a member of a class definition.
        *
        * @return   The reference to the added table which is used in subsequent
        *           field adds.  Returns <code>null</code> on error.
        */
       public NameNode addTable(String  tablename,
                                int     type,
                                boolean global,
                                Aast    node,
                                Aast    am,
                                Aast    st)
       {
          if (tablename == null || type < BEGIN_RECORDTYPES || type > END_RECORDTYPES)
          {
             return null;
          }

          ClassDefinition def = getCurrentClassDef();

          // protected temp-tables and buffers are accessible from child classes
          if ((!inMethod || isPreScan()) && def != null && (type == TEMP_TABLE || type == BUFFER))
          {
             int     access   = (am != null) ? am.getType() : -1;
             boolean isStatic = (st != null && st.getType() == KW_STATIC);

             def.addTable(node, tablename, type, access, isStatic, schemaDict, inMethod);
          }

          // make edits in our local schema dictionary
          return schemaDict.addTableEntry(null, tablename, type, global, node);
       }

       /**
        * Allows the addition of a new field of any Progress data type to a
        * specific table in the schema namespace.  Additions are
        * always made to the global namespace, not to a specific scope.
        * <p>
        * Any of the valid field token types are accepted as the 3rd parameter
        * (e.g. <code>ProgressParserTokenTypes.FIELD_INT</code>).
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add the field.
        * @param    field
        *           The name of the field to add.
        * @param    ftype
        *           The parser token type representing the field's data type.
        * @param    node
        *           Node in source file from which this new field creation was
        *           triggered.
        *
        * @return   The <code>Object</code> representing the added field or
        *           <code>null</code> on failure.
        */
       public Object addField(Object table, String field, int ftype, Aast node)
       {
          if (table == null || field == null || ftype < BEGIN_FIELDTYPES || ftype > END_FIELDTYPES)
          {
             return null;
          }

          // make edits in our local schema dictionary
          return (Object) schemaDict.addFieldEntry((NameNode)table,
                                                   node,
                                                   field,
                                                   ftype,
                                                   true);
       }

       /**
        * Allows the addition of a new field of any Progress data type to a
        * specific table in the schema namespace based on reference to an already
        * existing variable or field.  Additions are always made to the global
        * namespace, not to a specific scope.
        * <p>
        * Depending on the token type stored in the 3rd parameter, 2 possible
        * outcomes can occur:
        * <p>
        * <ol>
        *    <li> If the AST represents a variable type, then the field's options
        *         will be set to any non-default values stored in the variable
        *         definition.
        *    <li> If the AST is a field, then the schema dictionary will create
        *         the field with its options set the same as the referenced
        *         field.
        * </ol>
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add the field.
        * @param    field
        *           The name of the field to add.
        * @param    ast
        *           The AST node being used as a reference variable or field.
        *
        * @return   The <code>Aast</code> instance representing the added field or
        *           <code>null</code> on failure.
        */
       public Aast addFieldLike(Object table, String field, Aast ast)
       {
          if (table == null || field == null || ast == null)
             return null;

          int     ftype   = ast.getType();
          int     extent  = getExtentSubscript(ast);
          String  oldname = ast.getText();
          boolean isFld   = ftype > BEGIN_FIELDTYPES && ftype < END_FIELDTYPES;

          if (isFld)
          {
             NameNode node = null;

             // this field should be LIKE another field, call the schema
             // dictionary to add the field.
             try
             {
                // find the source field's name node, even if it is in a different schema dict
                NameNode source = null;

                SchemaHelper<NameNode> ffn =
                   (SchemaDictionary dict) -> { return dict.findFieldNode(oldname); };

                Predicate<NameNode> notNull = (NameNode val) -> { return (val == null); };

                source = processHierarchy(ffn, notNull);

                if (source == null)
                {
                   throw new SchemaException("Field '" + oldname + "' not found");
                }

                // add to our local schema dictionary, using the (possibly remote) source node
                node = schemaDict.addFieldEntry((NameNode) table,
                                                ast,
                                                field,
                                                source,
                                                extent);
             }
             catch (SchemaException exc)
             {
                LOG.log(Level.WARNING,"", exc);
             }

             return node.getAst();
          }

          // OK, we are add a field like a variable, so we add the field and then
          // copy our variable options into it
          int type = -1;

          // the VAR_ type must be converted to the proper FIELD_ type
          switch (ftype)
          {
             case VAR_CHAR:
                type = FIELD_CHAR;
                break;
             case VAR_CLASS:
                type = FIELD_CLASS;
                break;
             case VAR_COM_HANDLE:
                type = FIELD_COM_HANDLE;
                break;
             case VAR_DATE:
                type = FIELD_DATE;
                break;
             case VAR_DATETIME:
                type = FIELD_DATETIME;
                break;
             case VAR_DATETIME_TZ:
                type = FIELD_DATETIME_TZ;
                break;
             case VAR_DEC:
                type = FIELD_DEC;
                break;
             case VAR_HANDLE:
                type = FIELD_HANDLE;
                break;
             case VAR_INT:
                type = FIELD_INT;
                break;
             case VAR_INT64:
                type = FIELD_INT64;
                break;
             case VAR_LOGICAL:
                type = FIELD_LOGICAL;
                break;
             case VAR_RAW:
                type = FIELD_RAW;
                break;
             case VAR_RECID:
                type = FIELD_RECID;
                break;
             case VAR_ROWID:
                type = FIELD_ROWID;
                break;
             default:
                // this MUST be at the end of the list of variables, it will
                // match LONGCHAR and MEMPTR which cannot be created as a field
                if (ftype > BEGIN_VARTYPES && ftype < BEGIN_VARTYPES)
                {
                   String err = String.format("Unexpected variable type %s!",
                                              ast.getDescriptiveTokenText());
                   throw new RuntimeException(err);
                }
                break;
          }

          if (type == -1)
             return null;

          Variable var = (Variable) lookupWrapped(varDict, oldname);
          NameNode node = null;
          try
          {
             // make edits in our local schema dictionary
             node = schemaDict.addFieldEntry((NameNode) table,
                                             ast,
                                             field,
                                             type,
                                             var.getOptions(extent));
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return node.getAst();
       }

       /**
        * Inspect the field definition and write any options into the field
        * object.
        *
        * @param    name
        *           The name of the field which must have its options set.
        * @param    ast
        *           The tree to inspect which defines a Progress field.
        */
       public void setFieldOptions(String name, Aast ast)
       {
          try
          {
             // make edits in our local schema dictionary
             schemaDict.setFieldProperties(name, ast);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Allows the addition of a list of fields (as defined in an existing
        * table) to a specific table in the schema namespace.  Additions are
        * always made to the global namespace, not to a specific scope.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add fields.
        * @param    tablename
        *           The name of the table whose fields should be added.
        *
        * @return   The list of ASTs representing the copied fields.
        */
       public Aast[] addFieldsFrom(Object table, String tablename)
       {
          return addFieldsFrom(table, tablename, false, false, true);
       }

       /**
        * Allows the addition of a list of fields (as defined in an existing
        * table) to a specific table in the schema namespace.  Additions are
        * always made to the global namespace, not to a specific scope.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add fields.
        * @param    tablename
        *           The name of the table whose fields should be added.
        * @param    skipGlobal
        *           if (@code true}, do not look in the global schema scope to find
        *           {@code tablename}.
        * @param    sequential
        *           <code>true</code> to add fields based on the <code>ORDER</code> schema property
        *           which is the equivalent of <code>LIKE-SEQUENTIAL</code>. <code>false</code> to
        *           add fields based on the <code>POSITION</code> schema property, which is the same
        *           as <code>LIKE</code> (this is the original way that the 4GL did it).
        * @param    fail
        *           Flag indicating if an exception should be thrown if the source table is not
        *           found.
        *
        * @return   The list of ASTs representing the copied fields.
        */
       public Aast[] addFieldsFrom(Object  table,
                                   String  tablename,
                                   boolean skipGlobal,
                                   boolean sequential,
                                   boolean fail)
       {
          List<Aast> fields = null;
          try
          {
             NameNode toTable = (NameNode) table;

             // find the source field's name node, even if it is in a different schema dict
             SchemaHelper<NameNode> ftfn = (SchemaDictionary dict) ->
             {
                NameNode nn = null;

                // findTableFromNode() is not safe to call if the table doesn't exist in the
                // current schema
                if (dict.isTable(tablename))
                {
                   nn = dict.findTableFromNode(tablename, toTable, skipGlobal);
                }

                return nn;
             };

             Predicate<NameNode> notNull = (NameNode val) -> { return (val == null); };

             NameNode fromTable = processHierarchy(ftfn, notNull);

             if (fromTable == null)
             {
                if (fail)
                {
                   throw new SchemaException("'From' table not found: " + tablename);
                }
                else
                {
                   return null;
                }
             }

             // make edits in our local schema dictionary
             // TODO: add support for proper ordering based on the sequential flag, it is likely that
             //       the existing approach is wrong as well (that it doesn't follow the POSITION
             //       attr)
             schemaDict.addFieldEntries(tablename, fromTable, toTable, skipGlobal);

             fields = new ArrayList<Aast>();
             Aast child = (Aast) toTable.getAst().getFirstChild();
             while (child != null)
             {
                if (child.getType() > BEGIN_FIELDTYPES && child.getType() < END_FIELDTYPES)
                {
                   fields.add(child);
                }

                child = (Aast) child.getNextSibling();
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }

          return fields == null ? null : fields.toArray(new Aast[0]);
       }

       /**
        * Adds an index from the original source table into the temp-table that
        * is passed in.  Additions are always made to the global namespace, not
        * to a specific scope.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add fields.
        * @param    node
        *           Node in source file from which this new field creation was
        *           triggered.
        * @param    name
        *           The name of the index to add.
        * @param    primary
        *           Whether or not the <code>PRIMARY</code> keyword was present.
        */
       public void addIndexFrom(Object  table,
                                Aast    node,
                                String  name,
                                boolean primary)
       {
          if (table == null || name == null)
          {
             // nothing to do
             return;
          }

          // add "use index" processing to schema dictionary
          try
          {
             // make edits in our local schema dictionary uses (possibly remote) state that has
             // been previously setup
             schemaDict.copyIndex((NameNode) table, node, name, primary);
          }
          catch (SchemaException exc)
          {
             // don't abend here, the 4GL allows garbage here, just warn the user
             System.out.printf("WARNING: USE-INDEX processing failed in DEFINE TEMP-TABLE LIKE, " +
                               "this is not fatal because the 4GL allows garbage to be specified" +
                               " (%s).\n",
                               exc.getMessage());
          }
       }

       /**
        * Adds an index as defined in the source code into the temp-table that
        * is passed in.  Additions are always made to the global namespace, not
        * to a specific scope.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference in which to add fields.
        * @param    name
        *           The name of the index to add.
        * @param    ast
        *           The AST storing the nodes that define the index.
        */
       public void addIndex(Object table, String name, Aast ast)
       {
          if (table == null || name == null || ast == null)
          {
             // nothing to do
             return;
          }

          try
          {
             // make edits in our local schema dictionary
             // add a new index for the given table by the given name and
             // set the options based on our AST
             schemaDict.addIndex((NameNode) table, name, ast);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Sets specific conditions on a temp-table definition based on
        * encountering certain tokens in the source code.  The following tokens
        * are known: <code>NO-UNDO, VALIDATE, RCODE-INFORMATION</code>.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference which is completed.
        * @param    token
        *           The type of the option that was encountered in the source
        *           code.
        * @param    node
        *           The node that triggered the table creation.
        */
       public void setTableOption(Object table, int token, Aast node)
       {
          // access our local schema dictionary
          Aast tableAst = schemaDict.getTable((NameNode) table);

          try
          {
             Aast prop = new ProgressAst();
             prop.setType(token);
             prop.setLine(node.getLine());
             prop.setColumn(node.getColumn());

             switch (token)
             {
                case KW_NO_UNDO:
                {
                   // set NO-UNDO option for this table
                   prop.setText("no-undo");
                   break;
                }

                case KW_VALIDATE:
                   // set VALIDATE option for this table
                   prop.setText("validate");
                   break;

                case KW_RCOD_INF:
                {
                   // set RCODE-INFORMATION option for this table
                   prop.setText("rcode-information");
                   break;
                }

                case KW_LIKE_SEQ:
                {
                   // set LIKE-SEQUENTIAL option for this table
                   prop.setText("like-sequential");
                   break;
                }
             }

             // make edits in our local schema dictionary
             schemaDict.setTableProperty(tableAst, prop);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Notifies the schema dictionary that the end of a temp-table definition
        * has been reached. This allows certain default processing to be executed
        * as needed.
        * <p>
        * In order to hide the schema dictionary interfaces from the caller,
        * the <code>NameNode</code> object that can be used to reference
        * the table is required.  This is an <code>Object</code> returned from a
        * previous call to {@link #addTable}.  This rule casts that to the
        * proper type before calling the dictionary.
        *
        * @param    table
        *           The table reference which is completed.
        * @param    ast
        *           The created table AST.
        */
       public void finalizeTableDefine(Object table, Aast ast)
       {
          try
          {
             // notify the local schema dictionary of the "end of temp-table definition"
             schemaDict.finalizeTableDefine((NameNode) table, ast);
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Adds a record scope to the schema namespace to the top of the stack
        * of scopes.  Any tables that are promoted (see {@link #promoteTable})
        * within this scope are given precedence in making their field names
        * unambiguous when compared to fields of the same name in tables that
        * are in scopes that are lower on the stack.
        *
        * @param    mark
        *           <code>true</code> to mark the associated scope as one which
        *           is an internal procedure, user-defined function or trigger.
        *           <code>false</code> for any other case including external
        *           procedures and inner blocks (<code>DO, REPEAT or FOR</code>).
        */
       public void addSchemaScope(boolean mark)
       {
          if (schemaDict != null)
          {
             schemaDict.addScope(mark);
          }
       }

       /**
        * Deletes the current record scope in the schema namespace (the top of
        * the stack of scopes).  Any tables that were promoted (see
        * {@link #promoteTable}) into this scope are naturally popped off the
        * stack by this action if <code>propagate</code> is <code>false</code>.
        * Otherwise the namespace updates made in this scope will be copied to
        * the namespaces of the enclosing scope.
        *
        * @param    propagate
        *           <code>true</code> to force namespace propagation to the
        *           enclosing scope.
        */
       public void deleteSchemaScope(boolean propagate)
       {
          try
          {
             if (schemaDict != null)
             {
                schemaDict.removeScope(propagate);
             }
          }

          catch (SchemaException exp)
          {
             // ignore it, we don't care!
          }
       }

       /**
        * Globally enable or disable schema table name promotion.
        *
        * @param    allow
        *           <code>true</code> to enable, <code>false</code> to disable.
        */
       public void allowPromotion(boolean allow)
       {
          this.allow = allow;
       }

       /**
        * Promotes a table (and all its fields) into the current top scope of
        * the schema dictionary's stack of scopes.  Any tables that are promoted
        * within this scope are given precedence in making their field names
        * unambiguous when compared to fields of the same name in tables that
        * are in scopes that are lower on the stack.
        * <p>
        * Scopes are created using {@link #addSchemaScope} and are deleted using
        * {@link #deleteSchemaScope}.
        *
        * @param    table
        *           The name of the table (optionally qualified) to promote.
        * @param    force
        *           Override the default promotion behavior and force a refresh
        *           of the promoted table even if it was already promoted
        *           previously.
        * @param    noProp
        *           <code>true</code> to force any promoted nodes to have their
        *           no propagate flag set. This disables propagation of these
        *           nodes across the current scope boundary.
        */
       public void promoteTableName(String table, boolean force, boolean noProp)
       {
          promoteTableName(table, force, noProp, true);
       }

       /**
        * Promotes a table (and all its fields) into the current top scope of
        * the schema dictionary's stack of scopes.  Any tables that are promoted
        * within this scope are given precedence in making their field names
        * unambiguous when compared to fields of the same name in tables that
        * are in scopes that are lower on the stack.
        * <p>
        * Scopes are created using {@link #addSchemaScope} and are deleted using
        * {@link #deleteSchemaScope}.
        *
        * @param    table
        *           The name of the table (optionally qualified) to promote.
        * @param    force
        *           Override the default promotion behavior and force a refresh
        *           of the promoted table even if it was already promoted
        *           previously.
        * @param    noProp
        *           <code>true</code> to force any promoted nodes to have their
        *           no propagate flag set. This disables propagation of these
        *           nodes across the current scope boundary.
        * @param    self
        *           <code>true</code> to promote the node directly associated with
        *           <code>name</code>; <code>false</code> to promote only its
        *           descendants.
        */
       public void promoteTableName(String table, boolean force, boolean noProp, boolean self)
       {
          try
          {
             if (allow)
             {
                // we only promote if the name is in the local schema, there is no
                // scoping or need to scope the schema dictionaries in the class
                // hierarchy, because the temp-tables are already at the global
                // scope in those dictionaries
                if (schemaDict.isTable(table))
                {
                   schemaDict.promoteTable(table, force, noProp, self);
                }
             }
          }
          catch (SchemaException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Promote all tables in the specified schema, to the current scope.
        *
        * @param    schemaname
        *           The schema.
        *
        * @see      SchemaDictionary#promoteTable(String)
        */
       public void promoteAllTables(String schemaname)
       {
          String schema = schemaname.substring(0, schemaname.indexOf('.'));
          Iterator<Aast> iter = schemaDict.tables(schema);
          while (iter.hasNext())
          {
             Aast table = iter.next();

             try
             {
                schemaDict.promoteTable(schema + '.' + table.getText());
             }
             catch (SchemaException e)
             {
                LOG.log(Level.WARNING,"", e);
             }
          }
       }

       /**
        * Promotes a table (and all its fields) into the current top scope of
        * the schema dictionary's stack of scopes.  Any tables that are promoted
        * within this scope are given precedence in making their field names
        * unambiguous when compared to fields of the same name in tables that
        * are in scopes that are lower on the stack.
        * <p>
        * Scopes are created using {@link #addSchemaScope} and are deleted using
        * {@link #deleteSchemaScope}.
        *
        * @param    table
        *           The <code>Object</code> reference representing a table
        *           that had been previously added to the schema dictionary.
        * @param    force
        *           Override the default promotion behavior and force a refresh
        *           of the promoted table even if it was already promoted
        *           previously.
        * @param    noProp
        *           <code>true</code> to force any promoted nodes to have their
        *           no propagate flag set. This disables propagation of these
        *           nodes across the current scope boundary.
        * @param    self
        *           <code>true</code> to promote the node directly associated with
        *           <code>name</code>; <code>false</code> to promote only its
        *           descendants.
        */
       public void promoteTable(Object table, boolean force, boolean noProp, boolean self)
       {
          promoteTableName(((NameNode) table).getName(), force, noProp, self);
       }

       /**
        * Post-process the given field, in terms of honoring the VALIDATE clause, if present. This
        * will:
        * <ul>
        *    <li>
        *       If the VALIDATE clause is not present, it will remove the VALEXP and VALMSG children
        *    </li>
        *    <li>
        *       If the VALIDATE clause is present, it will remove all extent and LBRACKET information
        *       from the VALEXP nodes. Also, any reference of the source field in the VALEXP will
        *       be rewritten to reference the new field.
        *    </li>
        * </ul>
        *
        * @param    field
        *           The field which needs to be processed. This is a field defined using a
        *           <code>LIKE table.sourceField</code> clause, with or without the VALIDATE clause.
        * @param    hasValidate
        *           When <code>true</code>, it indicates the VALIDATE clause was present and the
        *           VALEXP node will need to be processed.
        * @param    sourceSchemaName
        *           The schema name of the source field
        */
       public void processLikeField(Aast field, boolean hasValidate, String sourceSchemaName)
       {
          try
          {
             if (schemaDict != null)
             {
                // make edits in our local schema dictionary
                schemaDict.processLikeField(field, hasValidate, sourceSchemaName);
             }
          }
          catch (AmbiguousSchemaNameException exc)
          {
             LOG.log(Level.WARNING,"", exc);
          }
       }

       /**
        * Prints a report to <code>stderr</code> with the listing of each
        * dictionary.  One record per dictionary entry will be printed and
        * there will be a section for each namespace type.
        */
       public void dump()
       {
          StringOutputStream outputStream = new StringOutputStream();
          PrintStream ps = new PrintStream(outputStream);

          ps.println("\n-------------Keyword Listing-------------");
          kwDict.dump(ps);

          ps.println("\n-------------Variable Listing-------------");
          varDict.dump(ps);

          ps.println("\n-------------DATASET  Listing-------------");
          dsetDict.dump(ps);

          ps.println("\n------------DS-SOURCE Listing-------------");
          dsrcDict.dump(ps);

          ps.println("\n-------------Function Listing-------------");
          funcDict.dump(ps);

          ps.println("\n-------------Procedure Listing-------------");
          procDict.dump(ps);

          ps.println("\n---------------Label Listing---------------");
          labelDict.dump(ps);

          ps.println("\n--------------Widget Listing--------------");
          widgetDict.dump(ps);

          ps.println("\n---------------Frame Listing---------------");
          frameDict.dump(ps);

          ps.println("\n-----------Field Widget Listing-----------");
          fieldWidgetDict.dump(ps);

          ps.println("\n----------- Frame USE-DICT-EXPS Listing-----------");
          useDictExps.dump(ps);

          ps.println("\n---------------Menu Listing---------------");
          menuDict.dump(ps);

          ps.println("\n-------------Menu Item Listing-------------");
          menuItemDict.dump(ps);

          ps.println("\n--------------Stream Listing--------------");
          streamDict.dump(ps);

          LOG.log(Level.INFO, outputStream.getData());
       }

       /**
        * Prints a report to <code>schema.dmp</code> with the listing of all
        * current contents of the Schema Dictionary.
        */
       public void dumpSchema()
       throws IOException,
              FileNotFoundException
       {
          FileOutputStream fout = null;
          PrintStream      ps   = null;
          try
          {
             if (schemaDict != null)
             {
                File file = new File("schema.dmp");
                fout = new FileOutputStream(file);
                ps = new PrintStream(fout);
                schemaDict.dump(ps);
                System.out.println(file.getAbsolutePath() + " dump file created");
             }
          }
          finally
          {
             if (ps != null)
             {
                ps.close();
             }
             else if (fout != null)
             {
                fout.close();
             }
          }
       }

       /**
        * Prints a report of the given class' .NET dependencies to the file
        * <code>dotnet_dependencies.txt</code>.
        *
        * @param    cls
        *           The class for which to dump the dependencies.
        */
       public void dumpDotNetDependencies(ClassDefinition cls)
       {
          FileOutputStream fout = null;
          PrintStream      ps   = null;
          try
          {
             if (cls != null && cls.isIndirectDotNetReference())
             {
                File file = new File("dotnet_dependencies.txt");

                // open in append mode
                fout = new FileOutputStream(file, true);

                // auto-flush
                ps = new PrintStream(fout, true);

                String type = cls.isClass() ? "CLASS" : (cls.isInterface() ? "INTERFACE" : "ENUM");
                ps.printf("%s %s\n---\n", type, cls.getName());

                if (cls.isDerivedFromDotNet())
                {
                   for (String cname : cls.getDerivedDotNet())
                   {
                      ps.printf("DERIVED_FROM %s\n", cname);
                   }
                }
                if (cls.hasDirectDotNet())
                {
                   for (String cname : cls.getDirectDotNet())
                   {
                      ps.printf("DIRECT_REF %s\n", cname);
                   }
                }
                if (cls.hasIndirectDotNet())
                {
                   for (String cname : cls.getIndirectDotNet())
                   {
                      ps.printf("INDIRECT_REF %s\n", cname);
                   }
                }

                ps.printf("\n");
             }
          }
          catch (IOException e)
          {
             LOG.log(Level.SEVERE, "Failure in .NET dependencies output generation.", e);
          }
          catch (Throwable t)
          {
             LOG.log(Level.SEVERE, "Non-I/O Failure in .NET dependencies output generation.", t);
          }
          finally
          {
             try
             {
                if (ps != null)
                {
                   ps.close();
                }
                else if (fout != null)
                {
                   fout.close();
                }
             }
             catch (IOException ioe)
             {
                // ignore
             }
          }
       }

       /**
        * Returns the token type from the passed wrapped object or -1 for an
        * is invalid {@link TokenDataWrapper} object.
        *
        * @param   wrapped
        *          The object from which to obtain the token type.
        *
        * @return  The token type that was found or -1 for an invalid wrapper
        *          object.
        */
       public int lookupWrappedType(TokenDataWrapper wrapped)
       {
          return wrapped != null ? wrapped.getTokenType() : -1;
       }

       /**
        * Set the {@link SchemaDictionary#markPreferred} flag, so all info for the tables which are
        * weak references will be collected in the preferred nodes for the current scope.
        *
        * @param    preferred
        *           The new value of the flag.
        *
        * @return   The old value of the {@link SchemaDictionary#markPreferred} field.
        */
       public boolean markPreferred(boolean preferred)
       {
          boolean old = false;

          if (schemaDict != null)
          {
             // make edits in our local schema dictionary
             old = schemaDict.markPreferred(preferred);
          }

          return old;
       }

       /**
        * Set the {@link SchemaDictionary#markStrongScope} flag, so all info for the tables which are
        * strong references will be stored for special disambiguation in the current scope.
        *
        * @param    strong
        *           The new value of the flag.
        */
       public void markStrongScope(boolean strong)
       {
          if (schemaDict != null)
          {
             // make edits in our local schema dictionary
             schemaDict.markStrongScope(strong);
          }
       }

       /**
        * Mark the state that lookups are done in a chain.
        *
        * @param    chain
        *           Flag indicating if we are in a chain reference.
        */
       public void setChainedReference(boolean chain)
       {
          this.chainedReference = chain;
       }

       /**
        * Get the state of the {@link #chainedReference} flag.
        *
        * @return   See above.
        */
       public boolean isChainedReference()
       {
          return chainedReference;
       }

       /**
        * Check if we are running in a FWD server runtime.
        *
        * @return    The {@link Configuration#isRuntimeConfig()} value.
        */
       protected boolean isRuntimeConfig()
       {
          return Configuration.isRuntimeConfig();
       }

       /**
        * Load the given class and create a class definition.
        *
        * @param    name
        *           The fully qualified Java class name to load.
        *
        * @return   The class definition wrapping the Java class.
        */
       private ClassDefinition loadJavaClass(String name)
       {
          ClassDefinition cls = null;

          try
          {
             cls = new JavaClassDefinition(Class.forName(name));

             classDict.addSymbol(false, name, cls);
          }

          catch (Throwable thr)
          {
             // most likely this was a ClassNotFoundException but there also could have been a
             // LinkageError or ExceptionInInitializerError (both are abnormal ends)

             // ignore, let things return null below

             // Only use this for debugging.  This method can be called in a "try" mode where a failure
             // must not be fatal and must not be verbose.
             LOG.log(Level.FINEST, "Could not resolve java class: " + name, thr);
          }

          return cls;
       }

       /**
        * Check if the given name is a valid Java class.
        *
        * @param    name
        *           The fully qualified Java class name to check.
        * @param    result
        *           Object in which to store the search results (the class
        *           name, the built-in flag and the .NET flag) when the search
        *           is successful. If nothing is found, this object is unchanged.
        *
        * @return   {@code true} if the class is a valid Java class.
        */
       private boolean isJavaClass(String name, SearchResult result)
       {
          Class<?> cls = null;

          try
          {
             cls = Class.forName(name);

             // if we are here, the Java class exists
             result.clsname = name;
             result.builtin = false;
             result.dotnet  = false;
             result.java    = true;
             result.type    = UsingType.JAVA;
          }
          catch (LinkageError exc)
          {
             // log linkage errors
             LOG.log(Level.SEVERE, "Could not load Java class " + name, exc);
          }
          catch (Throwable thr)
          {
             // most likely this was a ClassNotFoundException but there also could have been a
             // LinkageError or ExceptionInInitializerError (both are abnormal ends)

             // ignore, let things detect null below
          }

          return (cls != null);
       }

       /**
        * Ensure the entire class hierarcy (from super-classes and implementing interfaces) is parsed,
        * so that the provisional members are resolved.
        *
        * @param    clsName
        *           The qualified legacy name.
        * @param    wa
        *           The work area.
        *
        * @throws   NullPointerException
        *           If a parent class can not be found.
        */
       private void parseHierarchy(String clsName, WorkArea wa)
       throws NullPointerException
       {
          ClassDefinition cdef = getClassDefinition(clsName);

          if (cdef == null)
          {
             throw new NullPointerException("Can not find class " + clsName);
          }

          if (cdef.isProcessed())
          {
             return;
          }

          if (cdef.getInterfaces() != null)
          {
             for (String iface : cdef.getInterfaces())
             {
                parseHierarchy(iface, wa);
             }
          }

          ClassDefinition[] parents = cdef.getParents();

          if (parents != null)
          {
             for (ClassDefinition parent : parents)
             {
                parseHierarchy(parent.getName(), wa);
             }
          }

          cdef.setProcessed(true);

          String file = cdef.getFilename();

          if (!cdef.isBuiltIn() && new File(file).exists())
          {
             generator.processFile(file, null, -1);

             // at this point we have been fully parsed and all parents have been fully parsed, so
             // it is safe to detect whether or not one of our ancestors is a .NET class
             cdef.derivedFromDotNet();

             // write the dependencies into a log file
             dumpDotNetDependencies(cdef);
          }
       }

       /**
        * Searches a definition in the local (or upper) scopes.
        *
        * @param    name
        *           The member to search for.
        *
        * @return   The found variable member or <code>null</code> otherwise.
        */
       private Variable lookupLocalDef(String name)
       {
          return (Variable) lookupWrapped(name, accumDict, varDict, widgetDict, menuDict, menuItemDict);
       }

       /**
        * Searches the named class' hierarchy for the named data member.
        *
        * @param    cname
        *           Class name to search within or <code>null</code> if this is
        *           a member within the local class definition.
        * @param    name
        *           Member name to lookup.
        * @param    isStatic
        *           If <code>true</code>, only static methods will be looked up.
        *           Otherwise any method can be returned.
        *
        * @return   The token type of the member or -1 if no match was found.
        */
       private int lookupDataMemberWorker(String cname, String name, boolean isStatic)
       {
          ClassDefinition cls = (cname == null) ? getCurrentClassDef() : lookupClass(cname);

          if (cls == null)
          {
             return -1;
          }

          int access = calcAccessMode(cname);

          Variable clsVar = cls.lookupVariableWrapper(name, access, isStatic, chainedReference);
          if (clsVar == null)
          {
             return -1;
          }

          // ensure we do not have a local var scoped to the method, with the same name
          if (inMethod && !classLookup)
          {
             Variable localVisibleVar = (Variable) varDict.lookupSymbol(name);

             if (localVisibleVar != null && localVisibleVar != clsVar)
             {
                return -1;
             }
          }

          return clsVar.getTokenType();
       }

       /**
        * Determine the parent class based on the optional inherits clause and then load or
        * return the existing class definition(s).
        *
        * @param    name
        *           Text defining the fully qualified (including package) child class name.
        * @param    inherits
        *           Node defining one or more fully qualified superclass names or <code>null</code>
        *           for none.
        * @param    dotNet
        *           Flag indicating this is a .NET class name.
        *
        * @return   The parent class definitions or <code>null</code> if Progress.Lang.Object is
        *           the child class.  Only interfaces can have multiple inheritance.
        */
       private ClassDefinition[] calculateParents(String name, Aast inherits, boolean dotNet)
       {
          WorkArea wa = locate();

          ClassDefinition[] parents = null;
          ClassDefinition   parent  = null;

          if (inherits != null)
          {
             int num = inherits.getNumImmediateChildren();

             parents = new ClassDefinition[num];

             for (int i = 0; i < num; i++)
             {
                Aast   child = inherits.getChildAt(i);
                String pname = (String) child.getAnnotation("qualified");
                if (pname == null)
                {
                   String err = String.format("Missing annotation (qualified) in inherits node\n%s", child.dumpTree(true));
                   throw new RuntimeException(err);
                }
                parent = lookupClass(pname);

                if (parent == null)
                {
                   // will cause recursion if successful
                   loadClass(pname);
                   parent = lookupClass(pname);
                }

                if (parent == null)
                {
                   if (wa.preScanRefs.contains(pname))
                   {
                      String err = String.format("Parent class already in pre-scan - postponed: %s!", pname);
                      throw new ParentUnavailableException(err);
                   }

                   String err = String.format("Missing parent class definition for %s!", pname);
                   throw new RuntimeException(err);
                }

                parents[i] = parent;
             }
          }
          else if (dotNet)
          {
             if (!ROOT_DOTNET_OBJ_NAME.equals(name))
             {
                // default parent for all classes
                loadClass(ROOT_DOTNET_OBJ_NAME);
                parent = lookupClass(ROOT_DOTNET_OBJ_NAME);

                if (parent != null)
                   parents = new ClassDefinition[] { parent };
             }
          }
          else if (!ROOT_OBJ_NAME.equals(name))
          {
             // default parent for all classes
             loadClass(ROOT_OBJ_NAME);
             parent = lookupClass(ROOT_OBJ_NAME);

             if (parent != null)
                parents = new ClassDefinition[] { parent };
          }

          return parents;
       }

       /**
        * Initialize the maps of all possible class and interface definitions that
        * exist in the project. This includes all <code>.cls</code> files which can
        * be found via the <code>PROPATH</code> as well as all Progress built-ins
        * and all .NET classes/interfaces that can be accessed.
        * <p>
        * These files will be listed from the directories represented by the
        * {@link #propath}, {@link #OO4GL_PATH}, {@link #DOTNET_PATH} and the
        * {@link #ASSEMBLY_PATH}.
        * <p>
        * If file-system case-sensitivity is <code>false</code> then all map keys
        * will be lowercased.
        *
        * @param    propathCls
        *           The map to store the classes.
        * @param    propath
        *           The propath to scan for initialization. The propath can change
        *           with each instantiation of this class.  If it changes then
        *           the propath class mappings must be reinitialized. This may be
        *           <code>null</code> in which case the map will be empty.
        * @param    caseSens
        *           <code>true</code> if the legacy system had a case-sensitive
        *           file-system.
        */
       private static synchronized ScopedSymbolDictionary initPossibleClasses(Map<String, List<String>> propathCls,
                                                                              String[]                  propath,
                                                                              boolean                   caseSens)
       {
          String propathKey = "";

          WorkArea wa = locate();

          if (propath != null)
          {
             boolean curDir = false;

             for (int i = 0; i < propath.length; i++)
             {
                propathKey = propathKey + (propath[i] == null ? "<null>" : propath[i]) + "$$$";

                if (propath[i] != null)
                {
                   String srch = propath[i];

                   if (srch.length() == 0 || srch.equals("."))
                   {
                      // only search the current directory once
                      if (curDir)
                         continue;

                      srch   = ".";
                      curDir = true;
                   }

                   createClassMappings(propathCls,
                                       getClassFileList(srch, false),
                                       srch,
                                       false);
                }
             }
          }

          // all classes are loaded globally - the current appraoch of keeping (propath, classdict)
          // is incorrect, we need to keep a (classQName, (file, ClassDefinition)) mapping, and
          // resolve the correct definition based on the current PROPATH
          propathKey = "";
          // TODO: solve this
          ScopedSymbolDictionary classDict = wa.propathClassDicts.get(propathKey);
          if (classDict == null)
          {
             classDict = new ScopedSymbolDictionary<>(null, false);
             wa.propathClassDicts.put(propathKey, classDict);
          }

          // add the built-ins, .net and custom fakeout classes/interfaces; this
          // only happens the first time this method is called
          // TODO: this assumes the caseSens value is the same project-wide AND
          //       that the correct value is passed in the first time the
          //       class is instantiated (which can be by the SchemaLoader)
          if (wa.oo4glCls == null)
          {
             // 4GL classes are matched case-insensitively (even on case-sensitive file systems)
             wa.oo4glCls = new HashMap<String, List<String>>();
             createClassMappings(wa.oo4glCls,
                                 getClassFileList(oo4glPath, false),
                                 oo4glPath,
                                 false);
          }
          if (wa.dotnetCls == null)
          {
             wa.dotnetCls = new HashMap<String, List<String>>();
             createClassMappings(wa.dotnetCls,
                                 getClassFileList(dotnetPath, caseSens),
                                 dotnetPath,
                                 caseSens);
          }
          if (wa.assemblyCls == null)
          {
             wa.assemblyCls = new HashMap<String, List<String>>();
             createClassMappings(wa.assemblyCls,
                                 getClassFileList(ASSEMBLY_PATH, caseSens),
                                 ASSEMBLY_PATH,
                                 caseSens);
          }

          return classDict;
       }

       /**
        * List the 4GL class/interface files in a given path.
        *
        * @param    path
        *           The directory to search.
        *
        * @return   The list of files that exist.  If there are no files, the
        *           array will have a 0 length.
        */
       private static ArrayList<File> getClassFiles(String path)
       {
          ArrayList<File> results = new ArrayList<File>();

          scan(new File(path), results);

          return results;
       }

       /**
        * Core implementation of the listing algorithm which uses the
        * <code>File</code> class to make a complete list of the directory
        * contents, then this list is used to build a filtered list of just
        * those files that match the user-defined regular expression. Note
        * that if recursion was previously requested (on construction) by the
        * caller, this method will recursively call itself for every directory
        * it encounters in the list.  Only files will be returned in the results
        * list and all results are added to the results list that is passed as
        * a parameter.
        *
        * @param    dir
        *           The directory in which to list all files.
        * @param    results
        *           The list to which all matching files must be appended.
        */
       private static void scan(File dir, ArrayList<File> results)
       {
          // we don't use a FilenameFilter here because this would filter out
          // all directories that don't match the regular expression, but this
          // would not be correct, we need to see all directories and we will
          // manually filter the filenames down ourselves
          File[] rawResults  = dir.listFiles();

          // separate file and directory results
          for (int i = 0; i < rawResults.length; i++)
          {
             if (rawResults[i].isDirectory())
             {
                // recursively call ourselves to process this subdirectory
                scan(rawResults[i], results);
             }
             else
             {
                // only accept matches that meet the user's criteria (regular
                // expression)
                if (rawResults[i].getName().endsWith(CLASS_EXT))
                {
                   // add the file to the results list
                   results.add(rawResults[i]);
                }
             }
          }
       }

       /**
        * Attempt to match the {@link #sortFields} as a prefix for the {@link #indexes} in the
        * {@link #indexTable}.
        *
        * @param    exact
        *           Flag indicating if prefix index match is allowed (when <code>false</code>);
        *           otherwise, the index must be matched fully.
        *
        * @return   <code>true</code> if a match was possible.
        *
        * @throws   SchemaException
        *           If we are not able to match partially an index prefix, and there are
        *           inconsistencies between unqualified field names (they can be resolved to multiple
        *           or another table).
        */
       private boolean indexPrefixMatch(boolean exact)
       throws SchemaException
       {
          int foundIndex = -1;
          // walk each index and check if it matches as a prefix for the collected fields
          Iterator<Aast> iter = indexes.keySet().iterator();
          while (iter.hasNext())
          {
             Aast next = iter.next();
             List<Aast> fields = indexes.get(next);

             if (exact && fields.size() > sortFields.size())
             {
                // can't match as prefix
                continue;
             }

             boolean match = true;
             int idx = 0;
             for (Aast idxField : fields)
             {
                Aast sortField = sortFields.get(idx);
                boolean idxAsc = !idxField.downPath(KW_DESCEND);
                boolean sortAsc = sortField.getNextSibling() == null ||
                                  sortField.getNextSibling().getType() != KW_DESCEND;

                if (idxAsc != sortAsc)
                {
                   match = false;
                   break;
                }

                if (sortField.getType() == EXPRESSION)
                {
                   sortField = (Aast) sortField.getFirstChild();
                }

                String sortName = (String) sortField.getAnnotation("name");

                if (sortName.indexOf(".") == -1)
                {
                   sortName = String.format("%s.%s", indexTable, sortName);
                }
                Aast fieldAst = lookupFieldAst(sortName);

                if (fieldAst == null || !fieldAst.getText().equalsIgnoreCase(idxField.getText()))
                {
                   match = false;
                   break;
                }

                idx = idx + 1;

                if (idx >= sortFields.size())
                {
                   break;
                }
             }

             if (match)
             {
                foundIndex = idx;
                break;
             }
          }

          if (!exact && foundIndex < 0)
          {
             // everything between 0..idx is revalidated
             for (int i = 0; i <= Math.min(sortFields.size() - 1, foundIndex); i++)
             {
                Aast sortField = sortFields.get(i);
                if (sortField.getType() == EXPRESSION)
                {
                   sortField = (Aast) sortField.getFirstChild();
                }
                String sortName = (String) sortField.getAnnotation("name");
                if (sortName.indexOf(".") != -1)
                {
                   continue;
                }

                String qsortName = (String) sortField.getAnnotation("schemaname");

                FieldInfo finfo = lookupFieldInfo(null, sortName);
                if (finfo == null || !finfo.getQualified().equals(qsortName))
                {
                   // something is wrong...
                   throw new SchemaException(String.format("Inconsistent index field found: %s %s %s!",
                                                           qsortName,
                                                           finfo == null ? "n/a" : finfo.getQualified(),
                                                           sortField.dumpTree(true)));
                }
             }
          }

          return foundIndex >= 0;
       }

       /**
        * Multi-level lookup routine for processing searches on variable and
        * function dictionaries based on
        * the {@link com.goldencode.p2j.util.ScopedSymbolDictionary} class. All
        * searches resolve to a symbol that exactly matches the passed key or to
        * a built-in function or variable that is represented by a language
        * keyword.  The token type of the match is returned.  Note that a token
        * type of -1 is invalid in the Lexer and Parser, so this value is used to
        * denote a search that found no match.  Abbreviation processing is
        * handled for built-ins ONLY and is naturally done by usage of the
        * keyword dictionary in the first phase of the lookup process.  The
        * second lookup phase uses any returned keyword to modify the search or
        * if no keyword is found, then a standard exact search is executed.
        * <p>
        * The algorithm checks for a keyword match.  If not, a simple actual text
        * lookup is done based on the exact text found in the source. If there is
        * a keyword match, then if the keyword is reserved, the keyword's full
        * text is used in the name lookup.  The result would match the preloaded
        * built-in name.  If the keyword match is with an unreserved keyword,
        * then the exact text from the source is used for a first lookup.  If this
        * does not succeed, then a second lookup using the full keyword text is
        * used.  This approach allows unreserved keywords to be used as user-
        * defined names that hide built-in names if they exist.  However the
        * built-in names are still found if there is no user-defined name.
        * <p>
        * This usage of the full keyword text for lookup of built-ins is required
        * since only the full text definition exists in the variable dictionary,
        * rather than the full set of abbreviations.  Instead the abbreviation
        * matching is handled by the keyword dictionary and this method is aware
        * of the difference.
        *
        * @param   dict
        *          <code>ScopedSymbolDictionary</code> instance to search.
        * @param   actual
        *          Text string to match.
        * @return  The label's token type that was found or -1 if no match was
        *          found.
        * @see     #lookupVariable
        * @see     #lookupFunction
        */
       private int lookupWorker(ScopedSymbolDictionary dict, String actual)
       {
          Keyword kw    = lookupKeyword(actual);
          int     type  = -1;

          if (kw != null)
          {
             // the actual text matches a keyword
             if (kw.isReserved())
             {
                // since this is a reserved keyword, this must be a
                // built-in variable/function
                type = lookupWorkerExact(dict, kw.getFullText());
             }
             else
             {
                // this is an unreserved keyword, this MAY be a
                // built-in variable/function BUT we must allow a user-defined
                // name to take precedence (note that if the actual text is
                // is the same as the full keyword text, then we may still
                // get a match here that is the built-in, assuming no
                // user-defined name exists --> that is OK too)
                type = lookupWorkerExact(dict, actual);

                // check if there was a match to the actual text
                if (type == -1)
                {
                   // match the full text of the keyword with the built-in
                   type = lookupWorkerExact(dict, kw.getFullText());
                }
             }
          }
          else
          {
             // there is no keyword, so this must be a user-defined name
             type = lookupWorkerExact(dict, actual);
          }

          return type;
       }

       /**
        * Primary worker routine for processing searches on dictionaries based on
        * the {@link com.goldencode.p2j.util.ScopedSymbolDictionary} class. All
        * searches are for a symbol exactly matching the passed key.  The token
        * type of the match is returned.  Note that a token type of -1 is invalid
        * in the Lexer and Parser, so this value is used to denote a search that
        * found no match.  No abbreviation processing will be provided!
        *
        * @param   dict
        *          {@code ScopedSymbolDictionary} instance to search.
        * @param   name
        *          Exact text string to match.
        *
        * @return  The label's token type that was found or -1 if no match was found.
        */
       private int lookupWorkerExact(ScopedSymbolDictionary dict, String name)
       {
          if (dict == null)
          {
             return -1; // frame not found because there is no frame dictionary in use
          }

          Integer result = (Integer) dict.lookupSymbol(name);
          if (result != null)
          {
             return result;
          }

          return -1; // not found
       }

       /**
        * Centralizes the process of setting an file-unique temporary index for
        * each new wrapped object and then adding that object to the passed-in
        * dictionary.
        *
        * @param    ssd
        *           Symbol dictionary.
        * @param    global
        *           Add to global scope if <code>true</code>.
        * @param    name
        *           Variable name to add.
        * @param    wrapped
        *           Wrappered token object to add.
        */
       private void addWrappedWorker(ScopedSymbolDictionary ssd,
                                     boolean                global,
                                     String                 name,
                                     TokenDataWrapper       wrapped)
       {
          ClassDefinition cls = getCurrentClassDef();
          if (cls != null)
          {
             if (wrapped instanceof Variable)
             {
                // inherit the temp-idx from the pre-scan - PRIVATE allows to match on everything
                Variable var = (Variable) wrapped;

                if ((ssd == varDict || ssd == dsetDict || ssd == dsrcDict) && !inMethod && !isPreScan())
                {
                   // private will match any access mode
                   boolean st = var.isStatic();
                   Variable old = ssd == varDict
                                     ? cls.lookupVariableWrapper(name, KW_PRIVATE, st)
                                     : ssd == dsetDict ? cls.lookupDataSetWrapper(name, KW_PRIVATE, st)
                                                       : cls.lookupDataSourceWrapper(name, KW_PRIVATE, st);
                   if (old != null)
                   {
                      var.setTempIndex(old.getTempIndex());
                   }
                   else
                   {
                      var.setTempIndex(cls.nextTempIdx());
                   }
                }
                else
                {
                   var.setTempIndex(cls.nextTempIdx());
                }

                var.setClassDefinition(cls);
             }
             else
             {
                wrapped.setTempIndex(cls.nextTempIdx());
             }
          }
          else
          {
             wrapped.setTempIndex(tempIdx);
             tempIdx = tempIdx + 1;
          }
          if (ssd != null)
          {
             ssd.addSymbol(global, name, wrapped);
          }
       }

       /**
        * Given an AST which represents a field or variable which possibly has
        * an extent, check for a subscript and return it.  The assumed structure
        * of the AST, if it does have an extent subscript is:
        * <pre>
        *    FIELD/VAR
        *      |
        *      +--LBRACKET
        *           |
        *           +--EXPRESSION
        *                |
        *                +--NUM_LITERAL
        * </pre>
        * where the <code>FIELD/VAR</code> node is the <code>ast</code> parameter
        * passed to this method.
        * <p>
        * Currently, only the <code>NUM_LITERAL</code> token type is supported
        * for the subscript value.  Any other type of expression will raise an
        * exception.
        *
        * @param   ast
        *          Field or variable AST.
        *
        * @return  The 1-based subscript value from the <code>NUM_LITERAL</code>
        *          node depicted above, or 0 if there is no subscript.
        *
        * @throws  UnsupportedOperationException
        *          if a token type other than a simple <code>NUM_LITERAL</code>
        *          is provided for the subscript expression.
        * @throws  IllegalArgumentException
        *          if the <code>NUM_LITERAL</code> token cannot be parsed into an
        *          integral value.
        */
       private int getExtentSubscript(Aast ast)
       {
          int subscript = 0;

          if (ast.getFirstChild() != null)
          {
             Aast lbracket = ast.getImmediateChild(LBRACKET, null);
             if (lbracket != null)
             {
                Aast expr = lbracket.getChildAt(0);
                Aast numlit = expr.getImmediateChild(NUM_LITERAL, null);

                if (numlit == null)
                {
                   numlit = expr.getImmediateChild(HEX_LITERAL, null);

                   if (numlit == null)
                   {
                      String err = "Non-NUM_LITERAL/non-HEX_LITERAL extent value not supported " +
                                   "in this context!";
                      throw new UnsupportedOperationException(err);
                   }

                   String hex = numlit.getText();

                   try
                   {
                      decimal d = decimal.fromUnsignedHexLiteral(hex);
                      subscript = d.intValue();
                   }
                   catch (NumberFormatException exc)
                   {
                      throw new IllegalArgumentException("Invalid hex extent subscript:  " + hex);
                   }
                }
                else
                {
                   try
                   {
                      subscript = Integer.parseInt(numlit.getText());
                   }
                   catch (NumberFormatException exc)
                   {
                      throw new IllegalArgumentException(
                         "Invalid extent subscript:  " + numlit.getText());
                   }
                }
             }
          }

          return subscript;
       }

       /**
        * Convert any "." or "+" (internal .NET class) characters (in a Progress qualified class or
        * interface name) to the current system's file name separator string.
        *
        * @param    name
        *           Qualified class or interface name.
        *
        * @return   Name that is safe to use with the current file system.
        */
       private String convertSeparator(String name)
       {
          return name.replace('.', File.separatorChar).replace('+', File.separatorChar);
       }

       /**
        * Search the PROPATH for a specific class or interface OR if this is a Java class, try to
        * load it. For a non-Java class, the given name should be a Progress style qualified name
        * (package + class). This will be converted to a file system name and searched for.
        *
        * @param    name
        *           Qualified class or interface name.
        * @param    type
        *           The type of the using clause that is being checked. This
        *           changes the search order and may drop some paths from the
        *           search when possible.
        * @param    result
        *           Object in which to store the search results (the file system
        *           name, the built-in flag and the .NET flag) when the search
        *           is successful. If nothing is found, this object is unchanged.
        *
        * @return   <code>true</code> if the class file was found.
        */
       private boolean findFile(String name, UsingType type, SearchResult result)
       {
          if (type == UsingType.JAVA)
          {
             return isJavaClass(name, result);
          }

          // we tried an approach based on FileSystemDaemon.search() but it is
          // slow, so we switched to a preloaded in-memory cache approach

          name = convertSeparator(name);

          Map<String, List<String>>[] searchList = null;

          boolean[] builtin   = new boolean[4];
          boolean[] dotnet    = new boolean[4];

          // 4GL always matches case-insensitively (even on case-sensitive file system)
          // the code below will differentiate using this var
          boolean[] honorCase = new boolean[4];

          // choose the search paths based on the type
          switch (type)
          {
             case PROPATH:
                // no need to search .NET stuff
                searchList    = new HashMap[2];
                searchList[0] = propathCls;
                searchList[1] = locate().oo4glCls;

                builtin[0] = false; dotnet[0] = false; honorCase[0] = false;
                builtin[1] = true;  dotnet[1] = false; honorCase[1] = false;
                break;
             case DOTNET:
                // the documentation says there is no need to search PROPATH but
                // working code has found Progress.* using stmts with the FROM
                // ASSEMBLY option, so we add the OO4GL stuff in as a last resort
                // TODO: should this be more general?
                searchList    = new HashMap[3];
                searchList[0] = locate().dotnetCls;
                searchList[1] = locate().assemblyCls;
                searchList[2] = locate().oo4glCls;

                builtin[0] = true;  dotnet[0] = true;  honorCase[0] = caseSens;
                builtin[1] = false; dotnet[1] = true;  honorCase[1] = caseSens;
                builtin[2] = true;  dotnet[2] = false; honorCase[2] = false;
                break;
             default:
                // search everything!
                searchList    = new HashMap[4];
                searchList[0] = propathCls;
                searchList[1] = locate().oo4glCls;
                searchList[2] = locate().dotnetCls;
                searchList[3] = locate().assemblyCls;

                builtin[0] = false; dotnet[0] = false; honorCase[0] = false;
                builtin[1] = true;  dotnet[1] = false; honorCase[1] = false;
                builtin[2] = true;  dotnet[2] = true;  honorCase[2] = caseSens;
                builtin[3] = false; dotnet[3] = true;  honorCase[3] = caseSens;
                break;
          }

          WorkArea wa = locate();
          Set<String> cvtSources = wa.cvtSources;
          for (int i = 0; i < searchList.length; i++)
          {
             Map<String, List<String>> map  = searchList[i];
             String                    srch = honorCase[i] ? name : name.toLowerCase();

             List<String> propathList = map.get(srch);
             if (propathList == null)
             {
                // also try the other separator (Windows on Linux or Linux on Windows), this allows
                // to define the search paths with both separators
                srch = srch.replace(File.separatorChar, File.separatorChar == '/' ? '\\' : '/');
                propathList = map.get(srch);
             }

             if (propathList != null)
             {
                if (propathList.size() == 1)
                {
                   // Class previous found in oo4glCls, but also previous
                   // found in propath. In this case it should be removed
                   // from conversion.
                   if (builtin[i] && !dotnet[i] && result.filename != null)
                   {
                      File referenceFile = new File(result.filename);
                      if (ScanDriver.removeSource(referenceFile))
                      {
                         LOG.log(Level.WARNING,
                                 String.format("Legacy builtin class %s found in conversion list, but is part of "
                                             + "FWD skeletons - this file will be removed from conversion.", srch));
                      }

                   }

                   result.filename = propathList.get(0);
                   result.builtin = builtin[i];
                   result.dotnet  = dotnet[i];

                   if (builtin[i] && !dotnet[i])
                   {
                      break;
                   }

                   continue;
                }

                // multiple propath entries, we should take only that
                // present in the conversion list.
                if (!builtin[i] && !dotnet[i] && cvtSources != null)
                {
                   for (String filename: propathList)
                   {
                      if (cvtSources.contains(filename))
                      {
                         result.filename = filename;
                         result.builtin  = builtin[i];
                         result.dotnet   = dotnet[i];
                         break;
                      }
                   }

                   // Log a warning that we could not the filename from multiple
                   // propath entries.
                   if (result.filename == null)
                   {
                      LOG.log(Level.WARNING,
                              String.format("Multiple entries found in PROPATH for %s, but none is in conversion list. "
                                          + "Please fix the PROPATH or the conversion list.", srch));
                   }
                }
             }
          }

          if (result.filename != null)
          {
             // Source found in propath, should be added to the conversion list.
             if (!result.builtin && !result.dotnet && !cvtSources.contains(result.filename))
             {

                String srch = name.replace(File.separatorChar, '.');
                if (loadClassDefinition(this, srch) == null)
                {
                   File referenceFile = new File(result.filename);
                   ScanDriver.addSource(referenceFile);
                }
             }

             return true;
          }

          return false;
       }

       /**
        * For 4GL, .NET or unspecified types, search the PROPATH for the given partially qualified
        * class or interface.  For Java types, a non-propath search is made.
        *
        * @param    name
        *           Partially qualified class or interface name.
        * @param    type
        *           The type of the using clause that is being checked. This
        *           changes the search order and may drop some paths from the
        *           search when possible.
        * @param    result
        *           Object in which to store the search results (the file system
        *           name, the built-in flag and the .NET flag) when the search
        *           is successful. If nothing is found, this object is unchanged.
        *
        * @return   <code>true</code> if the class file was found.
        */
       private boolean srchPropathFindFile(String name, UsingType type, SearchResult result)
       {
          if (propath != null && type != UsingType.JAVA)
          {
             for (int i = 0; i < propath.length; i++)
             {
                String fqn = null;

                // deal with special case of solo dot
                fqn = (propath[i].equals(".")) ?
                   name : Configuration.normalizeFilename(propath[i] + File.separatorChar + name);

                // since we are dealing with a class or interface, ditch the leading path indicators
                if (fqn.startsWith("." + File.separator))
                {
                   fqn = fqn.substring(2);
                }

                if (findFile(fqn, type, result))
                {
                   return true;
                }
             }
          }
          else if (type == UsingType.JAVA)
          {
             return findFile(name, type, result);
          }

          return false;
       }

       /**
        * Test if the given package exists in the PROPATH, or in the 4GL system packages or in the
        * .NET system namespaces.
        *
        * @param    pkg
        *           Package name to search for.
        *
        * @return   The normalized OS path of this package.
        */
       private String pkgExists(String pkg)
       {
          pkg = convertSeparator(pkg);

          File file = null;
          String path = null;

          for (int i = 0; i < propath.length; i++)
          {
             if (propath[i].length() == 0 || ".".equals(propath[i]))
             {
                // check a relative file in the current directory
                file = new File(pkg);
             }
             else
             {
                // check a relative file in the specified path
                file = new File(propath[i], pkg);
             }

             // 4GL classes are always matched case-insensitively (even on case-sensitive
             // file systems
             path = exists(file, false);
             if (path != null)
             {
                path = Configuration.normalizeFilename(path);
                return Configuration.normalizeFilename(Configuration.getParameter("basepath"), path);
             }
          }

          // check if the package exists as part of the 4GL system
          file = new File(oo4glPath, pkg);
          path = exists(file, false);
          // 4GL classes are always matched case-insensitively (even on case-sensitive file systems
          if (path != null)
          {
             return Configuration.normalizeFilename(oo4glPath, path);
          }

          // check if the namespace for the system classes in .NET
          file = new File(dotnetPath, pkg);
          path = exists(file, caseSens);
          if (path != null)
          {
             return Configuration.normalizeFilename(dotnetPath, path);
          }

          // check if the namespace for the user assemblies in .NET
          file = new File(ASSEMBLY_PATH, pkg);
          path = exists(file, caseSens);
          if (path != null)
          {
             return Configuration.normalizeFilename(ASSEMBLY_PATH, path);
          }

          return null;
       }

       /**
        * Implements a check for the file's existence while honoring the
        * case-sensitive file-system flag.  If the original file-system was
        * case-sensitive, then a simple <code>exists()</code> method call is
        * made on the file object.  Otherwise a case-insensitive search is
        * undertaken.
        *
        * @param    file
        *           The file to check.
        * @param    caseSens
        *           <code>true</code> if the legacy system had a case-sensitive
        *           file-system.
        *
        * @return   The file's canonical path.
        */
       private String exists(File file, boolean caseSens)
       {
          // quick out: does the exact match exist?
          if (file.exists())
          {
             try
             {
                return file.getCanonicalPath();
             }
             catch (IOException e)
             {
                // ignore
             }
          }

          if (!caseSens)
          {
             // TODO: this is broken!!!!  It was found to fail when the exact case-sensitive match
             //       actually existed (resolved by checking for that first, above).  It is not
             //       known if it fails in other cases too.
             return FileSystemDaemon.getCaseInsensitiveMatch(file, true);
          }

          return null;
       }

       /**
        * Map the keyword type to the corresponding field type.
        *
        * @param    ktype
        *           Keyword type.
        *
        * @return   The matching field type or -1 if the input is invalid.
        */
       private int keywordToFieldType(int ktype)
       {
          int ftype = -1;

          switch (ktype)
          {
          case KW_BLOB:
             ftype = FIELD_BLOB;
             break;
          case KW_CHAR:
             ftype = FIELD_CHAR;
             break;
          case KW_CLASS:
          case CLASS_NAME:
             ftype = FIELD_CLASS;
             break;
          case KW_CLOB:
             ftype = FIELD_CLOB;
             break;
          case KW_COM_HNDL:
             ftype = FIELD_COM_HANDLE;
             break;
          case KW_DATE:
             ftype = FIELD_DATE;
             break;
          case KW_DATETIME:
             ftype = FIELD_DATETIME;
             break;
          case KW_DATE_TZ:
             ftype = FIELD_DATETIME_TZ;
             break;
          case KW_DEC:
             ftype = FIELD_DEC;
             break;
          case KW_HANDLE:
             ftype = FIELD_HANDLE;
             break;
          case KW_INT:
             ftype = FIELD_INT;
             break;
          case KW_INT64:
             ftype = FIELD_INT64;
             break;
          case KW_LOGICAL:
             ftype = FIELD_LOGICAL;
             break;
          case KW_RAW:
             ftype = FIELD_RAW;
             break;
          case KW_RECID:
             ftype = FIELD_RECID;
             break;
          case KW_ROWID:
             ftype = FIELD_ROWID;
             break;
          case KW_WID_HAND:
             ftype = FIELD_HANDLE;
             break;
          }

          return ftype;
       }

       /**
        * Process a request for the schema dictionary.  If that request cannot be satisfied from
        * the current schema dictionary instance AND the current scope is processing a class, then
        * the schema dictionaries of the parent hierarchy will be checked until the request is
        * satisfied or there are no more parents in the inheritance chain.
        *
        * @param    worker
        *           The core processing to be executed on the given schema dictionary.
        * @param    continu
        *           A condition which if it returns <code>true</code>, then the next schema
        *           dictionary should be tried.  On <code>false</code>, the current result is
        *           returned.
        *
        * @return   The result of the processing.
        */
       private <V> V processHierarchy(SchemaHelper<V> worker, Predicate<V> continu)
       throws SchemaException
       {
          SchemaException[] exc = new SchemaException[1];

          V result = worker.process(schemaDict);
          final V backup = result;

          // the predicate determines if we continue searching
          if (continu.test(result))
          {
             ClassDefinition def = getCurrentClassDef();

             if (def != null)
             {
                java.util.function.Function<ClassDefinition, V> func = (ClassDefinition cls) ->
                {
                   V value = backup;

                   SchemaDictionary dict = cls.getSchemaDict();

                   // only test this class if there is something to test
                   if (dict != null)
                   {
                      try
                      {
                         // try again
                         value = worker.process(dict);
                      }
                      catch (SchemaException e)
                      {
                         exc[0] = e;
                      }
                   }

                   return value;
                };

                result = def.calcFromParentGraph(func, continu, backup, true, false);

                if (exc[0] != null)
                {
                   throw exc[0];
                }
             }
          }

          return result;
       }

       /**
        * Return the access mode as encoded by the AST type.
        *
        * @param    am
        *           The access mode node.
        *
        * @return   The type of the node or KW_PRIVATE if there is no node.
        */
       private int decodeAccessMode(Aast am)
       {
          return (am != null) ? am.getType() : KW_PRIVATE;
       }

       /**
        * Return the static mode as encoded by the AST type.
        *
        * @param    st
        *           The static mode node.
        *
        * @return   <code>true</code> if the type of the node KW_STATIC.
        */
       private static boolean decodeStatic(Aast st)
       {
          return (st != null && st.getType() == KW_STATIC);
       }

       /**
        * Calculates the access mode for method and data member searches of
        * class definitions, based on the presence and contents of the given
        * class name and the current context. By default the access mode will
        * be <code>KW_PUBLIC</code>. If this request is made within the context
        * of a class definition, then the access mode will be
        * <code>KW_PRIVATE</code> if <code>cname</code> is <code>null</code> or
        * if <code>cname</code> is the same as the current class definition
        * name. If both the current class and the searched one are in the same package,
        * the access mode will be <code>KW_PK_PRIV</code>. The access mode will
        * be <code>KW_PROTECTD</code> if the <code>cname</code> is not <code>null</code>
        * and it is the equivalent of one of the parent class names in the current class
        * definition's inheritance chain.
        *
        * @param    cname
        *           Fully qualified class name being searched or <code>null</code>
        *           if the current class definition is to be searched.
        *
        * @return   The calculated access mode of <code>KW_PUBLIC</code>,
        *           <code>KW_PROTECTD</code>, <code>KW_PK_PRIV</code> or <code>KW_PRIVATE</code>.
        */
       private int calcAccessMode(String cname)
       {
          int access = KW_PUBLIC;

          ClassDefinition cls = getCurrentClassDef();

          if (cls != null)
          {
             if (cname != null)
             {
                String current = cls.getName();

                if (cname.equalsIgnoreCase(current))
                {
                   // inside a class def and explicitly referencing the current
                   // class, this is a private search
                   access = KW_PRIVATE;
                }
                else
                {
                   if (getLegacyPackageName(current) != null &&
                       getLegacyPackageName(cname) != null   &&
                       getLegacyPackageName(current).equalsIgnoreCase(getLegacyPackageName(cname)))
                   {
                      access = KW_PK_PRIV;
                   }

                   Predicate<ClassDefinition> check = (ClassDefinition def) ->
                   {
                      return cname.equalsIgnoreCase(def.getName());
                   };

                   if (cls.testParentGraph(check))
                   {
                      // this is explicit access to a parent class from within
                      // a subclass definition, this is a protected search
                      access = KW_PROTECTD;
                   }
                }
             }
             else
             {
                // we are inside a class def and have no explicit class to
                // search, this is a private search of the current class'
                // hierarchy
                access = KW_PRIVATE;
             }
          }

          return access;
       }

       /**
        * Extracts the legacy package name from a fully qualified class name.
        * <p>
        * If the class name is {@code null} or empty, this method returns {@code null}.
        * If the class name does not contain a dot (i.e., is not in a package),
        * this method returns an empty string.
        * Otherwise, it returns the substring before the last dot, which represents the package name.
        *
        * @param   className
        *          The fully qualified class name.
        * @return  The package portion of the class name, an empty string if no package exists,
        *          or {@code null} if the input is {@code null} or empty.
        */
       public static String getLegacyPackageName(String className)
       {
          if (className == null || className.isEmpty())
          {
             return null;
          }

          int lastDot = className.lastIndexOf('.');
          return (lastDot >= 0) ? className.substring(0, lastDot) : "";
       }

       /**
        * Throws runtime exception or returns an error message to inform that the class of the supplied qualified
        * name was not found in PROPATH.
        *
        * @param   qname
        *          Class qualified name.
        * @param   throwEx
        *          The method will throw a runtime exception when <code>true</code>, otherwise it will return
        *          the formatted error message.
        *
        * @return  The formatted error message
        */
       private String reportClassNotFound(String qname, boolean throwEx)
       {
          String err = String.format("Cannot find class/interface %s in PROPATH.", qname);
          if (throwEx)
          {
             throw new RuntimeException(err);
          }

          return err;
       }

       /**
        * Create a new {@link Variable}, with the specified details.
        *
        * @param    name
        *           The variable legacy name.
        * @param    tokenType
        *           The type of the variable (which can be any kind of member for a legacy class).
        * @param    node
        *           The defining node for this variable
        * @param    isStatic
        *           Flag indicating that this is a static definition.
        *
        * @return   See above.
        */
       static Variable createVariable(String name, int tokenType, Aast node, boolean isStatic)
       {
          Variable dataset = new Variable(name, tokenType, null);
          dataset.setOptions(node);
          dataset.setStatic(isStatic);

          return dataset;
       }

       /**
        * Describes the type of the USING specification.
        */
       private static enum UsingType
       {
          PROPATH, DOTNET, JAVA, UNSPECIFIED
       };

       /**
        * Functional interface for use with lambdas.
        */
       private interface SchemaHelper<V>
       {
          /**
           * Execute the logic against the given instance of the schema dictionary.
           *
           * @param    dict
           *           The schema dictionary.
           *
           * @return   The resulting value generated.
           */
          public V process(SchemaDictionary dict)
          throws SchemaException;
       }

       /**
        * Store the data in a USING statement.
        */
       private static class UsingSpec
       {
          /** Match specification (package/.NET namespace or full classname). */
          private String spec = null;

          /** Match type. */
          private UsingType type = UsingType.UNSPECIFIED;

          /**
           * Primary constructor.
           *
           * @param    spec
           *           Match specification (package/.NET namespace or full
           *           classname).
           * @param    type
           *           Match type.
           */
          public UsingSpec(String spec, UsingType type)
          {
             this.spec = spec;
             this.type = type;
          }
       }

       /**
        * Store the data resulting from a given file system search.
        */
       private static class SearchResult
       {
          /** File system object that was found (includes pathing). */
          private String filename = null;

          /** Fully qualified class or interface name relative to PROPATH. */
          private String clsname = null;

          /** Flag to mark a built-in Progress (OO 4GL) class. */
          private boolean builtin = false;

          /** Flag to mark a .NET class. */
          private boolean dotnet = false;

          /** Flag to mark a Java class. */
          private boolean java = false;

          /** Search hint. */
          private UsingType type = UsingType.UNSPECIFIED;
       }

       /**
        * Container with context-local data.
        */
       private static class WorkArea
       {
          /**
           * Flag indicating that {@link SymbolResolver#loadClass} will throw an exception if it can't find the
           * class, used by {@link SymbolResolver#tryLoadClass}.
           */
          private boolean forceLoadFailure = false;

          /** Flag indicating that the legacy builtin classes have been loaded. */
          private boolean legacyClassesLoaded = false;

          /**
           * A cache of class definitions previously converted (not by the current run). The key is
           * the qualified class name.
           */
          private Map<String, ClassDefinition> classCache = new HashMap<>();

          /** Mapping of class files to their qualified name. */
          private Map<String, String> fname2qname = new HashMap<>();

          /** Mapping of builtin class qualified name to its Java converted name. */
          private Map<String, String> legacyToJava = null;

          /**
            * Holds class dictionary for each used PROPATH.  This allows to limit the visible classes
            * only to the ones in the current PROPATH (a performance penalty will exist, as each class
            * may be loaded in every distinct PROPATH in the project).
            */
          private Map<String, ScopedSymbolDictionary> propathClassDicts = new HashMap<>();

          /** 4GL sources defined in the conversion list */
          private Set<String> cvtSources = null;

          /** Classes in the OO4GL_PATH, mapped qualified classname to filename. */
          private Map<String, List<String>> oo4glCls = null;

          /** Classes in the DOTNET_PATH, mapped qualified classname to filename. */
          private Map<String, List<String>> dotnetCls = null;

          /** Classes in the ASSEMBLY_PATH, mapped qualified classname to filename. */
          private Map<String, List<String>> assemblyCls = null;

          /**
           * The nesting level of {@link SymbolResolver#loadClass} calls when performing 1st-level
           * load of a class.
           */
          private int preScanPass = 0;

          /** The set of class/interface references processed during 1st level parse of a class. */
          private Set<String> preScanRefs = "TRUE".equalsIgnoreCase(Configuration.getParameter("case-sensitive"))
                                              ? new HashSet<>()
                                              : new CaseInsensitiveHashSet<>();

          /** An ordered set of class/interface references processed. */
          private LinkedList<String> processed = new LinkedList<>();

          /** The converter to generated Java names. */
          private NameConverter nconvert = new NameConverter();

          /** Set of pending classes to be loaded. */
          private HashSet<String> pendingLoading = new HashSet<>();
      }
    }