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<>();
  }
}