# ExpressionConversionWorker.java

    /*
    ** Module   : ExpressionConversionWorker.java
    ** Abstract : provides a simple, callable interface to convert entire
    **            expression trees from Progress to Java AST forms
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 GES 20050406   @20862 Created initial version which supports a
    **                           manual tree walking approach such that an
    **                           expression can be converted "in-line" from
    **                           any rule and the result will be properly
    **                           located in the target tree.  This is critical
    **                           since expressions are used *everywhere*, so
    **                           the conversion is best done by calling this
    **                           worker's high level routines when a node
    **                           of type EXPRESSION is encountered.
    ** 002 GES 20050501   @20960 Added support for variable definitions and
    **                           references.
    ** 003 GES 20050505   @21121 Moved the majority of expression processing
    **                           into rule-sets and this leaves only the
    **                           minimum helper methods here.  Some methods
    **                           were moved here from other classes to
    **                           centralize the few helpers that are needed
    **                           specifically for expressions.
    ** 004 ECF 20050526   @21290 Changes to support new expression engine
    **                           implementation. Changed library registration
    **                           mechanism based on superclass' modifications
    **                           to support single library per pattern worker
    **                           limit.
    ** 005 GES 20050530   @21369 Migrated to new data type wrappers.
    ** 006 GES 20050603   @21399 Removed variable definition/init helpers
    **                           because the worker is deleted and the logic
    **                           exists in rule sets directly.
    ** 007 GES 20050607   @21445 Added a helper to determine the Java wrapper
    **                           type associated with an expression.
    ** 008 GES 20050614   @21487 Updated FUNC_POLY processing to support the
    **                           IF/THEN/ELSE function and to have better
    **                           error handling.
    ** 009 GES 20050901   @22433 Removed operator processing and updated
    **                           expressionType() to support the new method
    **                           and attribute data types.
    ** 010 GES 20050928   @22873 Removed createDateReference() and simplified
    **                           naming.
    ** 011 GES 20051024   @23111 Added conversion rule-set access to the
    **                           convertToRegEx and convertToSQLLike methods
    **                           of the character wrapper.
    ** 012 ECF 20051108   @23278 Added support for RECID/ROWID types in
    **                           expressions. Both convert to integer in our
    **                           implementation.
    ** 013 ECF 20060114   @23912 Added support for ACCUM function to
    **                           expressionType. Return type depends on type
    **                           of accumulation as well as expression being
    **                           accumulated.
    ** 014 GES 20060228   @24788 Added support for INPUT function.
    ** 015 GES 20060302   @24846 Added support for RAW/MEMPTR types.
    ** 016 GES 20060308   @24933 Added support for CUSTOMER_SPECIFIC types.
    ** 017 ECF 20060321   @25170 Added compareClientWhereNodes user function.
    **                           Helper function for client-side where clause
    **                           processing.
    ** 018 GES 20060407   @25417 Fix for INPUT morphing mode.
    ** 019 GES 20060410   @25455 Better handling for unknown value literals
    **                           in the ternary (IF) builtin function.
    ** 020 GES 20060421   @25638 Added a safe way to test if a given node is
    **                           a valid part of an expression.
    ** 021 GES 20060421   @25642 Fix for infinite loop created when an INPUT
    **                           function is compared to another INPUT
    **                           function.
    ** 022 GES 20060911   @29487 Exposed new helpers for string literal and
    **                           format string processing.
    ** 023 GES 20060920   @29720 Added formattedResultSize().
    ** 024 GES 20061018   @30481 Added minimal support for a symbol node in
    **                           expressionType(). The only case supported is
    **                           an in-line var def in a UI language stmt.
    ** 025 GES 20070907   @35014 Support for 10.1B data types.
    ** 026 GES 20080327   @37697 Support for handle types (how did I miss that
    **                           before?).
    ** 027 ECF 20080730   @39284 Added first class support for rowid type.
    **                           This type now has its own wrapper class. The
    **                           integer wrapper is still used for recid.
    ** 028 GES 20090424   @41929 Import change.
    ** 029 SVL 20090225   @41373 Record IDs are mapped to recid, not to integer.
    ** 030 GES 20090518   @42379 Import change.
    ** 031 SIY 20090915   @43902 Added isFormatCompatible() method.
    ** 032 SVL 20110622          Added convertToSQLBegins,
    **                           postprocessStringLiteral, toUpperCase.
    ** 033 SIY 20111101          Updated imports.
    ** 034 OM  20121218          Added support for int64 datatype.
    ** 035 EVL 20130107          Changing internal progressToJavaString()
    **                           implementation to take into account possible
    **                           Windows-target style of the '\' character in
    **                           path related string handling.
    ** 036 CA  20130123          Provided support for DYNAMIC-FUNCTION.
    ** 037 OM  20130128          Added support for datetime(tz) data types.
    ** 038 CA  20130128          Updated reference from character to TextOps.
    **                           Allow longchar expressions in concatenation.
    ** 039 OM  20130212          Added support for CONTAINS clause.
    ** 040 GES 20130225          Major improvement/cleanup to *_POLY node processing.
    **                           Some new _POLY types are added and context analysis was
    **                           added to the ones that can't be otherwise predicted.
    ** 041 GES 20130301          Removed some incorrect analysis in PLUS typing. Added a
    **                           best efforts try to determine the MINUS case when the
    **                           1st operand is missing.
    ** 042 GES 20130302          Fixed a ClassCastException.
    ** 043 GES 20130304          Added DB_REF_NON_STATIC support.
    ** 044 CS  20130307          Added extra flag to expresionType marking if context should be
    **                           looked up in the left side operand of assign or not. default is
    **                           true.
    ** 045 GES 20130308          Add helpers for cleanup of date, num and decimal literals.
    ** 046 CA  20130309          Added APIs to disambiguate unknown vals.
    ** 047 CA  20130313          Fixed conversion of varargs parameters.
    ** 048 GES 20130309          System handles need to be reported as handles. The lookUp flag
    **                           was made into an infer flag and was made a generic facility.
    **                           This means that any caller with this flag as false will disable
    **                           context analysis (type inference) and the result will return
    **                           BaseDataType for sub-expressions where the _POLY node is both
    **                           indeterminate AND is the node which would determine the type.
    **                           Many other expression (and "near expression" elements have been
    **                           added so that expressionType() can be called in a much wider range
    **                           of cases without failing. Major rework to integrate the signature
    **                           processing into expressionType().
    ** 049 CA 20130315           The HANDLE attribute must return the WrappedResource classname, as
    **                           P2J does not convert it to a handle. Needed by cases when the HANDLE
    **                           attribute is used as a parameter.
    ** 050 SVL 20130319          Added fuzzy parameter to a number of functions, added
    **                           inImpicitInput flag, added VALIDATE option case in analyzeContext.
    ** 051 GES 20130323          Rewrote the ternary IF function analysis to fix all known cases
    **                           including the proper processing of indeterminate and incompatible
    **                           operand types.  The DYNAMIC-FUNCTION() case was also modified to
    **                           return BDT if infer is false, even when the casttype is available.
    **                           This enables callers to make better wrapping choices.
    ** 052 OM  20130325          Added socket read const literals: READ_AVAILABLE_LITERAL and
    **                           READ_EXACT_NUM_LITERAL.
    ** 053 OM  20130403          Support for intermediary 64 bit computing added.
    ** 054 OM  20130516          Added "datetimetz" type for DATETIME_TZ_LITERAL expressions.
    ** 055 GES 20130517          Added byte ordering literals (4GL compiler constants).
    ** 056 OM  20130521          Added DATETIME_LITERAL type to datetime.
    ** 057 CA  20131208          Emit SEARCH-TARGET and SEARCH-SELF literals as integer.
    ** 058 CA  20140314          Added asString(int), to convert a int ASCII code to a string.
    ** 059 EVL 20140318          The helper function progressToJavaString() has been extended to take
    **                           into account path string handling in Windows OS meaning unix-escapes
    **                           is flase.  In this case we need to handle usual strings containing
    **                           "\" characters and path related strings differently.
    ** 060 ECF 20140405          Replaced static variables with context local work area.
    ** 061 OM  20140408          Fall back to "character" instead of "BaseDataType" in case of
    **                           polymorphic return type of a DB_REF_NON_STATIC expression.
    ** 062 ECF 20140508          Reverted 061, which regressed some expression conversions.
    ** 063 OM  20140918          Split progressToJavaString into parseProgressCharLiteral and
    **                           encodeToJavaSource so that HQL processing can be run in between.
    **                           Added unix-escapes to convertToSQLLike / convertToRegEx.
    ** 064 OM  20141024          Added isCaseInsensitive() method as support for Java7 string switch.
    ** 065 GES 20150127          Added datetime/datetimetz support to isFormatCompatible().
    ** 066 GES 20150131          Reworked formattedResultSize() to delegate the implementation to the
    **                           matching BDT class.
    ** 067 HC  20150225          Fixed var char case sensitivity detection.
    ** 068 CA  20150722          PLUS operator can accept mixed int/date types.
    ** 069 OM  20150907          Added some support for evaluation type of KW_SELF and KW_INPT_VAL
    **                           _POLY node in expressions. Added support for type of widget
    **                           attributes. Added asciiCode() exposed function.
    ** 070 IAS 20160331          Potential NPE fix.
    ** 071 HC  20160714          Fixed case sensitivity of ENTRY built-in function during conversion.
    ** 072 CA  20160919          POLY attrs/methods must emit DynamicOps when used as an operand.
    ** 073 GES 20160802          Removed compareClientWhereNodes() which is no longer needed.
    ** 074 OM  20170504          Added Editor find/replace constants as INTEGER constants.
    ** 075 CA  20170523          Made isAssignmentCompatible public.
    **     GES 20170720          Added early support for COM automation and OO invocations.
    ** 076 OM  20170530          Included support for type evaluation of com-handles.
    ** 077 CA  20170825          Added evaluateCharacterExpression, which can compute a all-literal
    **                           string concatenation expression.
    **     GES 20170827          Added isQuoted() and stripQuotes().
    **     OM  20170829          Result type of MIN and MAX int expressions is in64.
    ** 078 CA  20171023          Instead of throwing an exception, evaluateCharacterExpression now
    **                           returns null if the AST contains other than concatenated strings,
    ** 079 GES 20171016          Added fromUnsignedHexLiteral() and isHexLiteralValid().
    **         20171017          Added hexadecimal literal support to simpleExpressionType().
    **         20171018          Added numeric literal parsing methods.
    ** 080 CA  20180323          Added CALL-TYPE literals: PROCEDURE-CALL-TYPE, FUNCTION-CALL-TYPE,
    **                           GET-ATTR-CALL-TYPE, SET-ATTR-CALL-TYPE, DLL-CALL-TYPE
    ** 081 CA  20180514          Fixed classname resolution for widget types: COMBO-BOX, EDITOR,
    **                           RADIO-SET, TOGGLE-BOX, SELECTION-LIST - resolve it from 'refid'.
    **     CA  20180517          Fixed comhandle class name (it was com_handle).
    ** 082 EVL 20181022          Fixed isCaseInsensitive() method to check if the var:attribute is
    **                           character with letter case ignoring in CASE...WHEN statement.  Thanks
    **                           to OM for an idea of the solution.
    ** 083 CA  20181029          Added CALL handle support - correct mapping of CALL-TYPE literals.
    ** 084 GES 20181108          First pass at OO_METH_* support.
    **     CA  20181128          Added some missing builtin literals to simpleExpressionType.
    **                           Disabled analyzeContext for OO method arguments.
    ** 085 CA  20181208          Added management for KW_RET_VAL in expressionType's ATTR_POLY case.
    **                           For MIN/MAX expr type calculation, if there was a integer or int64
    **                           argument found, then ignore any POLY reference. Added CLASS_NAME.
    **     GES 20181222          Object types return "object" if there is no "qualified" annotation
    **                           or "object<? extends QUALIFIED>" if the annotaton exists.
    **     GES 20181227          MULTIPLY, MINUS and PLUS must return integer type when both operands
    **                           are integer.
    ** 086 OM  20190423          Attempt to use oriref annotation to detect expression type.
    **     CA  20190424          resolveNumericType must return BDT if either left or right operands'
    **                           type is BDT.  Other misc fixes.
    ** 087 CA  20190508          MAX() and MIN() have BDT parameters, not INT.
    **                           Fixed DATASET-HANDLE and TABLE-HANDLE signature interpretation.
    **     CA  20190509          MIN and MAX can be POLY, too.
    **     CA  20190520          Process KW_<type> in simpleExpressionType.
    ** 089 CA  20190529          Added comhandle to simpleExpressionType.
    ** 090 CA  20190905          Fixed a NPE for dynamic min/max.
    ** 091 CA  20190927          Added support for direct Java access from 4GL code.
    ** 092 CA  20191211          Fixed a typo for datetime(tz) expressions.
    ** 093 CA  20200304          Java fields referenced directly from 4GL code must have their type
    **                           the real field type.
    ** 094 RFB 20200310          In expressionType, the KW_INPT_VAL case needed to include SYS_HANDLE
    **                           in the type that would result in a BaseDataType, instead of handle.
    **     HC  20200328          Improved expressionType to handle COM_INVOCATION expressions with known types.
    **     CA  20200429          Added BITWISE_AND, _NOT, _XOR and _OR - not implemented.
    **     GES 20200526          Implemented BITWISE_AND, _NOT, _XOR and _OR.
    ** 095 GES 20200628          Added DEFAULT-VALUE.
    **     HC  20200729          Added isClassAbstract.
    **     HC  20200903          Improved method resolution when wrapping arguments passed to OCX-
    **                           converted methods.
    **     CA  20210125          Progress.Lang.Class:Invoke and Progress.Reflect.Method:Invoke must be treated as
    **                           having a POLY returned type.
    **     OM  20201218          Added support for early type-checking.
    **     VVT 20210317          isFormatCompatible() updated for #4880.
    **     HC  20210401          Made isNumericType and isDateType part of the library.
    **     GES 20210610          Added KW_WID_HAND.
    **     HC  20211001          Implementation of i18n support.
    **     AL2 20211104          DB_REF_NON_STATIC generates case insensitive values.
    **     CA  20211214          The AST manager plugin must be context-local, for runtime conversion, as the
    **                           InMemoryRegistryPlugin is not thread-safe.
    **     CA  20220106          MAX/MIN functions will have as type their argument's type (when int/int64 is
    **                           involved), when the function is used as an argument for an OO call.  This is an
    **                           improvement for OM/20170829 change.
    **     AL2 20220207          Evaluate INPUT_VALUE type as BaseDataType if not using type inference.
    **     ME  20211028          Added the POLY 'virtual' data type.
    **     CA  20220307          Legacy OO skeletons are updated to use POLY, removed the 'invoke' checks for
    **                           poly methods, as these will be handled automatically.
    **     GES 20220311          Added safety code for DYNAMIC-PROPERTY as a FUNC_POLY.
    **     CA  20220325          Added isTopLevelInternal (extracted from common-progress.rules).
    **     AL2 20220412          Dynamic functions should return a non-null value when type checking.
    **     OM  20221124          Added special processing for buffer-fields passed as argument via
    **                           CALL:SET-PARAMETER or to Progress.Lang.ParameterList:SetParameter.
    **     CA  20220428          Fixed STREAM-HANDLE arguments (used by builtin functions).
    **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
    ** 096 GES 20230111          Fixed the MINUS operator when op1 is a FUNC_POLY and the op2 is date/datetime
    **                           or datetime-tz.
    ** 097 RFB 20230221          Fixed GET-MOUSE-POSITION (KW_GET_MOP) to have jcls of integer[] instead of METH_POLY.
    **                           Ref #7135.
    ** 098 OM  20230115          Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
    **                           versions, based on node types rather on string paths.
    ** 099 CA  20230526          In a ternary IF, when first operand is character and second is longchar, allow
    **                           longchar type for the IF, but only if we are not in a builtin function argument.
    ** 100 CA  20230426          Added 'hasOperators'.
    **     CA  20230501          'ABS' return type is 'int64' for an 'integer' expression.
    ** 101 CA  20230712          Throw a NullPointerException if the Java class can't be resolved for a definition
    **                           using a Java type.
    ** 102 ES  20230721          Change for isCaseInsensitive method in case of substring method
    ** 103 CA  20231004          Fixed conversion for class events with parameters of type DATASET, TABLE,
    **                           DATASET-HANDE, TABLE-HANDLE and BUFFER.
    ** 104 CA  20230928          A DYNAMIC-INVOKE with 'dynamic-poly' annotation is not a realy DYNAMIC-INVOKE
    **                           call.
    ** 105 CA  20231019          SUBSTRING function's return type is determined by the first argument.
    ** 106 OM  20240103          When analysing the type, if the node was duplicated (extracted QUERY_SUBST),
    **                           look at the original node structure.
    ** 107 SVL 20240130          Added KW_SUBSTR to expressionType resolver.
    ** 108 OM  20240101          Limit search from H106 to cases where the original node is accessible.
    ** 109 SVL 20240212          Fixed case sensitivity of the returned value of all built-in character functions.
    ** 110 AI  20250224          Fixed case when expressionType for PLUS operand between integer and character
    **                           would return date type.
    ** 111 CA  20250319          For INPUT-VALUE, instead of assuming 'GenericWidget', the type needs to be
    **                           'BaseDataType', to allow 'poly' resolution of method calls or other expressions.
    */

    /*
    ** 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.convert;

    import java.lang.reflect.*;
    import java.util.*;
    import com.goldencode.ast.*;
    import com.goldencode.util.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.p2j.ui.client.format.*;
    import com.goldencode.p2j.pattern.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.security.ContextLocal;

    /**
     * A pattern worker whose purpose is to provide helpers for conversion of
     * Progress language expression trees into the corresponding Java trees.  The
     * general idea is to call a user function (from a pattern engine rule set)
     * referencing a Progress source AST node that is part of a subtree that
     * is rooted at type <code>EXPRESSION</code>.
     * <p>
     * In all expression conversion, the structure of the expression is
     * maintained, such that the generated target AST node(s) will be
     * parented/rooted at the corresponding 'peer' nodes in the source AST.
     * <p>
     * This class provides access to a static method in <code>character</code>
     * for string conversion, {@link ExpressionHelper#progressToJavaString}.
     * <p>
     * More importantly, this class provides an analysis method which is used
     * to determine the type of an expression or any sub-node of an expression.
     */
    public final class ExpressionConversionWorker
    extends AbstractPatternWorker
    implements ProgressParserTokenTypes
    {
       /** 4GL quote characters. */
       private final static char[] QUOTES = { '\"', '\'' };

       /** Context local work area */
       private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /**
        * Default constructor which initializes libraries.
        */
       public ExpressionConversionWorker()
       {
          super();
          setLibrary(new ExpressionHelper());
       }

       /**
        * Check if the operand represents any of the valid text classes.
        *
        * @param    cls
        *           The classname to test.
        *
        * @return   <code>true</code> if the classname is <code>character</code>,
        *           <code>longchar</code> or <code>Text</code>.
        */
       private static boolean isTextType(String cls)
       {
          return ("character".equals(cls) || "longchar".equals(cls) || "Text".equals(cls));
       }

       /**
        * Check if the operand represents any of the valid numeric classes.
        *
        * @param    cls
        *           The classname to test.
        *
        * @return   <code>true</code> if the classname is <code>integer</code>,
        *           <code>int64</code>, <code>decimal</code>, <code>recid</code> or
        *           <code>NumberType</code>.
        */
       private static boolean isNumericType(String cls)
       {
          return ("integer".equals(cls) || "int64".equals(cls)      ||
                  "decimal".equals(cls) || "NumberType".equals(cls) ||
                  "recid".equals(cls));
       }

       /**
        * Check if the operand represents any of the valid date classes.
        *
        * @param    cls
        *           The classname to test.
        *
        * @return   <code>true</code> if the classname is <code>date</code>,
        *           <code>datetime</code> or <code>datetimetz</code>.
        */
       private static boolean isDateType(String cls)
       {
          return ("date".equals(cls) || "datetime".equals(cls) || "datetimetz".equals(cls));
       }

       /**
        * Check if the operand represents any of the valid binary classes.
        *
        * @param    cls
        *           The classname to test.
        *
        * @return   <code>true</code> if the classname is <code>memptr</code>,
        *           <code>raw</code> or <code>BinaryData</code>.
        */
       private static boolean isBinaryType(String cls)
       {
          return ("memptr".equals(cls) || "raw".equals(cls) || "BinaryType".equals(cls));
       }

       /**
        * Find the nearest top level parent block.
        *
        * @param    target
        *           The target AST to start the lookup.
        *
        * @return   The nearest top level parent block.
        */
       public static Aast findTopLevel(Aast target)
       {
          while (target.getParent() != null)
          {
             Aast parent = target.getParent();
             int ttype = parent.getType();

             // this duplicates the rule from 'isTopLevelInternal'
             if (ttype == PROCEDURE                             ||
                 ttype == FUNCTION                              ||
                 ttype == TRIGGER_BLOCK                         ||
                 ttype == METHOD_DEF                            ||
                 ttype == CONSTRUCTOR                           ||
                 ttype == DESTRUCTOR                            ||
                 target.relativePath(DEFINE_PROPERTY, KW_GET)   ||
                 target.relativePath(DEFINE_PROPERTY, KW_SET))
             {
                return parent;
             }

             target = parent;
          }

          return target.getAncestor(-1);
       }

       /**
        * Analyzes the type of operands and infers if the two types are assignment
        * compatible.
        *
        * @param   ftype
        *          The type of the first operand.
        * @param   stype
        *          The type of the second operand.
        *
        * @return  <code>true</code> if the two types can be assigned interchangeably.
        */
       public static boolean isAssignmentCompatible(final String ftype, final String stype)
       {
          boolean result = false;

          if (ftype != null && ftype.equals(stype))
          {
             result = true;
          }
          else if (isNumericType(ftype) && isNumericType(stype))
          {
             result = true;
          }
          else if (isTextType(ftype) && isTextType(stype))
          {
             result = true;
          }
          else if (isDateType(ftype) && isDateType(stype))
          {
             result = true;
          }
          else if (isBinaryType(ftype) && isBinaryType(stype))
          {
             result = true;
          }

          return result;
       }

       /**
        * Convert the given <code>NUM_LITERAL</code> or <code>HEX_LITERAL</code> into an int.
        *
        * @param    literal
        *           The literal to convert.  It MUST be a <code>NUM_LITERAL</code> or
        *           <code>HEX_LITERAL</code> node.
        *
        * @return   The parsed literal as an int.
        */
       public static int literalToInt(Aast literal)
       {
          int val = 0;

          int    ttype = literal.getType();
          String txt   = literal.getText();

          if (ttype == NUM_LITERAL)
          {
             val = Integer.parseInt(txt);
          }
          else if (ttype == HEX_LITERAL)
          {
             val = decimal.fromUnsignedHexLiteral(txt).intValue();
          }
          else
          {
             String msg = String.format("Input must be a NUM_LITERAL or HEX_LITERAL, not %s.",
                                        literal.getSymbolicTokenType());
             throw new IllegalArgumentException(msg);
          }

          return val;
       }

       /**
        * Parse a <code>Long</code> from <code>text</code> and return it.  Do
        * not raise an exception if <code>text</code> cannot be parsed.
        *
        * @param   text
        *          String representing a base-10 <code>long</code> value.
        *
        * @return  <code>text</code> as a <code>Long</code>, or
        *          <code>null</code> if <code>text</code> cannot be parsed into
        *          a <code>Long</code>.
        */
       public static Long parseLongQuiet(String text)
       {
          try
          {
             return Long.valueOf(text);
          }
          catch (NumberFormatException exc)
          {
             return null;
          }
       }

       /**
        * Compares the given value to the largest and smallest possible hexadecimal literals that
        * can be directly represented as a Java hexadecimal literal. Since the Java hex literal is
        * a signed 64-bit value, very large positive and very large negative values cannot be
        * directly represented.  This method uses constants
        * <code>decimal.MIN_VALID_HEX_LITERAL</code> and
        * <code>decimal.MIAX_VALID_HEX_LITERAL</code> to define the range.
        *
        * @param    hex
        *           The hexadecimal string to be parsed.
        *
        * @return   <code>true</code> if the value is in range and can be represented as a Java
        *           literal.
        */
       public static boolean isHexLiteralValid(String hex)
       {
          decimal d = decimal.fromUnsignedHexLiteral(hex);

          boolean smaller = d.compareTo(decimal.MIN_VALID_HEX_LITERAL) < 0;
          boolean larger  = d.compareTo(decimal.MAX_VALID_HEX_LITERAL) > 0;

          return !(smaller || larger);
       }

       /**
        * Parse a <code>Long</code> from <code>text</code> using the given
        * radix, and return it.  Do not raise an exception if <code>text</code>
        * cannot be parsed.
        *
        * @param   text
        *          String representing a <code>long</code> value with the
        *          specified radix.
        * @param   radix
        *          Radix to be used interpreting <code>text</code>.
        *
        * @return  <code>text</code> as a <code>Long</code>, or
        *          <code>null</code> if <code>text</code> cannot be parsed into
        *          a <code>Long</code>.
        */
       public static Long parseLongQuiet(String text, int radix)
       {
          try
          {
             return Long.valueOf(text, radix);
          }
          catch (NumberFormatException exc)
          {
             return null;
          }
       }

       /**
        * Convert the given <code>NUM_LITERAL</code> or <code>HEX_LITERAL</code> into a long.
        *
        * @param    literal
        *           The literal to convert.  It MUST be a <code>NUM_LITERAL</code> or
        *           <code>HEX_LITERAL</code> node.
        *
        * @return   The parsed literal as a long.
        */
       public static long literalToLong(Aast literal)
       {
          long val = 0;

          int    ttype = literal.getType();
          String txt   = literal.getText();

          if (ttype == NUM_LITERAL)
          {
             val = Long.parseLong(txt);
          }
          else if (ttype == HEX_LITERAL)
          {
             val = decimal.fromUnsignedHexLiteral(txt).longValue();
          }
          else
          {
             String msg = String.format("Input must be a NUM_LITERAL or HEX_LITERAL, not %s.",
                                        literal.getSymbolicTokenType());
             throw new IllegalArgumentException(msg);
          }

          return val;
       }

       /**
        * Convert the given <code>NUM_LITERAL</code>, <code>HEX_LITERAL</code> or
        * <code>DEC_LITERAL</code> into a double.
        *
        * @param    literal
        *           The literal to convert.  It MUST be a <code>NUM_LITERAL</code> or
        *           <code>HEX_LITERAL</code> node.
        *
        * @return   The parsed literal as a double.
        */
       public static double literalToDouble(Aast literal)
       {
          double val = 0;

          int    ttype = literal.getType();
          String txt   = literal.getText();

          if (ttype == NUM_LITERAL)
          {
             val = (double) Long.parseLong(txt);
          }
          else if (ttype == HEX_LITERAL)
          {
             val = decimal.fromUnsignedHexLiteral(txt).doubleValue();
          }
          else if (ttype == DEC_LITERAL)
          {
             val = decimal.fromLiteral(txt).doubleValue();
          }
          else
          {
             String msg = String.format("Input must be a NUM_LITERAL, HEX_LITERAL or " +
                                        "DEC_LITERAL, not %s.",
                                        literal.getSymbolicTokenType());
             throw new IllegalArgumentException(msg);
          }

          return val;
       }

       /**
        * Analyzes the type of operands and infers the numeric expression type.  For binary
        * operations (plus, minus, multiply) widening is automatically done if either operand
        * is int64 or decimal.  Only if both operands are integer will the result be integer.
        * <p>
        * This method must not be called for:
        * <ul>
        * <li>Modulus: as it always returns "int64"
        * <li>Divide operator: will always return "decimal"
        * </ul>
        *
        * @param   ftype
        *          The type of the first operand.
        * @param   stype
        *          The type of the second operand.
        *
        * @return  The type of the expression as string ("decimal", "int64", "integer") if operand
        *          types are valid and <code>null</code> otherwise.
        */
       private static String resolveNumericType(final String ftype, final String stype)
       {
          if ("BaseDataType".equals(ftype) || "BaseDataType".equals(stype))
          {
             // we are in dynamic operators mode here
             return "BaseDataType";
          }

          boolean bInt32 = false;
          boolean bInt64 = false;
          boolean bFloat = false;
          boolean bUnknown = false;

          // analyze type of the first operand
          if ("integer".equals(ftype) || "int".equals(ftype))
          {
             bInt32 = true;
          }
          else if ("int64".equals(ftype) || "long".equals(ftype))
          {
             bInt64 = true;
          }
          else if ("decimal".equals(ftype) || "double".equals(ftype))
          {
             bFloat = true;
          }
          else
          {
             bUnknown = true;
          }

          // analyze type of the second operand
          if ("integer".equals(stype) || "int".equals(stype))
          {
             bInt32 = true;
          }
          else if ("int64".equals(stype) || "long".equals(stype))
          {
             bInt64 = true;
          }
          else if ("decimal".equals(stype) || "double".equals(stype))
          {
             bFloat = true;
          }
          else
          {
             bUnknown = true;
          }

          if (bUnknown)
          {
             // at least one operand is not numeric
             return null;
          }
          else if (bFloat)
          {
             // at least one of the operands is decimal
             return "decimal";
          }
          else if (bInt64)
          {
             // at least one of the operands is a long
             return "int64";
          }
          else
          {
             assert bInt32; // both operands are 32 bit integers
             return "integer";
          }
       }

       /**
        * Analyzes the type of operands and infers the date expression type for binary
        * operations (plus, minus, multiply, modulus), maybe others.  This will "widen"
        * the type to be the type with more information so long as both types are date
        * types.  For example, if one operand is a <code>date</code> and another is
        * <code>datetime</code>, then <code>datetime</code> will be returned. This matches
        * 4GL behavior (at least in the ternary IF function).
        *
        * @param   ftype
        *          The type of the first operand.
        * @param   stype
        *          The type of the second operand.
        *
        * @return  The type of the expression as a string ("date", "datetime",
        *          "datetimetz") if operand types are valid or <code>null</code> otherwise.
        */
       private static String resolveDateType(final String ftype, final String stype)
       {
          boolean u1  = "unknown".equals(ftype);
          boolean u2  = "unknown".equals(stype);

          if (u1 && u2)
          {
             return null;
          }

          if (u1)
          {
             return stype;
          }

          if (u2)
          {
             return ftype;
          }

          boolean d1  = "date".equals(ftype);
          boolean d2  = "date".equals(stype);
          boolean dt1 = "datetime".equals(ftype);
          boolean dt2 = "datetime".equals(stype);
          boolean tz1 = "datetimetz".equals(ftype);
          boolean tz2 = "datetimetz".equals(stype);

          String cls = "date";

          if ((d1 && d2) || (dt1 && dt2) || (tz1 && tz2))
          {
             cls = ftype;
          }
          else if (tz1 || tz2)
          {
             cls = "datetimetz";
          }
          else if (dt1 || dt2)
          {
             cls = "datetime";
          }

          return cls;
       }

       /**
        * Resolve the full signature of the method/function specified with the given AST.
        *
        * @param    ast
        *           The function/method call AST.
        * @param    fuzzy
        *           Determines if "fuzzy" function signature matching is allowed. See {@link
        *           SignatureHelper#getSignature(int, int, String[], boolean)}.
        *
        * @return   The signature, as a {@link ParmType} array.
        */
       private static ParmType[] resolveSignature(Aast ast, boolean fuzzy)
       {
          ParmType[] signature = null;

          int type = ast.getType();
          int kw = ((Long) ast.getAnnotation("oldtype")).intValue();

          // check special cases first
          if (kw == KW_DYN_FUNC)
          {
             // the number of mandatory params is determined by the presence of the KW_IN child
             int mand = ast.downPath(KW_IN) ? 2 : 1;
             signature = SignatureHelper.getSignature(kw, type, mand);
          }
          else if (kw == KW_NEW && ast.downPath(CLASS_NAME))
          {
             // nothing specific is needed here...
             return null;
          }
          else if (kw == KW_DYN_INVK && ast.isAnnotation("dynamic-poly"))
          {
             // DYNAMIC-INVOKE with 'dynamic-poly' is not a real DYNAMIC-INVOKE
             return null;
          }
          else
          {
             boolean multiSignature = SignatureHelper.isMultiSignature(kw, type);

             if (!multiSignature)
             {
                // normal case, just get the signature
                signature = SignatureHelper.getSignature(kw, type);
             }
             else
             {
                // complex case, need to identify the correct signature based on the existing
                // parameter types plus the number of passed parameters
                String[] ptypes = new String[ast.getNumImmediateChildren()];
                for (int i = 0; i < ptypes.length; i++)
                {
                   // disable type inference
                   String ptype = expressionType(ast.getChildAt(i), false);

                   if (ptype == null)
                   {
                      ptype = "BaseDataType";
                   }

                   ptypes[i] = ptype;
                }

                signature = SignatureHelper.getSignature(kw, type, ptypes, fuzzy);
             }
          }

          return signature;
       }

       /**
        * Lookup the parameter type at the specified index position for the built-in function
        * or method specified by given AST.
        *
        * @param    ast
        *           The parent AST of the parameter which needs its type resolved.
        * @param    idx
        *           The zero-based index position of the parameter to lookup.
        * @param    fuzzy
        *           Determines if "fuzzy" function signature matching is allowed. See {@link
        *           SignatureHelper#getSignature(int, int, String[], boolean)}.
        *
        * @return   The Java class name of the wrapper class for that parameter or
        *           null if there is no match found for any reason.
        */
       private static String parameterTypeName(Aast ast, int idx, boolean fuzzy)
       {
          // TODO: do we need to support OO_METH_* lookups here?
          ParmType[] signature = resolveSignature(ast, fuzzy);

          if (signature == null)
          {
             // TODO: show warning that it couldn't resolve the signature
             return null;
          }

          ParmType type = null;

          if (idx <= signature.length - 1)
          {
             type = signature[idx];
          }
          else
          {
             // check if we are in a varargs node
             ParmType last = signature[signature.length - 1];
             if (last.isVarargs())
             {
                type = last;
             }
             else
             {
                final String msg =
                   "The parameter index %d for function %s at %d @%d:%d is out of bounds!";
                int kw = ((Long) ast.getAnnotation("oldtype")).intValue();

                String err = String.format(msg,
                                           idx, ProgressParser.lookupTokenName(kw), ast.getId(),
                                           ast.getLine(), ast.getColumn());
                throw new ArrayIndexOutOfBoundsException(err);
             }
          }

          if (type.isVarargs())
          {
             switch (type)
             {
                case BDT_VARARGS:
                   type = ParmType.BDT;
                   break;
                case NUM_VARARGS:
                   type = ParmType.NUM;
                   break;
                case TEXT_VARARGS:
                   type = ParmType.TEXT;
                   break;
             }
          }

          String wrapper = null;

          // special processing of types that are abstract or are not valid wrappers
          switch (type)
          {
             case TEXT:
                // default to character
                type = ParmType.CHAR;
                break;
             case NUM:
                // default to int
                type = ParmType.INT;
                break;
             case WID:
                // TODO: default to what?
                wrapper = "GenericWidget";
                type = null;
                break;
             case RECORD:
                // TODO: default to what?
                wrapper = "Buffer";
                type = null;
                break;
             case BIN:
                // default to memptr
                type = ParmType.MEM;
                break;
             case BDT:
                // can't default to anything
                type = null;
                break;
          }

          if (type != null)
          {
             // the type was determined
             wrapper = type.toString();
          }

          return wrapper;
       }

       /**
        * This is the core logic for detecting the return type for the INPUT built-in
        * function.
        *
        * @param    source
        *           The AST node to be inspected. If this is an explicit call to the function,
        *           then this node will be the child of the FUNC_POLY (with oldtype == KW_INPUT).
        *           Otherwise it will be a node that represents an editable widget in a frame
        *           as an implicit INPUT reference inside a validation expression.
        * @param    infer
        *           If <code>true</code> AND if the type is indeterminate (this typically only
        *           can occur for polymorphic return types), then the type will be inferred
        *           from the surrounding context (e.g. containing nodes) when possible.
        *
        * @return   The type of the expression.
        */
       private static String processInputBuiltinFunc(Aast source, boolean infer)
       {
          if (!source.isAnnotation("implicit_input")      &&
              source.getParent().isAnnotation("override") &&
              (Boolean) source.getParent().getAnnotation("override"))
          {
             // if 'override' mode is on, then FWD will use getScreenValue to emulate the INPUT call
             // this must force dynamic operands on this reference.
             return "BaseDataType";
          }

          // normal return type is the same as the referent
          String jcls = expressionType(source, false, true);

          // INPUT has a special mode that can return a character
          // type if the SURROUNDING expression requires it
          if (!jcls.equals("character"))
          {
             Aast parent = source.getParent();
             Aast target = source;

             if (parent.getType() == FUNC_POLY)
             {
                int oldtype = unpackOldType(parent);

                if (oldtype == KW_INPUT)
                {
                   // we must start higher in the tree when this is an explicit call
                   target = parent;
                }
             }

             // this case does not need to be turned off even if infer is false
             // since INPUT doesn't return BDT, it returns a specific type or
             // character
             String otype = analyzeContext(target, infer, true);

             if (otype != null && otype.equals("character"))
             {
                jcls = "character";
             }
          }

          return jcls;
       }

       /**
        * Check if this AST is an operand for an arithmetic operator or ternary if.
        *
        * @param    source
        *           The source AST to check.
        *
        * @return   See above.
        */
       private static boolean hasOperatorAncestor(Aast source)
       {
          Aast spref = source.getParent();
          int sptype = spref.getType();
          while (spref != null && sptype == COLON)
          {
             spref = spref.getParent();
             sptype = spref.getType();
          }

          while (spref != null && sptype == LPARENS)
          {
             spref = spref.getParent();
             sptype = spref.getType();
          }

          return (sptype == PLUS     ||
                  sptype == MINUS    ||
                  sptype == MULTIPLY ||
                  sptype == DIVIDE   ||
                  sptype == KW_MOD   ||
                  sptype == UN_MINUS ||
                  sptype == UN_PLUS  ||
                  (sptype == FUNC_POLY &&
                  (long) spref.getAnnotation("oldtype") == KW_IF));
       }

       /**
        * Evaluate the given expression by concatenating all the operands.
        *
        * @param    expr
        *           The AST expression.
        *
        * @return   The resulted concatenated string or <code>null</code> if it can't be determined
        *           as a constant string, i.e. the expression contains children of type other than:
        *           {@link ProgressParserTokenTypes#EXPRESSION},
        *           {@link ProgressParserTokenTypes#PLUS} or
        *           {@link ProgressParserTokenTypes#STRING}.
        */
       public static String evaluateCharacterExpression(Aast expr)
       {
          Aast firstChild = (Aast) expr.getFirstChild();

          int type = expr.getType();

          switch (type)
          {
             case EXPRESSION:
                return evaluateCharacterExpression(firstChild);

             case PLUS:
                String lval = evaluateCharacterExpression(firstChild);
                String rval = evaluateCharacterExpression((Aast) firstChild.getNextSibling());

                return rval == null || lval == null ? null : lval + rval;

             case STRING:
                String nodeText = expr.getText();

                boolean windows = !Configuration.getParameter("unix-escapes", true);
                return character.progressToJavaString(nodeText, windows, false);

             default:
                System.out.println("Could not evaluate character expression" + expr.dumpTree(true));
                return null;
          }
       }

       /**
        * Walks the <code>ProgressAst</code> node specified and determines the
        * data type of the wrapper object returned by that sub-expression.  This
        * method calls itself recursively as needed to determine the proper type.
        * <p>
        * At this time there is support for all basic data types (date, character,
        * integer, decimal, logical, recid, rowid, raw and memptr). The special
        * <code>FUNC_POLY, ATTR_POLY, METH_POLY, OO_METH_POLY</code> types are only
        * supported for the following:
        * <pre>
        * ABSOLUTE based on the operand
        * ACCUM based on the accumulator subtype and sometimes the expression operand
        * BUFFER-VALUE with context analysis
        * DYNAMIC-FUNCTION, with "casttype" annotation or content analysis
        * GET-BYTES
        * IF based on the 2nd (and sometimes 3rd) operand
        * INPUT the referent type and sometimes overridden with context analysis
        * MAXIMUM based on the list of parameters
        * MINIMUM based on the list of parameters
        * SUPER based on the return type of the containing function definition
        * </pre>
        * <p>
        * The INPUT built-in function normally returns the same type as the
        * single parameter (which must be an lvalue which is found in a frame
        * that is in scope).  However, this function can be used in an expression
        * that requires a <code>character</code> data type even in the case where
        * the parameter is NOT of the <code>character</code> type!  This is like
        * an override option and it must be detected from the SURROUNDING
        * expression (above the FUNC_POLY node). This support is implemented
        * except for one case: <b>the usage of INPUT as a parameter to another
        * built-in function (or presumably a method) which expects a
        * <code>character</code> parameter will also cause this conversion. At
        * this time there is no support in these cases. The resulting type (if
        * not already <code>character</code>) will cause the wrong type to be
        * emitted.</b>
        * <p>
        * Attributes and methods are fully supported except for some
        * <code>ATTR_POLY and METH_POLY</code> nodes that are not processed.
        *
        * @param    source
        *           The AST node to be inspected.
        * @param    infer
        *           If <code>true</code> AND if the type is indeterminate (this typically only
        *           can occur for polymorphic return types), then the type will be inferred
        *           from the surrounding context (e.g. containing nodes) when possible.
        *
        * @return   The classname of the wrapper type returned by this
        *           sub-expression.
        */
       public static String expressionType(Aast source, boolean infer)
       {
          return expressionType(source, infer, false);
       }

       /**
        * Walks the <code>ProgressAst</code> node specified and determines the
        * data type of the wrapper object returned by that sub-expression.  This
        * method calls itself recursively as needed to determine the proper type.
        * <p>
        * At this time there is support for all basic data types (date, character,
        * integer, decimal, logical, recid, rowid, raw and memptr). The special
        * <code>FUNC_POLY, ATTR_POLY, METH_POLY, OO_METH_POLY</code> types are only
        * supported for the following:
        * <pre>
        * ABSOLUTE based on the operand
        * ACCUM based on the accumulator subtype and sometimes the expression operand
        * BUFFER-VALUE with context analysis
        * DYNAMIC-FUNCTION, with "casttype" annotation or content analysis
        * GET-BYTES
        * IF based on the 2nd (and sometimes 3rd) operand
        * INPUT the referent type and sometimes overridden with context analysis
        * MAXIMUM based on the list of parameters
        * MINIMUM based on the list of parameters
        * SUPER based on the return type of the containing function definition
        * </pre>
        * <p>
        * The INPUT built-in function normally returns the same type as the
        * single parameter (which must be an lvalue which is found in a frame
        * that is in scope).  However, this function can be used in an expression
        * that requires a <code>character</code> data type even in the case where
        * the parameter is NOT of the <code>character</code> type!  This is like
        * an override option and it must be detected from the SURROUNDING
        * expression (above the FUNC_POLY node). This support is implemented
        * except for one case: <b>the usage of INPUT as a parameter to another
        * built-in function (or presumably a method) which expects a
        * <code>character</code> parameter will also cause this conversion. At
        * this time there is no support in these cases. The resulting type (if
        * not already <code>character</code>) will cause the wrong type to be
        * emitted.</b>
        * <p>
        * Attributes and methods are fully supported except for some
        * <code>ATTR_POLY and METH_POLY</code> nodes that are not processed.
        *
        * @param    source
        *           The AST node to be inspected.
        * @param    infer
        *           If <code>true</code> AND if the type is indeterminate (this typically only
        *           can occur for polymorphic return types), then the type will be inferred
        *           from the surrounding context (e.g. containing nodes) when possible.
        * @param    fuzzy
        *           Determines if "fuzzy" function signature matching is allowed. See {@link
        *           SignatureHelper#getSignature(int, int, String[], boolean)}.
        *
        * @return   The classname of the wrapper type returned by this
        *           sub-expression.
        */
       public static String expressionType(Aast source, boolean infer, boolean fuzzy)
       throws IllegalArgumentException,
              UnsupportedOperationException
       {
          if (source == null)
             throw new IllegalArgumentException("Source node cannot be null!");

          Aast     parRef   = null;
          String   ftype    = null;
          String   stype    = null;
          String   errmsg   = null;
          String   ocls     = null;
          int      type     = source.getType();
          int      ptype    = -1;
          int      oldtype  = -1;
          WorkArea wa       = context.get();

          int      parentType = source.getParent().getType();
          boolean  ooArg      = (parentType >= BEGIN_OO_METH && parentType <= END_OO_METH)                   ||
                                (parentType == OBJECT_INVOCATION && source.getParent().downPath(KW_PUBLISH)) ||
                                (parentType == LPARENS && source.getParent().getParent().getType() == KW_NEW);

          // special case for implicit INPUT function support
          if (source.isAnnotation("implicit_input") && !wa.inImplicitInput)
          {
             String res = null;
             wa.inImplicitInput = true;
             try
             {
                res = processInputBuiltinFunc(source, infer);
             }
             finally
             {
                wa.inImplicitInput = false;
             }
             return res;
          }

          if (type == VAR_CLASS              &&
              source.isAnnotation("is-java") &&
              (Boolean) source.getAnnotation("is-java"))
          {
             String qname = (String) source.getAnnotation("qualified");
             Class<?> cls = JavaClassDefinition.typeToJavaClass(qname, null, false);
             if (cls == null)
             {
                throw new NullPointerException("Could not resolve jobject type <" + qname + ">!");
             }
             type = JavaClassDefinition.decodeVarType(cls);
          }

          if (type >= BEGIN_VARTYPES                    &&
              type <= END_VARTYPES                      &&
              source.isAnnotation("is-java")            &&
              (boolean) source.getAnnotation("is-java") &&
              source.getParent().getType() == OBJECT_INVOCATION)
          {
             Aast ref0 = (Aast) source.getParent().getFirstChild();
             if (ref0.getType() == OBJECT_INVOCATION)
             {
                ref0 = ref0.getChildAt(1);
             }

             String qtype = (String) ref0.getAnnotation("qualified");
             try
             {
                Class<?> cls = Class.forName(qtype);
                String fname = source.getText();
                while (cls != null)
                {
                   try
                   {
                      Field f = cls.getDeclaredField(fname);
                      if (f != null)
                      {
                         return f.getType().getName();
                      }
                   }
                   catch (Exception e)
                   {
                      // ignore
                   }
                   cls = cls.getSuperclass();
                }
             }
             catch (ClassNotFoundException e)
             {
                throw new RuntimeException(e);
             }
          }

          // first pass (may be null)
          String jcls = simpleExpressionType(type);

          if ("object".equals(jcls) && source.isAnnotation("qualified"))
          {
             if (source.isAnnotation("is-java") && (Boolean) source.getAnnotation("is-java"))
             {
                jcls = "jobject";
             }

             jcls = String.format("%s<? extends %s>", jcls, source.getAnnotation("qualified"));
          }

          // check whether this is com_invocation being converted to Java call
          if (source.getType() == COM_INVOCATION && source.isAnnotation("java-class-name"))
          {
             try
             {
                Class bdt = Class.forName((String) source.getAnnotation("java-class-name"));
                if (BaseDataType.class.isAssignableFrom(bdt))
                {
                   return bdt.getSimpleName();
                }
             }
             catch (ClassNotFoundException e)
             {
                // this must never happen, abort
                throw new RuntimeException(e);
             }
          }

          // some of the simple cases may need additional processing and this also
          // handles the nodes that cannot be determined by type alone
          switch (type)
          {
             case FUNC_CHAR:
                if (source.isAnnotation("oldtype") && (Long) source.getAnnotation("oldtype") == KW_SUBSTR)
                {
                   // get the first parameter
                   String paramType = expressionType(source.getChildAt(0), infer, fuzzy);
                   if ("character".equals(paramType) || "longchar".equals(paramType))
                   {
                      jcls = paramType;
                   }
                }
                break;

             case KW_SUBSTR:
                String paramType = expressionType(source.getChildAt(0), infer, fuzzy);
                if ("character".equals(paramType) || "longchar".equals(paramType))
                {
                   jcls = paramType;
                }
                break;

             case CUSTOMER_SPECIFIC:
                jcls = (String) source.getAnnotation("etype");
                break;

             case SYMBOL:
                // ONLY in-line var definitions (in UI stmts using a format
                // phrase) are supported at this time!
                Aast def = (Aast) source.getNextSibling();

                while (def != null && def.getType() != AGGREGATE)
                {
                   def = (Aast) def.getNextSibling();
                }
                // TODO: def.getType() != FORMAT_PHRASE because of the while leave condition (def.getType() == AGGREGATE)
                if (def != null && def.getType() == FORMAT_PHRASE)
                {
                   if (def.isAnnotation("classname") &&
                       def.isAnnotation("vardef"))
                   {
                      jcls = (String) def.getAnnotation("classname");
                   }
                }
                break;

             case UN_MINUS:
                jcls  = expressionType(source.getChildAt(0), infer, fuzzy);
                // negates returns decimal for decimals and int64 for both integer types
                if (jcls != null && jcls.equals("integer"))
                {
                   jcls = "int64";
                }
                break;

             case EXPRESSION:
             case LPARENS:
             case LBRACKET:
             case UN_PLUS:
             case KW_IN:
             case PARAMETER:
             case KW_VALUE:
                jcls  = expressionType(source.getChildAt(0), infer, fuzzy);
                break;

             case KW_FRAME:
             case KW_BROWSE:
                parRef  = source.getParent();
                oldtype = unpackOldType(parRef);
                ptype   = parRef.getType();

                if (ptype == FUNC_POLY && oldtype == KW_INPUT)
                {
                   jcls = expressionType((Aast) source.getNextSibling(), infer, fuzzy);
                }
                else if ((ptype == FUNC_INT || ptype == FUNC_DEC) &&
                         (oldtype == KW_FR_COL  || oldtype == KW_FR_DOWN ||
                          oldtype == KW_FR_LINE || oldtype == KW_FR_ROW))
                {
                   // should only be the KW_FRAME case
                   jcls = "CommonFrame";
                }
                break;

             case COLON:
             case OBJECT_INVOCATION:
                jcls  = expressionType(source.getChildAt(1), infer, fuzzy);
                break;

             case VAR_COM_HANDLE:
             case FIELD_COM_HANDLE:
             case FUNC_COM_HANDLE:
             case ATTR_COM_HANDLE:
             case METH_COM_HANDLE:
             case OO_METH_COM_HANDLE:
                jcls = "comhandle";
                break;

             case BITWISE_OR:
             case BITWISE_XOR:
             case BITWISE_NOT:
             case BITWISE_AND:
                jcls  = expressionType(source.getChildAt(0), infer, fuzzy);
                break;

             case MULTIPLY:
                // must be numeric but we need to know if it promotes to int64 / decimal
                ftype = expressionType(source.getChildAt(0), infer, fuzzy);
                stype = expressionType(source.getChildAt(1), infer, fuzzy);

                jcls = resolveNumericType(ftype, stype);
                if (jcls == null && !("BaseDataType".equals(ftype) || "BaseDataType".equals(stype)))
                {
                   // at least one operand is not numeric
                   errmsg = "MULTIPLY cannot have op1 (" + ftype + ") and op2 (" + stype + ").";
                }
                break;

             case KW_MOD:
                // if at least one of the operands are int64 the result is int64
                ftype = expressionType(source.getChildAt(0), infer, fuzzy);
                stype = expressionType(source.getChildAt(1), infer, fuzzy);

                // both operands should be numbers, only need to know if result is null or not
                jcls = resolveNumericType(ftype, stype);

                if (jcls == null)
                {
                   if (!("BaseDataType".equals(ftype) || "BaseDataType".equals(stype)))
                   {
                      // at least one operand is not numeric
                      errmsg = "MODULO cannot have op1 (" + ftype + ") and op2 (" + stype + ").";
                   }
                }
                else
                {
                   // if at least one operand is decimal, rounding will occur,
                   // modulo is always performed on integer-type operands and returns int64
                   // even in the error messages the value that caused it is rounded
                   jcls = "int64";
                }
                break;

             case KW_INPUT:
             case KW_OUTPUT:
             case KW_IN_OUT:
                parRef = source.getParent();
                oldtype = unpackOldType(parRef);

                if (parRef.getType() == FUNC_INT && oldtype == KW_SEEK)
                {
                   jcls = "Stream";
                }
                else if (parRef.getType() == PARAMETER)
                {
                   jcls = expressionType((Aast) source.getNextSibling(), infer, fuzzy);
                }
                break;

             case FUNC_INT:
                // possible override of the simple type
                oldtype = unpackOldType(source);
                if (oldtype == KW_RANDOM)
                {
                   // if at least one of the operands are int64 the result is int64
                   ftype = expressionType(source.getChildAt(0), infer, fuzzy);
                   stype = expressionType(source.getChildAt(1), infer, fuzzy);

                   // both operands should be numbers, only need to know if result is null or not
                   jcls = resolveNumericType(ftype, stype);

                   if (jcls == null)
                   {
                      // at least one operand is not numeric
                      errmsg = "Incompatible data types in expression or assignment. (223)";
                   }
                   else
                   {
                      // same like MODULO operator, if at least one operand is decimal, rounding will
                      // occur; RANDOM is always performed on integer-type operands and returns int64
                      jcls = "int64";
                   }
                }
                break;

             case NUM_LITERAL:
             case VAR_INT:
             case FIELD_INT:
             case ATTR_INT:
             case METH_INT:
             case OO_METH_INT:
                // possible override of the simple type
                Boolean use64bit = (Boolean) source.getAnnotation("use64bit");
                if (use64bit != null && use64bit)
                {
                   jcls = "int64";
                }
                break;

             case PLUS:
                ftype = expressionType(source.getChildAt(0), infer, fuzzy);
                stype = expressionType(source.getChildAt(1), infer, fuzzy);

                if (ftype == null || "BaseDataType".equals(ftype))
                {
                   // in this case we need to look at the 2nd operand and determine
                   // what is possible from that (not as much as we would like since
                   // numeric 2nd operands can be used with the various date types as
                   // well as with numerics in the 1st operand)
                   if ("character".equals(stype) || "longtype".equals(stype))
                   {
                      jcls = stype;
                   }
                   else
                   {
                      jcls = "BaseDataType";
                   }
                }
                else if ("date".equals(ftype)       ||
                         "datetime".equals(ftype)   || // adds a number of millis
                         "datetimetz".equals(ftype) || // adds a number of millis
                         "longchar".equals(ftype))
                {
                   jcls = ftype;
                }
                else if ("character".equals(ftype) || "character".equals(stype))
                {
                   // if we see BDT here, leave the result as character, but for longchar
                   // we know we need to change the result type since a longtype in either
                   // operand results in longtype, for the result to be character, both
                   // operands must be character; but in the BDT case we will assume that
                   // the result will be coerced into character (TODO: this may be wrong
                   // in the case where the returned BDT value is a longchar)
                   jcls = "longchar".equals(stype) || "longchar".equals(ftype) ? "longchar" : "character";
                }
                else if ("integer".equals(ftype) ||
                         "int64".equals(ftype) ||
                         "decimal".equals(ftype))
                {
                   // must be numeric but we need to know if it promotes to decimal or int64
                   jcls = resolveNumericType(ftype, stype);

                   if (jcls == null)
                   {
                      // try a date type
                      jcls = resolveDateType(ftype, stype);
                   }

                   if (jcls == null)
                   {
                      // at least one operand is not numeric
                      errmsg = "PLUS cannot have op2 (" + stype + ").";
                      jcls = null;
                   }
                }
                else
                {
                   errmsg = "PLUS cannot have op1 (" + ftype + ").";
                }
                break;

             case MINUS:

                // case 1: numeric - numeric
                //
                // The return values are in this table, depending on the operands.
                //
                //                                op2
                //               -------------------------------------
                //                 integer       int64       decimal
                //   op1         -----------  -----------  -----------
                // integer         integer      int64        decimal
                // int64           int64        int64        decimal
                // decimal         decimal      decimal      decimal

                // case 2: date - date (returns integer)
                // case 3: date - numeric (returns date)
                // case 4: datetime/datetime-tz - numeric returns datetime/datetime-tz
                // case 5: datetime/datetime-tz - datetime/datetime-tz returns int64

                ftype = expressionType(source.getChildAt(0), infer, fuzzy);

                // even if we can predict the type (which we can if stype is date/datetime/datetime-tz, we
                // cannot report the type here because it will be processed at runtime as POLY and returned
                // as BDT; if we don't report this as BDT then any parent expression may not properly be
                // detected as POLY
                if (ftype == null || "BaseDataType".equals(ftype))
                {
                   jcls = "BaseDataType";
                }
                else if ("date".equals(ftype))
                {
                   // check the second child to guess which form operator is used
                   stype = expressionType(source.getChildAt(1), infer, fuzzy);
                   jcls  = "date".equals(stype) ? "integer" : "date";
                }
                else if ("datetime".equals(ftype) || "datetimetz".equals(ftype))
                {
                   // check the second child to guess which form operator is used
                   stype = expressionType(source.getChildAt(1), infer, fuzzy);
                   jcls  = ("datetime".equals(stype) || "datetimetz".equals(stype)) ?
                           "int64" :
                           ftype;
                }
                else if ("integer".equals(ftype) ||
                         "decimal".equals(ftype) ||
                         "int64".equals(ftype))
                {
                   // must be numeric but we need to know if it promotes to decimal or int64
                   stype = expressionType(source.getChildAt(1), infer, fuzzy);

                   jcls = resolveNumericType(ftype, stype);
                   if (jcls == null && !"BaseDataType".equals(stype))
                   {
                      // the second operand is not numeric
                      errmsg = "MINUS cannot have op2 (" + stype + ").";
                   }
                }
                else
                {
                   // at this moment, ftype != "BaseDataType"
                   stype = expressionType(source.getChildAt(1), infer, fuzzy);
                   if (!"BaseDataType".equals(stype))
                   {
                      errmsg = "MINUS cannot have op1 (" + ftype + ").";
                      jcls = null;
                   }
                }
                break;

             case UNKNOWN_VAL:
                if (infer)
                {
                   // try detecting from context
                   jcls = analyzeContext(source, infer, fuzzy);
                }

                if (jcls == null)
                {
                   jcls = "unknown";
                }
                break;

             // polymorphic return values need lots of help to determine the real type
             case DB_REF_NON_STATIC:
             case COM_INVOCATION:
                if (infer)
                {
                   if (hasOperatorAncestor(source))
                   {
                      jcls = "BaseDataType";
                   }
                   else
                   {
                      // try detecting from context
                      jcls = analyzeContext(source, infer, fuzzy);
                   }
                }

                // fall back to a "generic" result
                if (jcls == null)
                {
                   jcls = "BaseDataType";
                }
                break;

             case ATTR_HANDLE:
                oldtype = unpackOldType(source);
                if (oldtype == KW_HANDLE)
                {
                   // if this is the HANDLE attribute, the type of this expression is WrappedResource,
                   // and not handle.
                   jcls = "WrappedResource";
                }
                break;

             case SYS_HANDLE:
                oldtype = unpackOldType(source);
                if (oldtype == KW_SELF)
                {
                   Long refId = (Long) source.getAnnotation("refid");
                   if (refId != null)
                   {
                      Aast refAst = AstSymbolResolver.getResolver().getAst(refId);

                      // the expression type of this should be stored in 'type' annotation
                      Long selfType = (Long) refAst.getAnnotation("type");
                      if (selfType != null)
                      {
                         jcls = simpleExpressionType(selfType.intValue());
                      }
                   }
                   // otherwise the type could not be detected at in annotations
                }
                if (oldtype == KW_COM_SELF)
                {
                   jcls = "comhandle";
                }
                break;

             case WID_COMBO:
             case WID_EDITOR:
             case WID_FILL_IN:
             case WID_RADIO:
             case WID_SEL_LST:
             case WID_TOGGLE:
                // use the refid annotation to go to backing variable and return its classname
                Long refId = (Long) source.getAnnotation("refid");
                if (refId != null)
                {
                   jcls = (String) AstSymbolResolver.getResolver()
                                      .getAst(refId)
                                      .getAnnotation("classname");
                }
                // otherwise the jcls remains as detected by first pass, by simpleExpressionType()
                break;

             // in this case the result is unknown or dynamically deduced
             case STATIC_WRAPPER:
                jcls = "BaseDataType";
                break;

             // polymorphic return values need lots of help to determine the real type
             case FUNC_POLY:
             case ATTR_POLY:
             case METH_POLY:
             case OO_METH_POLY:
                oldtype = unpackOldType(source);
                Aast child = source.getChildAt(0);

                switch (oldtype)
                {
                   case KW_ABS:
                      // return the same type as the first operand
                      jcls = expressionType(child, infer, fuzzy);
                      if ("integer".equals(jcls))
                      {
                         // for integer arg, int64 is returned
                         jcls = "int64";
                      }
                      break;

                   case KW_ACCUM:
                      // get the child node of AGGREGATE which describes the type
                      // of accumulation we're doing
                      Aast accumTypeAst = (Aast) child.getFirstChild();
                      int accumType = accumTypeAst.getType();
                      // get the expression AST, which is AGGREGATE's next
                      // sibling
                      Aast accumExprAst = (Aast) child.getNextSibling();
                      switch (accumType)
                      {
                         case KW_COUNT:
                         case KW_SUB_CNT:
                            jcls = "integer"; // TODO: OM: investigate int64
                            break;
                         case KW_AVERAGE:
                         case KW_SUB_AVG:
                            jcls = "decimal";
                            break;
                         case KW_MAX:
                         case KW_SUB_MAX:
                         case KW_MIN:
                         case KW_SUB_MIN:
                         case KW_TOTAL:
                         case KW_SUB_TOT:
                            jcls = expressionType(accumExprAst, infer, fuzzy);
                            break;
                         default:
                            errmsg = "Unknown " + source.lookupTokenName(type) +
                                     " (" + source.lookupTokenName(accumType) +
                                     ")";
                      }
                      break;

                   case KW_ADD_INVL:
                      // return the same type as the first operand which should always be
                      // one of date, datetime or datetimetz
                      jcls = expressionType(child, infer, fuzzy);
                      break;

                   case KW_BUF_VAL:
                   case KW_DEF_VAL:
                      if (infer)
                      {
                         if (hasOperatorAncestor(source))
                         {
                            jcls = "BaseDataType";
                         }
                         else
                         {
                            // try detecting from context
                            jcls = analyzeContext(source, infer, fuzzy);
                         }

                         // complain if we have no other solution
                         if (jcls == null)
                         {
                            // fallback to a generic result
                            jcls = "BaseDataType";

                            String msg1 = "WARNING: no type calculated for %s-VALUE in [%d] %s";
                            msg1 = String.format(msg1,
                                                 (oldtype == KW_BUF_VAL) ? "BUFFER" : "DEFAULT",
                                                 source.getId(),
                                                 source.dumpTree());
                            System.out.println(msg1);
                         }
                      }
                      else
                      {
                         // when we can't infer the type, we at least report that a BDT is returned
                         jcls = "BaseDataType";
                      }
                      break;

                   case KW_DYN_INVK:
                   case KW_DYN_PROP:
                      // TODO: check if we can improve on this
                      jcls = "BaseDataType";
                      break;

                   case KW_DYN_FUNC:
                      if (infer)
                      {
                         if (hasOperatorAncestor(source))
                         {
                            jcls = "BaseDataType";
                         }
                         else
                         {
                            // check if the tree already has an annotation that tells us the type
                            jcls = (String) source.getAnnotation("casttype");

                            if (jcls == null)
                            {
                               // try detecting from context
                               jcls = analyzeContext(source, infer, fuzzy);
                            }
                         }

                         // complain if we have no other solution
                         if (jcls == null)
                         {
                            String msg2 = "WARNING: 'casttype' annotation missing and context " +
                                          "analysis failed for DYNAMIC-FUNCTION call [%d] %s";
                            System.out.println(String.format(msg2, source.getId(), source.dumpTree()));

                            // fallback to a non-null data type: show that this can be evaluated
                            jcls = "BaseDataType";
                         }
                      }
                      else
                      {
                         // when we can't infer the type, we at least report that a BDT is returned
                         jcls = "BaseDataType";
                      }
                      break;

                   case KW_GET_BYTS:
                      // TODO: determine what is really possible here, some sample code
                      //       has been seen where the result is assigned directly to an
                      //       integer like this: my-int-var = GET-BYTES(some-byte-source, my-pos, 2)
                      //       Perhaps the value is always returned as a memptr and then
                      //       the return value must naturally be handled by the context.
                      //       Or perhaps this is wrong.
                      jcls = "memptr";
                      break;

                   case KW_GET_MOP:
                      // TODO: check if we can improve on this
                      jcls = "integer[]";
                      break;

                   case KW_IF:
                      // by default return the same type as the second operand
                      jcls = expressionType(source.getChildAt(1), false, fuzzy);
                      ocls = expressionType(source.getChildAt(2), false, fuzzy);

                      // the character types do NOT widen in the 4GL, so even though they
                      // (character and longchar) are interchangeable in the 3rd operand, the 2nd
                      // operand determines the type of the ternary (proven by testcases)

                      Aast p1 = source.getParent();
                      int ptype1 = p1 == null ? -1 : p1.getType();
                      if (infer                    &&
                          "longchar".equals(ocls)  &&
                          "character".equals(jcls) &&
                          !(ptype1 >= BEGIN_FUNCTYPES &&
                            ptype1 <= END_FUNCTYPES   &&
                            Boolean.TRUE.equals((Boolean) p1.getAnnotation("builtin"))))
                      {
                         // we force a character 'else' argument only in cases of builtin functions; but even in
                         // these cases, the character length overflow is not checked, the instance is passed
                         // directly
                         jcls = "longchar";
                      }

                      if ("integer".equals(jcls) || "int64".equals(jcls) || "decimal".equals(jcls))
                      {
                         // some various numerics are interchangeable AND they naturally widen, so
                         // both operands need to be considered to select the wider one
                         // (decimal is already the "widest" numeric but checked to be on safe side)
                         String numcls = resolveNumericType(jcls, ocls);
                         if (numcls != null)
                         {
                            jcls = numcls;
                         }
                      }

                      // for date types, the 4GL widens to the type with more information (no need to
                      // check datetimetz here as this is already the widest date type)
                      if ("date".equals(jcls) || "datetime".equals(jcls))
                      {
                         // this will only widen if the 3rd operand is a date type AND it is
                         // wider than the 2nd operand; if this comes back null, that means
                         // there is a non-date or unknown literal in the 3rd operand and we
                         // just stick with our 2nd operand type
                         String datcls = resolveDateType(jcls, ocls);

                         if (datcls != null)
                         {
                            jcls = datcls;
                         }
                      }

                      // unknown value is not a valid type by itself; both operands are
                      // not allowed to be unknown, so there must be a real type in the
                      // 3rd operand which can be used as the type for both operands
                      if ("unknown".equals(jcls))
                      {
                         jcls = ocls;
                      }

                      // we always return a valid wrapper type, this signifies that the result is
                      // indeterminate
                      if (jcls == null || ocls == null || ocls.equals("BaseDataType"))
                      {
                         jcls = "BaseDataType";
                      }

                      break;

                   case KW_INPUT:
                      jcls = processInputBuiltinFunc(child, infer);
                      break;

                   case KW_MAX:
                   case KW_MIN:
                      jcls = findMinMaxType(child, infer, fuzzy);

                      if ("unknown".equals(jcls))
                      {
                         errmsg = "Unknown value cannot be a parameter for " +
                                  source.lookupTokenName(oldtype);
                      }

                      // widen to int64 if all values are only ever integer/int64
                      if (!ooArg && "integer".equals(jcls))
                      {
                         // minimum and maximum static methods return int64 and are also found in
                         // int64 class
                         jcls = "int64";
                      }

                      if (jcls == null)
                      {
                         errmsg = "Invalid function call - no parameters for " +
                                  source.lookupTokenName(oldtype);
                      }
                      break;

                   case KW_SUPER:
                      // based on the return type of the containing function definition
                      Aast function = source.getAncestor(-1, FUNCTION);
                      Aast funcdef  = function.getChildAt(0);
                      long functype = (Long) funcdef.getAnnotation("type");
                      jcls = simpleExpressionType((int) functype);
                      break;

                   case KW_INPT_VAL:
                      // this is an ATTR_POLY so it should be the second child of a COLON node and
                      // the other sibling should give our type
                      if (source.getParent() != null && source.getParent().getType() == COLON)
                      {
                         Aast refObject = source.getParent().getChildAt(0);
                         if (refObject.getType() == VAR_HANDLE   ||
                             refObject.getType() == FIELD_HANDLE ||
                             refObject.getType() == SYS_HANDLE)
                         {
                            // a handle cannot give us any clue about the expression type
                            if (hasOperatorAncestor(source))
                            {
                               jcls = "BaseDataType";
                            }
                            else
                            {
                               if (!infer)
                               {
                                  jcls = "BaseDataType";
                               }
                               else
                               {
                                  jcls = analyzeContext(source.getParent(), infer, fuzzy);
                               }
                            }
                         }
                         else
                         {
                            // if the refObject is a widget, we can extract the type from there
                            jcls = expressionType(refObject, infer, fuzzy);
                            if ("GenericWidget".equals(jcls))
                            {
                               // we are 'poly'
                               if (!infer)
                               {
                                  jcls = "BaseDataType";
                               }
                               else
                               {
                                  jcls = analyzeContext(source.getParent(), infer, fuzzy);
                                  if ("GenericWidget".equals(jcls))
                                  {
                                     jcls = "BaseDataType";
                                  }
                               }
                            }
                         }
                      }
                      else
                      {
                         errmsg = "Parent of INPUT-VALUE [ATTR_POLY] is not a COLON node.";
                      }
                      break;

    //               case KW_ENTRY: TODO: add jcls type

                   case KW_RET_VAL:
                      jcls = "BaseDataType";
                      break;

                   // unexpected *_POLY node, error out here
                   default:
                      if (type == OO_METH_POLY)
                      {
                         jcls = "BaseDataType";
                      }
                      else
                      {
                         errmsg = "Unknown " + source.lookupTokenName(type) +
                                  " with oldtype of " + source.lookupTokenName(oldtype);
                      }
                }
                break;

             // when assigned to, these statements expect a specific (numeric type) right-value
             case KW_PUT_BITS:
             case KW_PUT_BYTE:
             case KW_PUT_DBL:
             case KW_PUT_FLT:
             case KW_PUT_LONG:
             case KW_PUT_SHT:
             case KW_PUT_USHT:
             case KW_SET_SZ:
             case KW_SET_PTR:
             case KW_PUT_I64:
                jcls = "numbertype";
                break;

             // when assigned to, these statements expect a specific (string/character) right-value
             case KW_FIX_CP:
             case KW_PUT_STR:
                jcls = "text";
                break;

             // when assigned to, these statements expect a specific (RAW or MEMPTR) right-value
             case KW_PUT_BYTS:
                jcls = "binarytype";
                break;

             default:
                if (jcls == null)
                {
                   errmsg = "Unknown node type " + source.lookupTokenName(type);
                }
          }

          if (errmsg != null)
          {
             throw new UnsupportedOperationException(errmsg);
          }

          return jcls;
       }

       /**
        * Return the value of the given node's <code>oldtype</code> annotation.
        *
        * @param    node
        *           The node from which to obtain the annotation.
        *
        * @return   The oldtype or -1 if there is no such annotation.
        */
       public static int unpackOldType(Aast node)
       {
          int otype = -1;

          if (node.isAnnotation("oldtype"))
          {
             Long oldtype = (Long) node.getAnnotation("oldtype");
             otype = oldtype.intValue();
          }

          return otype;
       }

       /**
        * Check if this AST has operators.
        *
        * @param    source
        *           The source AST to check.
        *
        * @return   See above.
        */
       public static boolean hasOperators(Aast source)
       {

          Iterator<Aast> iter = source.iterator();
          while (iter.hasNext())
          {
             Aast spref = iter.next();
             int sptype = spref.getType();

             if (sptype == PLUS     ||
                 sptype == MINUS    ||
                 sptype == MULTIPLY ||
                 sptype == DIVIDE   ||
                 sptype == KW_MOD   ||
                 sptype == UN_MINUS ||
                 sptype == UN_PLUS  ||
                 (sptype == FUNC_POLY &&
                 (long) spref.getAnnotation("oldtype") == KW_IF))
             {
                return true;
             }
          }

          return false;
       }

       /**
        * Attempt to detect the type of the given node by looking at the context
        * above and to either side, instead of down.
        * <p>
        * At this time, this has simplistic recursion prevention.  On entry, the node
        * ID of the <code>source</code> is added to a set. If that node is already in
        * the set, it forces an immediate return of <code>null</code>. This protects
        * from possible infinite loops.
        *
        * @param    source
        *           The node to analyze.
        * @param    infer
        *           If <code>true</code> AND if the type is indeterminate (this typically only
        *           can occur for polymorphic return types), then the type will be inferred
        *           from the surrounding context (e.g. containing nodes) when possible.
        * @param    fuzzy
        *           Determines if "fuzzy" function signature matching is allowed. See {@link
        *           SignatureHelper#getSignature(int, int, String[], boolean)}.
        *
        * @return   The calculated wrapper type or <code>null</code> if no type could
        *           be determined.
        */
       public static String analyzeContext(Aast source, boolean infer, boolean fuzzy)
       {
          long currentId = source.getId();

          String otype = null;
          String ftype = null;
          WorkArea wa  = context.get();

          if (!wa.recursionIds.contains(currentId))
          {
             try
             {
                // add our node ID to the flag
                wa.recursionIds.add(currentId);

                Aast   past  = source.getParent();
                int    ptype = past.getType();
                int    index = source.getIndexPos();

                // quick out for ATTR_POLY that is index 0 with a COLON parent, this
                // by definition must be a handle
                if (source.getType() == ATTR_POLY && ptype == COLON && index == 0)
                {
                   return "handle";
                }

                // other ATTR_POLY cases must be checked below, but we must walk up
                // to get above the COLON ancestors; any chaining that isn't with us
                // as a handle (see above) must be the terminal node of the chain, so
                // we only need to "look up" the chain

                while (ptype == LPARENS            ||
                       ptype == LBRACKET           ||
                       ptype == COLON              ||
                       ptype == EXPRESSION         ||
                       ptype == PARAMETER          ||
                       ptype == KW_IN              ||
                       ptype == DB_REF_NON_STATIC)
                {
                   if (past.isAnnotation("oriref"))
                   {
                      // if the node was duplicated, look at the original node structure
                      Long id = (Long) past.getAnnotation("oriref");
                      Aast oriast = AstSymbolResolver.getAst(AstSymbolResolver.getResolver(), id, false);
                      if (oriast != null)
                      {
                         past = oriast;
                      }
                   }

                   // note the order of operations here
                   index = past.getIndexPos();
                   past  = past.getParent();
                   ptype = past.getType();
                }

                int pold = unpackOldType(past);

                boolean builtin = past.isAnnotation("builtin") &&
                                  (Boolean) past.getAnnotation("builtin");

                switch (ptype)
                {
                   case FUNC_CHAR:
                   case FUNC_LONGCHAR:
                   case FUNC_LOGICAL:
                   case FUNC_HANDLE:
                   case FUNC_DEC:
                   case FUNC_INT:
                   case FUNC_INT64:
                   case FUNC_DATE:
                   case FUNC_DATETIME:
                   case FUNC_DATETIME_TZ:
                   case FUNC_RAW:
                   case FUNC_MEMPTR:
                   case FUNC_RECID:
                   case FUNC_ROWID:
                   case FUNC_CLASS:
                      if (builtin)
                      {
                         otype = parameterTypeName(past, index, fuzzy);
                      }
                      break;

                   // ternary expression
                   case FUNC_POLY:
                      if (builtin)
                      {
                         if (pold == KW_IF)
                         {
                            if (index == 0)
                            {
                               // this is the condition expression
                               otype = "logical";
                            }
                            else
                            {
                               // based our type on the type of the other operand (not the
                               // condition)
                               otype = expressionType(past.getChildAt((index == 1) ? 2 : 1),
                                                      infer,
                                                      fuzzy);
                            }
                         }
                         else if (pold == KW_MAX || pold == KW_MIN)
                         {
                            otype = findMinMaxType((Aast) past.getFirstChild(), infer, fuzzy);
                         }
                         else
                         {
                            // generic approach
                            otype = parameterTypeName(past, index, fuzzy);
                         }
                      }
                      break;

                   case METH_CHAR:
                   case METH_CLASS:
                   case METH_DATE:
                   case METH_DATETIME:
                   case METH_DATETIME_TZ:
                   case METH_DEC:
                   case METH_HANDLE:
                   case METH_INT:
                   case METH_INT64:
                   case METH_LOGICAL:
                   case METH_LONGCHAR:
                   case METH_MEMPTR:
                   case METH_RAW:
                   case METH_RECID:
                   case METH_ROWID:
                   case METH_POLY:
                   case METH_VOID:
                      otype = parameterTypeName(past, index, fuzzy);
                      break;

                   case OO_METH_CHAR:
                   case OO_METH_CLASS:
                   case OO_METH_DATE:
                   case OO_METH_DATETIME:
                   case OO_METH_DATETIME_TZ:
                   case OO_METH_DEC:
                   case OO_METH_HANDLE:
                   case OO_METH_INT:
                   case OO_METH_INT64:
                   case OO_METH_LOGICAL:
                   case OO_METH_LONGCHAR:
                   case OO_METH_MEMPTR:
                   case OO_METH_RAW:
                   case OO_METH_RECID:
                   case OO_METH_ROWID:
                   case OO_METH_POLY:
                   case OO_METH_VOID:
                      // TODO: class support is not right here
                      // this is not working for OO, as the signature can't be resolved without
                      // knowing the target method definition
                      otype = null; // parameterTypeName(past, index, fuzzy);
                      break;

                   case UN_MINUS:
                   case UN_PLUS:
                   case MULTIPLY:
                   case DIVIDE:
                   case KW_MOD:
                      // TODO: we have picked the widest type here, but it may not be
                      //       correct (any numeric could be used)
                      otype = "decimal";
                      break;

                   case PLUS:
                      // not all cases can be detected
                      ftype = expressionType(past.getChildAt((index == 0) ? 1 : 0),
                                             infer,
                                             fuzzy);

                      if ("date".equals(ftype)     ||
                          "datetime".equals(ftype) ||
                          "datetimetz".equals(ftype))
                      {
                         // we must be index == 1 in this case

                         // TODO: we have picked the widest type here, but it may not be
                         //       correct (any numeric could be used)
                         otype = "decimal";
                      }
                      else if ("character".equals(ftype) || "longchar".equals(ftype))
                      {
                         // TODO: does not "widen" properly
                         otype = ftype;
                      }
                      else if (index == 1 &&
                               ("integer".equals(ftype) ||
                                "int64".equals(ftype)   ||
                                "decimal".equals(ftype)))
                      {
                         // TODO: we have picked the widest type here, but it may not be
                         //       correct (any numeric could be used)
                         otype = "decimal";
                      }
                      break;

                   case MINUS:
                      // not all cases can be detected
                      ftype = expressionType(past.getChildAt((index == 0) ? 1 : 0),
                                             infer,
                                             fuzzy);

                      if (index == 0 &&
                          ("date".equals(ftype)     ||
                           "datetime".equals(ftype) ||
                           "datetimetz".equals(ftype)))
                      {
                         // TODO: this may not be quite right because different types
                         //       can be intermixed
                         otype = ftype;
                      }
                      else if (index == 1 &&
                               ("integer".equals(ftype) ||
                                "decimal".equals(ftype) ||
                                "int64".equals(ftype)))
                      {
                         // TODO: we have picked the widest type here, but it may not be
                         //       correct (any numeric could be used)
                         otype = "decimal";
                      }
                      break;

                   case GT:
                   case LT:
                   case GTE:
                   case LTE:
                   case EQUALS:
                   case NOT_EQ:
                   case ASSIGN:
                      // check the type on the other side of the operator
                      otype = expressionType(past.getChildAt((index == 0) ? 1 : 0),
                                             infer,
                                             fuzzy);
                      break;

                   case KW_NOT:
                   case KW_AND:
                   case KW_OR:
                      // all operands are logical
                      otype = "logical";
                      break;

                   case BITWISE_OR:
                   case BITWISE_AND:
                   case BITWISE_XOR:
                      // both operands must be the same enum if it is a binary operator, check the other side
                      otype = expressionType(past.getChildAt((index == 0) ? 1 : 0), infer, fuzzy);
                      break;

                   case KW_BEGINS:
                   case KW_MATCHES:
                   case KW_CONTAINS:
                      // both operands are character
                      otype = "character";
                      break;

                   case KW_WHEN:
                      otype = "logical";
                      break;

                   case KW_IF:
                      // function case should have been handled above but safety first
                      if (past.getParent().getType() == STATEMENT)
                      {
                         otype = "logical";
                      }
                      break;

                   case KW_VALIDATE:
                      // VALIDATE option for a widget
                      otype = index == 0 ? "logical" : "character";
                      break;
                }

                // last resort, try to test the original node if the [source] was moved (for example,
                // in a substitution)
                if (otype == null && source.isAnnotation("oriref"))
                {
                   Aast oriRef = AstSymbolResolver.getResolver()
                                                  .getAst((Long) source.getAnnotation("oriref"));
                   otype = analyzeContext(oriRef, infer, fuzzy);
                }
             }
             finally
             {
                // clear the node ID from the set flag
                wa.recursionIds.remove(currentId);
             }
          }

          return otype;
       }

       /**
        * Convert the type directly to a wrapper classname, if possible.  Since there
        * is no tree provided, a <code>null</code> is returned if no simple conversion
        * is possible.
        *
        * @param    type
        *           The type to convert.
        *
        * @return   The classname of the wrapper type for this type or <code>null</code>
        *           if no simple conversion is possible.
        */
       public static String simpleExpressionType(int type)
       {
          String jcls = null;

          switch (type)
          {
             case DIVIDE:
             case DEC_LITERAL:
             case VAR_DEC:
             case FIELD_DEC:
             case FUNC_DEC:
             case ATTR_DEC:
             case METH_DEC:
             case OO_METH_DEC:
             case KW_DEC:
                jcls  = "decimal";
                break;

             // these may need extra processing!
             case NUM_LITERAL:
             case NO_LOCK_LITERAL:
             case SHARE_LOCK_LITERAL:
             case EXCLUSIVE_LOCK_LITERAL:
             case NO_WAIT_LITERAL:
             case READ_AVAILABLE_LITERAL:
             case READ_EXACT_NUM_LITERAL:
             case SEARCH_SELF_LITERAL:
             case SEARCH_TARGET_LITERAL:
             case PROC_CALL_TYPE_LITERAL:
             case FUNC_CALL_TYPE_LITERAL:
             case GET_ATTR_CTYPE_LITERAL:
             case SET_ATTR_CTYPE_LITERAL:
             case DLL_CALL_TYPE_LITERAL:
             case KW_ROW_UMOD:
             case KW_ROW_DELD:
             case KW_ROW_MODD:
             case KW_ROW_CRTD:
             case KW_SAX_COMP:
             case KW_SAX_PARE:
             case KW_SAX_RUNN:
             case KW_SAX_UNIN:
             case KW_SAX_WBEG:
             case KW_SAX_WCOM:
             case KW_SAX_WCON:
             case KW_SAX_WELM:
             case KW_SAX_WERR:
             case KW_SAX_WIDL:
             case KW_SAX_WTAG:
             case KW_WIN_DMIN:
             case KW_WIN_MAX:
             case KW_WIN_MIN:
             case KW_WIN_NORM:
             case KW_SEAR_SLF:
             case KW_SEAR_TRG:
             case KW_HOST_B_O:
             case KW_B_ENDIAN:
             case KW_L_ENDIAN:
             case VAR_INT:
             case FIELD_INT:
             case FUNC_INT:
             case ATTR_INT:
             case METH_INT:
             case OO_METH_INT:
             case KW_FIND_NO:
             case KW_FIND_PO:
             case KW_FIND_CS:
             case KW_FIND_GLO:
             case KW_FIND_WA:
             case KW_FIND_SEL:
             case KW_INT:
                jcls = "integer";
                break;

             case VAR_RECID:
             case FIELD_RECID:
             case FUNC_RECID:
             case ATTR_RECID:
             case METH_RECID:
             case OO_METH_RECID:
             case KW_RECID:
                jcls = "recid";
                break;

             case VAR_ROWID:
             case FIELD_ROWID:
             case FUNC_ROWID:
             case ATTR_ROWID:
             case METH_ROWID:
             case OO_METH_ROWID:
             case KW_ROWID:
                jcls = "rowid";
                break;

             case HEX_LITERAL:
             case VAR_INT64:
             case FIELD_INT64:
             case FUNC_INT64:
             case ATTR_INT64:
             case METH_INT64:
             case OO_METH_INT64:
             case KW_INT64:
                jcls = "int64";
                break;

             case VAR_CLASS:
             case CLASS_NAME:
             case FIELD_CLASS:
             case FUNC_CLASS:
             case ATTR_CLASS:
             case METH_CLASS:
             case OO_METH_CLASS:
             case BITWISE_OR:
             case BITWISE_AND:
             case BITWISE_XOR:
             case BITWISE_NOT:
                jcls  = "object";
                break;

             case KW_BEGINS:
             case KW_MATCHES:
             case KW_CONTAINS:
             case GT:
             case LT:
             case GTE:
             case LTE:
             case EQUALS:
             case NOT_EQ:
             case KW_NOT:
             case KW_AND:
             case KW_OR:
             case BOOL_TRUE:
             case BOOL_FALSE:
             case FUNC_LOGICAL:
             case VAR_LOGICAL:
             case FIELD_LOGICAL:
             case ATTR_LOGICAL:
             case METH_LOGICAL:
             case OO_METH_LOGICAL:
             case KW_LOGICAL:
                jcls = "logical";
                break;

             case STRING:
             case VAR_CHAR:
             case FIELD_CHAR:
             case FUNC_CHAR:
             case ATTR_CHAR:
             case METH_CHAR:
             case OO_METH_CHAR:
             case FILENAME:
             case KW_CHAR:
                jcls = "character";
                break;

             case VAR_LONGCHAR:
             case FUNC_LONGCHAR:
             case ATTR_LONGCHAR:
             case METH_LONGCHAR:
             case OO_METH_LONGCHAR:
             case KW_LONGCHAR:
                jcls = "longchar";
                break;

             case FIELD_BLOB:
                jcls = "blob";
                break;

             case FIELD_CLOB:
                jcls = "clob";
                break;

             case DATE_LITERAL:
             case VAR_DATE:
             case FIELD_DATE:
             case FUNC_DATE:
             case ATTR_DATE:
             case METH_DATE:
             case OO_METH_DATE:
             case KW_DATE:
                jcls = "date";
                break;

             case DATETIME_LITERAL:
             case VAR_DATETIME:
             case FIELD_DATETIME:
             case FUNC_DATETIME:
             case ATTR_DATETIME:
             case METH_DATETIME:
             case OO_METH_DATETIME:
             case KW_DATETIME:
                jcls = "datetime";
                break;

             case DATETIME_TZ_LITERAL:
             case VAR_DATETIME_TZ:
             case FIELD_DATETIME_TZ:
             case FUNC_DATETIME_TZ:
             case ATTR_DATETIME_TZ:
             case METH_DATETIME_TZ:
             case OO_METH_DATETIME_TZ:
             case KW_DATE_TZ:
                jcls = "datetimetz";
                break;

             case VAR_COM_HANDLE:
             case FIELD_COM_HANDLE:
             case FUNC_COM_HANDLE:
             case ATTR_COM_HANDLE:
             case METH_COM_HANDLE:
             case OO_METH_COM_HANDLE:
             case KW_COM_HNDL:
                jcls  = "comhandle";
                break;

             case VAR_HANDLE:
             case FIELD_HANDLE:
             case FUNC_HANDLE:
             case ATTR_HANDLE:
             case METH_HANDLE:
             case OO_METH_HANDLE:
             case SYS_HANDLE:
             case KW_HANDLE:
             case KW_STRM_HND:
             case KW_WID_HAND:
                jcls  = "handle";
                break;
             case KW_TAB_HAND:
                jcls  = "TABLE-HANDLE";
                break;
             case KW_DSET_HND:
                jcls  = "DATASET-HANDLE";
                break;

             case VAR_RAW:
             case FIELD_RAW:
             case FUNC_RAW:
             case ATTR_RAW:
             case METH_RAW:
             case OO_METH_RAW:
             case KW_RAW:
                jcls  = "raw";
                break;

             case VAR_MEMPTR:
             case FUNC_MEMPTR:
             case ATTR_MEMPTR:
             case METH_MEMPTR:
             case OO_METH_MEMPTR:
             case KW_MEMPTR:
                jcls  = "memptr";
                break;

             case VAR_POLY:
             case FUNC_POLY:
             case ATTR_POLY:
             case METH_POLY:
             case OO_METH_POLY:
             case KW_POLY:
                jcls  = "Object";
                break;

             case METH_VOID:
             case OO_METH_VOID:
                jcls = "void";
                break;

             case KW_MENU:
             case KW_SUB_MENU:
             case KW_MENU_ITM:
             case KW_BROWSE:
             case WID_BROWSE:
             case WID_BUTTON:
             case WID_COMBO:
             case WID_DIALOG:
             case WID_EDITOR:
             case WID_FILL_IN:
             case WID_IMAGE:
             case WID_LITERAL:
             case WID_MENU:
             case WID_MENU_ITM:
             case WID_RADIO:
             case WID_RECT:
             case WID_SEL_LST:
             case WID_SLIDER:
             case WID_SUB_MENU:
             case WID_TEXT:
             case WID_TOGGLE:
             case WID_WINDOW:
                jcls = "GenericWidget";
                break;

             case KW_FRAME:
             case WID_FRAME:
                jcls = "CommonFrame";
                break;

             case KW_FIRST:
             case KW_LAST:
             case KW_NEXT:
             case KW_PREV:
             case KW_CURRENT:
             case RECORD_PHRASE:
             case AGGREGATE:
                // this is just there to ensure we don't return null, the actual
                // value is meaningless
                jcls = "bogus";
                break;

             case STREAM:
                jcls = "Stream";
                break;

             case TABLE:
             case BUFFER:
             case WORK_TABLE:
             case TEMP_TABLE:
             case KW_BUFFER:
             case KW_TABLE:
                jcls = "Buffer";
                break;

             case QUERY:
                jcls = "P2JQuery";
                break;

             case KW_DATASET:
                jcls = "DATASET";
                break;

             default:
                // check for known attributes
                Integer attrType = SignatureHelper.getAttributeType(type);
                if (attrType != null)
                {
                   // recursive call to match ATTR_XYZ
                   jcls = simpleExpressionType(attrType);
                }
                break;
          }

          return jcls;
       }

       /**
        * Returns {@code true} when the supplied class is declared as abstract.
        *
        * @param   clazz
        *          The class to check.
        *
        * @return  See above.
        */
       public static boolean isClassAbstract(Class clazz)
       {
          return Modifier.isAbstract(clazz.getModifiers());
       }

       /**
        * Returns Java class for the supplied legacy type.
        *
        * @param   type
        *          Legacy type name.
        *
        * @return  See above.
        */
       public static Class getTypeClass(String type)
       {
          switch (type)
          {
             case"BaseDataType":
                return BaseDataType.class;
             case "NumberType":
                return NumberType.class;
             case "Text":
                return Text.class;
             case "blob":
                return blob.class;
             case "character":
                return character.class;
             case "clob":
                return clob.class;
             case "comhandle":
                return comhandle.class;
             case "date":
                return date.class;
             case "datetime":
                return datetime.class;
             case "datetimetz":
                return datetimetz.class;
             case "decimal":
                return decimal.class;
             case "handle":
                return handle.class;
             case "int64":
                return int64.class;
             case "integer":
                return integer.class;
             case "jobject":
                return jobject.class;
             case "logical":
                return logical.class;
             case "longchar":
                return longchar.class;
             case "memptr":
                return memptr.class;
             case "object":
                return object.class;
             case "raw":
                return raw.class;
             case "recid":
                return recid.class;
             case "rowid":
                return rowid.class;
             case "unknown":
                return unknown.class;
             default:
                try
                {
                   return Class.forName(type);
                }
                catch (ClassNotFoundException e)
                {
                   throw new RuntimeException(e);
                }
          }
       }

       /**
        * Check the argument types for the MIN and MAX functions, so that the return value can be
        * computed.
        *
        * @param    child
        *           The first parameter.
        * @param    infer
        *           If <code>true</code> AND if the type is indeterminate (this typically only
        *           can occur for polymorphic return types), then the type will be inferred
        *           from the surrounding context (e.g. containing nodes) when possible.
        * @param    fuzzy
        *           Determines if "fuzzy" function signature matching is allowed. See {@link
        *           SignatureHelper#getSignature(int, int, String[], boolean)}.
        *
        * @return   The type name for the arguments.
        */
       private static String findMinMaxType(Aast child, boolean infer, boolean fuzzy)
       {
          String jcls = null;
          // these functions return a type based on the list of operands
          boolean foundInt = false;
          boolean foundInt64 = false;
          String forceCls = null;
          while (child != null)
          {
             String ajcls = expressionType(child, infer, fuzzy);

             if ((ajcls != null && !ajcls.equals("BaseDataType")) || !(foundInt || foundInt64))
             {
                // save the type only if the operand type was found or a previous number
                // type was not found
                jcls = ajcls;
             }

             if (ajcls == null || ajcls.equals("BaseDataType"))
             {
                return "BaseDataType";
             }

             // TODO: check that the following can't be used in MIN/MAX:
             //       class, com-handle, handle, memptr, raw, recid,
             //       rowid, blob (field), clob (field)

             // if we are integer or int64 we allow the loop to continue,
             // and if all items are the same then this will be returned
             // at the end OR otherwise it will be "promoted" to
             // decimal
             if ("character".equals(jcls)  ||
                 "longchar".equals(jcls)   ||
                 "logical".equals(jcls)    ||
                 "date".equals(jcls)       ||
                 "datetime".equals(jcls)   ||
                 "datetimetz".equals(jcls) ||
                 "decimal".equals(jcls))
             {
                // we have found the type so we save this, but still look for POLY
                forceCls = jcls;
             }
             else if ("unknown".equals(jcls))
             {
                if (forceCls == null)
                {
                   return jcls;
                }
             }
             else if ("integer".equals(jcls))
             {
                foundInt = true;
             }
             else if ("int64".equals(jcls))
             {
                foundInt64 = true;
             }

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

          return forceCls == null ? (foundInt64 ? "int64" : jcls) : forceCls;
       }

       /**
        * Provides the core user-functions exported to pattern engine rules for
        * Progress to Java expression conversion.
        */
       public class ExpressionHelper
       {
          /**
           * Get the string-representation of the given ASCII code.
           *
           * @param    code
           *           The ASCII code.
           *
           * @return   The associated char.
           */
          public String asString(int code)
          {
             return Character.toString((char) code);
          }

          /**
           * Get the ASCII-code representation of the given string.
           *
           * @param   str
           *          Character to be converted.
           *
           * @return   The string representation (base 10) of ASCII code of the requested char.
           */
          public String asciiCode(String str)
          {
             return String.valueOf(character.asc(parseProgressCharLiteral(str)).intValue());
          }

          /**
           * Exposes a helper method to convert Progress string literals into
           * the correct Java string literal using the static method
           * <code>character.progressToJavaString()</code>.
           *
           * @param    text
           *           A valid Progress string literal enclosed in double or
           *           single quotes and possibly with string options.
           *
           * @return   The corresponding Java string literal.
           */
          public String progressToJavaString(String text)
          {
             return progressToJavaString(text, false);
          }

          /**
           * Exposes a helper method to convert Progress string literals into
           * the correct Java string literal using the static method
           * <code>character.progressToJavaString()</code>.
           *
           * @param    text
           *           A valid Progress string literal enclosed in double or
           *           single quotes and possibly with string options.
           * @param    isPathName
           *           Boolean flag indicating the first parameter is the path string
           *           to be handled differently than usual string.
           *
           * @return   The corresponding Java string literal.
           */
          public String progressToJavaString(String text, boolean isPathName)
          {
             boolean windows = !Configuration.getParameter("unix-escapes", true);
             return character.progressToJavaString(text, windows, isPathName);
          }

          /**
           * Detects if the first and last characters of the string are 4GL quote
           * characters.
           *
           * @param    text
           *           The text to check.
           *
           * @return   <code>true</code> if this is a quoted string.
           */
          public boolean isQuoted(String text)
          {
             if (text == null || text.length() < 2)
             {
                return false;
             }

             int  last  = text.length() - 1;
             char lead  = text.charAt(0);
             char trail = text.charAt(last);

             return (lead == '\"' && trail == '\"') || (lead == '\'' && trail == '\'');
          }

          /**
           * Remove a matching pair first and last characters if they both exist and if they are one
           * of the 4GL quote characters.
           *
           * @param    text
           *           The text to modify.
           *
           * @return   The modified text.
           */
          public String stripQuotes(String text)
          {
             return StringHelper.stripEnclosing(text, QUOTES);
          }

          /**
           * Parses a Progress 4GL compatible string, by stripping off enclosing quotes/string options
           * and converting the contents (including rewriting escape sequences).
           * <p>
           * Escaped characters (using either the tilde or the backslash character) and special
           * formations will be converted to in-memory values and any escaped characters will be
           * replaced with that value.
           * <p>
           * The flow of processing of string literals:
           * <ol>
           *    <li><code>parseProgressCharLiteral(string)</code>. This method. Constructs an
           *          in-memory of the string exactly as the Progress lexer sees it. The specially
           *          escaped characters are expanded in memory as their ASCII values. Because the
           *          plain text, further processing is clean and correct.
           *    <li><code>convertToSQL___</code>. Optional. The set of methods will transform the
           *          previously read string into a pattern for SQL LIKE clause. These methods handles
           *          the input string using Progress matching rules. Wildcard are converted to SQL
           *          equivalents, escaping at the same time the native ones. They are aware of the
           *          unix-escapes. Optionally, if P2J decided to execute on server side, the string
           *          literal is SQL encoded.
           *    <li>Because the input string is not encoded, there will not be collisions with
           *          backspaces from escaped characters. Using Java encoding would need special
           *          cautions and can cause non-determinism. (Ex: to apply UPPERCASE, un
           *          'undo' operation have to be applied to convert the escaped characters back to
           *          lowercase)
           *    <li>until now, the string was not java encoded. To be able to write it to converted
           *          java source, the literal is {@link #encodeToJavaSource(String)}. This will
           *          add necessary escapes so that the string is a valid java string literal. The
           *          time the literal stays in this 'serialization' is rather short because the java
           *          source file is compiled after a successful conversion. It will then be loaded
           *          by the runtime and stored in memory.
           *    <li>at runtime, depending on the dialect used, after evaluating expressions, the
           *          string instances can be inlined into the query string, in which case it will be
           *          automatically SQL encoded in {@link com.goldencode.p2j.persist.FQLPreprocessor}.
           *          Otherwise, the (back) unencoded string is passed to query as parameter, in which
           *          case the SQL driver will automatically handle it.
           * </ol>
           * <p>
           * If the string literal do not need special processing at conversion time, it is safe to
           * convert it from progress encoding directly to java encoding using
           * {@link #progressToJavaString(String)}.
           *
           * @param   progress
           *          The string as read from Progress source file with possible Progress encodings.
           *
           * @return   an unencoded string. It must be escaped to be written as a java string literal.
           */
          public String parseProgressCharLiteral(String progress)
          {
             boolean windows = !Configuration.getParameter("unix-escapes", true);
             return character.parseProgressCharLiteral(progress, windows);
          }

          /**
           * Converts a runtime string to text representation of a java string literal.
           * The following characters are escaped:
           * <ul>
           *    <li>" and \ so they became: \" and respectively \\. The compiler (java lexer) will
           *       convert them back when the source file is compiled.
           *    <li>The whitespace characters that can be escaped in Java are escaped using \.
           *    <li> Characters outside the ASCII printable are encoded as octal representation.
           * </ul>
           *
           * @param   string
           *          The runtime string.
           *
           * @return  the string escaped, ready to be written as a java string literal.
           */
          public String encodeToJavaSource(String string)
          {
             return character.encodeToJavaSource(string);
          }

          /**
           * Exposes a helper method to convert a Java string literal into
           * a runtime Java string (by processing the text of all escape
           * sequences into the corresponding character).
           *
           * @param    text
           *           A valid Java string literal (not enclosed in double quotes).
           *
           * @return   The corresponding Java runtime string.
           */
          public String processEscapes(String text)
          {
             return StringHelper.processEscapes(text);
          }

          /**
           * Expands Progress format strings into the full text, explicit form
           * of the format string. For example, "x(8)" would be returned as
           * "xxxxxxxx".
           *
           * @param    fmt
           *           A valid Progress format string.
           *
           * @return   The expanded format string.
           */
          public String preprocessFormatString(String fmt)
          {
             return character.preprocessFormatString(fmt);
          }

          /**
           * Calculates the correct maximum size of the text output using the
           * given format string for the given data type.
           * <p>
           * The given format should not have had its escape sequences processed
           * into a runtime form.
           *
           * @param    node
           *           The expression or sub-expression node for which the format
           *           string is to be used.
           * @param    fmt
           *           A valid format string.
           * @param    java
           *           <code>false</code> if the given string is as encoded in
           *           the original source file (it should not have been
           *           converted to a Java string).  <code>true</code> if this
           *           format string is text that is valid for encoding in a
           *           Java source file.
           *
           * @return   The maximum number of characters that can be generated
           *           using this format.
           */
          public int formattedResultSize(Aast node, String fmt, boolean java)
          {
             if (!java)
             {
                fmt = progressToJavaString(fmt);
             }

             BaseDataType bdt = instanceOfBDT(expressionType(node));

             return bdt.formatLength(processEscapes(fmt));
          }

          /**
           * Create an instance of the BDT that is specified by the given class name. The
           * instance will be created using the default constructor which means that it will
           * represent the unknown value.
           *
           * @param    clsname
           *           The classname of a BDT subclass, in the proper case (generally all
           *           lowercase).
           *
           * @return   An instance of the given class or if <code>null</code> or not a valid
           *           BDT subclass, then an instance of the <code>unknown.class</code> will
           *           be returned.
           */
          public BaseDataType instanceOfBDT(String clsname)
          {
             BaseDataType bdt = null;

             if (clsname == null)
                clsname = "unknown";

             switch (clsname)
             {
                case "character":
                   bdt = new character();
                   break;
                case "longchar":
                   bdt = new longchar();
                   break;
                case "logical":
                   bdt = new logical();
                   break;
                case "date":
                   bdt = new date();
                   break;
                case "datetime":
                   bdt = new datetime();
                   break;
                case "datetimetz":
                   bdt = new datetimetz();
                   break;
                case "decimal":
                   bdt = new decimal();
                   break;
                case "integer":
                   bdt = new integer();
                   break;
                case "recid":
                   bdt = new recid();
                   break;
                case "rowid":
                   bdt = new rowid();
                   break;
                case "memptr":
                   bdt = new memptr();
                   break;
                case "raw":
                   bdt = new raw();
                   break;
                case "handle":
                   bdt = new int64();
                   break;
                default:
                   bdt = new unknown();
                   break;
             }

             return bdt;
          }

          /**
           * Exposes a helper method to convert Progress match patterns into regular expressions using
           * the static method {@link SQLHelper#convertToRegEx(String, boolean)}.
           *
           * @param   text
           *          A valid Progress match pattern. It uses Progress encoding (including quotes).
           *
           * @return  the corresponding regular expression, using java encoding.
           *
           * @see     #parseProgressCharLiteral(String)
           * @see     com.goldencode.p2j.util.SQLHelper#convertToRegEx(String, boolean)
           * @see     #encodeToJavaSource(String)
           */
          public String convertToRegEx(String text)
          {
             String inMemoryStr = parseProgressCharLiteral(text);

             // check how backslash should be handled (on windows it's a normal character, in unix
             // it's an escape character similar to tilde (~))
             boolean unixEscape = Configuration.getParameter("unix-escapes", true);

             return encodeToJavaSource(SQLHelper.convertToRegEx(inMemoryStr, !unixEscape));
          }

          /**
           * Exposes a helper method to convert Progress match patterns into SQL LIKE match
           * expressions using the static method <code>character.convertToSQLLike()</code>.
           *
           * @param   text
           *          A valid Progress match pattern.
           *
           * @return  The corresponding SQL LIKE match expression.
           *
           * @see     #parseProgressCharLiteral(String)
           */
          public String convertToSQLLike(String text)
          {
             // check how backslash should be handled (on windows it's a normal character, in unix
             // it's an escape character similar to tilde (~))
             boolean unixEscape = Configuration.getParameter("unix-escapes", true);

             // the result of this method is always an inlined SQL literal
             return SQLHelper.convertToSQLLike(text, !unixEscape, true);
          }

          /**
           * Exposes a helper method to convert a second operand of the "op1 BEGINS op2" function into
           * corresponding SQL LIKE expression using the static method
           * {@link com.goldencode.p2j.util.SQLHelper#convertToSQLBegins(String, boolean)}
           *
           * @param   text
           *          A second operand of the "op1 BEGINS op2" function.
           *
           * @return  The corresponding SQL LIKE expression.
           *
           * @see     #parseProgressCharLiteral(String)
           */
          public String convertToSQLBegins(String text)
          {
             // the result of this method is always an inlined SQL literal
             return SQLHelper.convertToSQLBegins(text, true);
          }

          /**
           * Exposes a helper method to convert a second operand of the "op1 CONTAINS op2" function
           * into corresponding SQL LIKE expression using the static method
           * {@link com.goldencode.p2j.util.SQLHelper#convertToSQLContains(String, boolean)}
           *
           * @param   text
           *          A second operand of the "op1 CONTAINS op2" function.
           *
           * @return  The corresponding SQL LIKE expression.
           *
           * @see     #parseProgressCharLiteral(String)
           */
          public String convertToSQLContains(String text)
          {
             return SQLHelper.convertToSQLContains(text, true);
          }

          /**
           * Exposes a helper method which allows to return the uppercased
           * version of the runtime or hardcoded string using the static method
           * {@link com.goldencode.p2j.util.TextOps#toUpperCase(String, boolean)}
           *
           * @param    text
           *           The string to convert
           *
           * @return   The uppercased string instance.
           */
          public String toUpperCase(String text)
          {
             // this method is only called at conversion time
             return TextOps.toUpperCase(text, false);
          }

          /**
           * Parses the supplied Progress string and returns {@code true} if the string contains the 'U' 4GL
           * string literal option, i.e. the string is untransatable.
           *
           * @param   progress
           *          Unprocessed progress string to test.
           *
           * @return  See above.
           */
          public boolean isUntranslatable(String progress)
          {
             return character.isUntranslatable(progress);
          }

          /**
           * Returns string content from the supplied Progress string. I.e. enclosing quotes and any 4GL string
           * literal options with the colon delimiter are not returned in the result.
           *
           * @param   progress
           *          Unprocessed progress string.
           *
           * @return  See above.
           */
          public String getStringContent(String progress)
          {
             return character.getStringContent(progress);
          }

          /**
           * Remove unnecessary characters and extra zero digits from input that represents
           * a valid date.  This compensates for "tricks" that can be done in the 4GL which
           * have no useful value (they don't store any data).
           *
           * @param    input
           *           The text to cleanup.
           *
           * @return   The cleaned up text.
           */
          public String cleanFormattedDate(String input)
          {
             return date.cleanFormattedDate(input);
          }

          /**
           * Converts a Progress 4GL style string which represents an integer or
           * a decimal into a Java style string representing the same number. There
           * are 2 objectives. First, in order for J2SE to parse this string into
           * a number, several incompatibilities from the 4GL input format must be
           * translated into their J2SE form. Otherwise, the J2SE code will throw
           * an exception and we will catastrophically fail. Second, all of the 4GL
           * error handling must be faithfully replicated. To do so means that this
           * method must scan the entire string character by character until an
           * error is found or there are no more characters to scan. In addition, the
           * same character can generate different errors (or no error at all)
           * depending on its position in the string. All of these arcane rules must
           * be faithfully implemented.
           * <p>
           * On input in the 4GL, only the following characters may be present:
           * <ul>
           *    <li> numeric digits '0' through '9'
           *    <li> the currently configured group separator (',' by default)
           *    <li> the currently configured decimal separator ('.' by default)
           *    <li> the sign characters '-' or '+' which may be prefixed or postfixed
           *         on the numeric portion
           *    <li> whitespace may appear anywhere however it can have an affect on
           *         the parsing
           *    <li> the asterisk character '*' may appear in certain cases
           * </ul>
           * <p>
           * During scanning, if any other character (than those noted above) is
           * encountered, it will cause an invalid character error if some other error
           * has not been found previously.
           * <p>
           * The parsing can be considered as having 3 phases:
           * <ol>
           *    <li> pre-number
           *    <li> number parsing
           *    <li> post-number
           * </ol>
           * <p>
           * Phase 1 (pre-number) ends when the first digit or "near digit" character
           * is encountered. The near digits are the group and decimal separators. In
           * the pre-number phase, there can be zero or one sign characters, zero or
           * more asterisks and any amount of whitespace. The whitespace can be
           * anywhere in this section and the amount of contiguous whitespace is not
           * limited. If there are more one asterisk, they may be separated by
           * whitespace however, all asterisks must occur on only one side of the
           * sign (not on both sides of any sign character). Once phase 1 is complete,
           * asterisks may no longer be present in the string.
           * <p>
           * Phase 2 (number parsing) is the only phase in which digits or near digits
           * may be present. In addition, no other characters besides digits and near
           * digits may be present in this phase. So there can be no whitespace, sign
           * characters or asterisks.  The first non-digit (or non-near-digit) ends
           * number parsing. There may be zero or more group separators in any
           * configuration in the number parsing phase. Group separators are simply
           * dropped and have no meaning other than being a near-digit (and thus
           * affecting parsing). There may be zero or one decimal separator in the
           * number parsing section. There may be any number of digits (so long as it
           * is not larger than can be converted into a valid integer or decimal). Leading
           * zeros (in the portion before the decimal point) and trailing zeros (in the
           * portion after the decimal point) are dropped.  This corresponds to the 4GL
           * behavior where an unlimited number of leading and trailing zeros can be
           * present and are ignored (since they have no effect on the value of the
           * number).
           * <p>
           * Phase 3 (post-number) extends after phase 2 to the end of the string. Any
           * amount of whitespace is allowed in this section. In addition, if there was
           * no sign character in the pre-number section, then one sign character is
           * allowed here (as a post-fixed sign). Any other character is invalid.
           * <p>
           * There is a special case where the number parsing phase is empty. This can
           * happen if there are no digits (or near-digits) on input.  If the first
           * sign character follows an asterisk, this is taken as the end of the
           * number parsing phase, even if there were no digits and/or there are digits
           * following the sign. If there were such following digits, they would be
           * in the post-number phase and would trigger an error. If there are no
           * digits on output (effectively the empty string or just a "-", this is
           * a valid string and will be converted to the number "0".
           * <p>
           * Translations:
           * <pre>
           *  Input Character                    Output Result
           * ------------------  --------------------------------------------------
           * digit 0             If a leading 0 to the left of the decimal point or if a
           *                     trailing 0 to the right of the decimal point, then this
           *                     zero is dropped.  Otherwise it is copied to the output
           *                     string.
           * digit (1 - 9)       Copied to output string.
           * group separator     Dropped.
           * decimal separator   1st is copied to output string. 2nd causes an error.
           * whitespace          Dropped.
           * + sign character    1st is dropped. 2nd causes one of 2 possible errors.
           * - sign character    1st is prefixed in the output string. 2nd causes one of 2 possible errors.
           * asterisks           Dropped.
           * </pre>
           * <p>
           * Sign characters are normalized (prefixed and postfixed '+' is removed,
           * '-'. The 2nd sign character will always cause an error, but the error
           * that occurs will depend on the parsing phase. A post-fixed sign is fine
           * so long as it is the first sign character encountered. But any sign
           * character (first or second) appearing in the post-number phase will
           * generate an invalid character error. Before the 3rd phase, the 1st sign
           * will be honored and the second sign character will raise a "more than
           * 1 sign" error. Note that for the purposes of counting sign characters,
           * the direction of the sign has no meaning (-+, --, ++, +- are all
           * equivalent errors).
           * <p>
           * The decimal separator is '.' and the group separator is ',' for this
           * method, which is sufficient for processing 4GL decimal literals.
           * <p>
           * Do not pass invalid input to this method.  It uses
           * <code>NumberType.parseDecimal()</code> as a worker, which expects the
           * runtime to be operational and will raise error conditions. This won't
           * work during conversion.
           *
           * @param    str
           *           The Progress style integer or decimal encoded as a string.
           *           If this is an empty string, then the value 0 will be returned
           *           (just like Progress).
           * @param    isInt
           *           <code>true</code> if the result of this function will be
           *           interpreted as an integer. Affects error handling.
           *
           * @return   A Java style string representing the same numeric value as
           *           original string, but suitable for parsing into a primitive
           *           <code>double</code> or into a <code>BigDecimal</code>.
           */
          public String cleanDecimal(String str, boolean isInt)
          {
             return NumberType.parseDecimal(str, '.', ',', isInt);
          }

          /**
           * This parses an unsigned hexadecimal whole number and returns a decimal instance that
           * represents the exact value.  This is needed because the Progress hexadecimal literal
           * is an unsigned 64-bit value and the Java hexadecimal literal is a signed 64-bit value.
           * By storing the value as a decimal instance, the full range of 4GL values can be
           * represented with fidelity.
           * <p>
           * Input format follows the 4GL hexadecimal literal rules:
           * <ol>
           *   <li> Optional prefixed minus sign.
           *   <li> The characters 0x or 0X.
           *   <li> Between 0 and 16 (inclusive) case-insensitive hexadecimal characters (0-9, A-F).
           *   <li> No whitespace or any other characters are allowed.
           *   <li> If no hexadecimal characters are provided, the value is 0.
           * </ol>
           *
           * @param   hex
           *          The hexadecimal string to be parsed. Must not be <code>null</code>. See above
           *          for the valid format.
           *
           * @return  The decimal value of the given 4GL hexadecimal literal.
           *
           * @throws   NullPointerException
           *           If <code>null</code> is passed.
           * @throws   NumberFormatException
           *           If the input string is invalid.
           */
          public decimal fromUnsignedHexLiteral(String hex)
          {
             return decimal.fromUnsignedHexLiteral(hex);
          }

          /**
           * Compares the given value to the largest and smallest possible hexadecimal literals that
           * can be directly represented as a Java hexadecimal literal. Since the Java hex literal is
           * a signed 64-bit value, very large positive and very large negative values cannot be
           * directly represented.  This method uses constants
           * <code>decimal.MIN_VALID_HEX_LITERAL</code> and
           * <code>decimal.MIAX_VALID_HEX_LITERAL</code> to define the range.
           *
           * @param    hex
           *           The hexadecimal string to be parsed.
           *
           * @return   <code>true</code> if the value is in range and can be represented as a Java
           *           literal.
           */
          public boolean isHexLiteralValid(String hex)
          {
             return ExpressionConversionWorker.isHexLiteralValid(hex);
          }

          /**
           * Evaluate the given expression by concatenating all the operands. If the resulted type as
           * reported by {@link #expressionType(Aast, boolean)} is not <code>character</code>, return
           * <code>null</code>.
           *
           * @param    expr
           *           The AST expression.
           *
           * @return   The resulted concatenated string or <code>null</code> if it can't be determined
           *           as a constant string.
           */
          public String evaluateCharacterExpression(Aast expr)
          {
             String etype = expressionType(expr, false);

             if (etype == null || !etype.equals("character"))
             {
                return null;
             }

             return ExpressionConversionWorker.evaluateCharacterExpression(expr);
          }

          /**
           * Walks the <code>ProgressAst</code> node specified and determines the
           * data type of the wrapper object returned by that sub-expression.  This
           * method processes the subtree recursively as needed to determine the
           * proper type.
           * <p>
           * At this time there is support for all basic data types (date,
           * character, integer, decimal, logical, recid, rowid, raw and memptr).
           * The special <code>FUNC_POLY</code> type is only supported for the
           * following built-in functions:
           * <pre>
           * ABSOLUTE
           * IF
           * INPUT
           * MAXIMUM
           * MINIMUM
           * </pre>
           * <p>
           * The INPUT built-in function normally returns the same type as the
           * single parameter (which must be an lvalue which is found in a frame
           * that is in scope).  However, this function can be used in an
           * expression that requires a <code>character</code> data type even in
           * the case where the parameter is NOT of the <code>character</code>
           * type!  This is like an override option and it must be detected from
           * the SURROUNDING expression (above the FUNC_POLY node). This support
           * is implemented except for one case: <b>the usage of INPUT as a
           * parameter to another built-in function (or presumably a method) which
           * expects a <code>character</code> parameter will also cause this
           * conversion. At this time there is no support in these cases. The
           * resulting type (if not already <code>character</code>) will cause the
           * wrong type to be emitted.</b>
           * <p>
           * Attributes and methods are fully supported except for the
           * <code>ATTR_POLY and METH_POLY</code> which are recognized but no
           * specific cases are currently handled.  In particular, the
           * <code>BUFFER-VALUE</code> is particularly difficult and has not been
           * resolved.
           *
           * @param    source
           *           The AST node to be inspected.
           *
           * @return   The classname of the wrapper type returned by this
           *           sub-expression.
           */
          public String expressionType(Aast source)
          {
             return ExpressionConversionWorker.expressionType(source, true);
          }

          /**
           * Walks the <code>ProgressAst</code> node specified and determines the
           * data type of the wrapper object returned by that sub-expression.  This
           * method processes the subtree recursively as needed to determine the
           * proper type.
           * <p>
           * At this time there is support for all basic data types (date,
           * character, integer, decimal, logical, recid, rowid, raw and memptr).
           * The special <code>FUNC_POLY</code> type is only supported for the
           * following built-in functions:
           * <pre>
           * ABSOLUTE
           * IF
           * INPUT
           * MAXIMUM
           * MINIMUM
           * </pre>
           * <p>
           * The INPUT built-in function normally returns the same type as the
           * single parameter (which must be an lvalue which is found in a frame
           * that is in scope).  However, this function can be used in an
           * expression that requires a <code>character</code> data type even in
           * the case where the parameter is NOT of the <code>character</code>
           * type!  This is like an override option and it must be detected from
           * the SURROUNDING expression (above the FUNC_POLY node). This support
           * is implemented except for one case: <b>the usage of INPUT as a
           * parameter to another built-in function (or presumably a method) which
           * expects a <code>character</code> parameter will also cause this
           * conversion. At this time there is no support in these cases. The
           * resulting type (if not already <code>character</code>) will cause the
           * wrong type to be emitted.</b>
           * <p>
           * Attributes and methods are fully supported except for the
           * <code>ATTR_POLY and METH_POLY</code> which are recognized but no
           * specific cases are currently handled.  In particular, the
           * <code>BUFFER-VALUE</code> is particularly difficult and has not been
           * resolved.
           *
           * @param    source
           *           The AST node to be inspected.
           * @param    infer
           *           If <code>true</code> AND if the type is indeterminate (this typically only
           *           can occur for polymorphic return types), then the type will be inferred
           *           from the surrounding context (e.g. containing nodes) when possible.
           *
           * @return   The classname of the wrapper type returned by this
           *           sub-expression.
           */
          public String expressionType(Aast source, boolean infer)
          {
             return ExpressionConversionWorker.expressionType(source, infer);
          }

          /**
           * Walks the <code>ProgressAst</code> node specified and determines if the expression
           * is a case-insensitive character expression.
           * This method is called when deciding if a P4GL CASE expression that will be converted to
           * a java switch will be evaluated as case sensitive or not.
           *
           * @param  ast
           *         The AST node to be inspected.
           *
           * @return <code>true</code> if the expression is NOT a character expression or it is a
           *         case-insensitive character expression and <code>false</code> if the expression
           *         is a case sensitive character expression.
           */
          public boolean isCaseInsensitive(Aast ast)
          {
             switch (ast.getType())
             {
                case EXPRESSION:
                   /* we need to go up the tree to the only child to investigate */
                   return isCaseInsensitive(ast.getChildAt(0));

                case VAR_CHAR:
                   /* check the definition of the variable. 'casesens' annotation is present only
                      on character variables and if present is always 'true' */
                   Long refId = (Long) ast.getAnnotation("refid");
                   Aast refAst = getResolver().getAst(refId);
                   return refAst == null || !(refAst.getAnnotation("casesens") != null &&
                                              (Boolean) refAst.getAnnotation("casesens"));

                case FIELD_CHAR:
                   /* annotation is present only on character fields
                      and set to 'FALSE' on character fields lacking the case-sensitive attribute */
                   return ast.getAnnotation("casesens") == null ||
                          ast.getAnnotation("casesens") == Boolean.FALSE;

                case FUNC_CHAR:
                   int oldtype = ((Long) ast.getAnnotation("oldtype")).intValue();
                   if (oldtype == KW_TRIM     || oldtype == KW_R_TRIM   || oldtype == KW_L_TRIM ||
                       oldtype == KW_CAPS     || oldtype == KW_BASE64_E || oldtype == KW_CP_CVT ||
                       oldtype == KW_ENCODE   || oldtype == KW_FILL     || oldtype == KW_GET_CODP ||
                       oldtype == KW_GET_COLL || oldtype == KW_KBLABEL  || oldtype == KW_KW ||
                       oldtype == KW_KW_ALL   || oldtype == KW_LC       || oldtype == KW_SEARCH ||
                       oldtype == KW_NORMALZE || oldtype == KW_OS_G_ENV || oldtype == KW_QUOTER ||
                       oldtype == KW_SUBSTR)
                   {
                      // check first parameter
                      return isCaseInsensitive(ast.getChildAt(0));
                   }

                   if (oldtype == KW_REPLACE)
                   {
                      // check if all parameters are case insensitive
                      return isCaseInsensitive(ast.getChildAt(0)) &&
                             isCaseInsensitive(ast.getChildAt(1)) &&
                             isCaseInsensitive(ast.getChildAt(2));
                   }

                   if (oldtype == KW_PDBNAME || oldtype == KW_SDBNAME || oldtype == KW_STRING)
                   {
                      Aast param = ast.getChildAt(0);
                      String paramType = expressionType(param);
                      if (paramType.equals("character") || paramType.equals("longchar"))
                      {
                         return isCaseInsensitive(param);
                      }
                      else
                      {
                         return true;
                      }
                   }

                   // all other character functions are case insensitive
                   return true;

                case PLUS:
                   /* all direct children must be case insensitive for this node to be case
                      insensitive */
                   for (int k = 0; k < ast.getNumImmediateChildren(); k++)
                   {
                      if (!isCaseInsensitive(ast.getChildAt(k)))
                      {
                         // if a child is found to be case-sensitive then the result is case-sensitive
                         return false;
                      }
                   }
                   return true;
                case DB_REF_NON_STATIC:
                   // character buffer::field is always case insensitive (even if field is originally defined as
                   // case-sensitive in the table definition). This doesn't apply to
                   // buffer:buffer-field(i):buffer-value().
                   return true;

                case STRING:
                case ATTR_CHAR:
                case METH_CHAR:
                case OO_METH_CHAR:
                case FILENAME:
                   // character expression, they are not case sensitive
                   return true;

                case COLON:
                   // variable:attribute case, we need to go to the right child to investigate
                   return isCaseInsensitive(ast.getChildAt(1));

                // TODO it is likely that there are other cases of character expression to be added.

                default:
                   // otherwise the expression is not a character expression
                   return false;
             }
          }

          /**
           * Analyzes the type of operands and infers if the two types are assignment
           * compatible.
           *
           * @param   ftype
           *          The type of the first operand.
           * @param   stype
           *          The type of the second operand.
           *
           * @return  <code>true</code> if the two types can be assigned interchangeably.
           */
          public boolean isAssignmentCompatible(String ftype, String stype)
          {
             return ExpressionConversionWorker.isAssignmentCompatible(ftype, stype);
          }

          /**
           * Lookup the parameter type at the specified index position for the built-in function
           * or method specified by given AST.
           *
           * @param    ast
           *           The parent AST of the parameter which needs its type resolved.
           * @param    idx
           *           The zero-based index position of the parameter to lookup.
           * @param    fuzzy
           *           Determines if "fuzzy" function signature matching is allowed. See {@link
           *           SignatureHelper#getSignature(int, int, String[], boolean)}.
           *
           * @return   The Java class name of the wrapper class for that parameter or
           *           null if there is no match found for any reason.
           */
          public String parameterTypeName(Aast ast, int idx, boolean fuzzy)
          {
             return ExpressionConversionWorker.parameterTypeName(ast, idx, fuzzy);
          }

          /**
           * Check if specified format string is compatible with type of
           * expression passed as a parameter.
           *
           * @param    source
           *           The expression to check compatibility with.
           * @param    format
           *           Format string.
           *
           * @return   <code>true</code> if expression and format string are
           *           compatible with each other.
           */
          public boolean isFormatCompatible(Aast source, String format)
          {
             try
             {
                String type = ExpressionConversionWorker.expressionType(source, true);

                // FMTCVT - this is a location where format strings are processed into Java
                //          strings; this should never be done more than once in a row
                //          before output to Java source because escaped values will be
                //          transformed incorrectly
                format = progressToJavaString(format);

                try
                {
                   switch (type)
                   {
                      case "character":
                         new StringFormat(format);
                         break;
                      case "int64":
                      case "integer":
                      case "decimal":
                         new NumberFormat(format);
                         break;
                      case "date":
                         new DateFormat(format);
                         break;
                      case "datetime":
                         DatetimeFormat.parse(format, false);
                         break;
                      case "datetimetz":
                         DatetimeFormat.parse(format, true);
                         break;
                      case "logical":
                         new BoolFormat(format);
                         break;
                   }

                   return true;
                }
                catch (Exception e)
                {
                   return false;
                }
             }
             catch (Exception e)
             {
                return false;
             }
          }

          /**
           * Walks the <code>ProgressAst</code> node specified and determines if
           * the given node is a valid part of an expression.
           *
           * @param    source
           *           The AST node to be inspected.
           *
           * @return   <code>true</code> if the given node is a valid expression
           *           or sub-expression node.  If <code>false</code>, then this
           *           node and its subtree cannot be processed as part of an
           *           expression.
           */
          public boolean isExpressionNode(Aast source)
          {
             String cls = null;

             try
             {
                cls = ExpressionConversionWorker.expressionType(source, true);
             }

             catch (UnsupportedOperationException uoe)
             {
                // let the cls be null
             }

             return (cls != null && cls.length() > 0);
          }

          /**
           * Returns {@code true} when the supplied class is declared as abstract.
           *
           * @param   clazz
           *          The class to check.
           *
           * @return  See above.
           */
          public boolean isClassAbstract(Class clazz)
          {
             return ExpressionConversionWorker.isClassAbstract(clazz);
          }

          /**
           * Returns Java class for the supplied legacy type.
           *
           * @param   type
           *          Legacy type name.
           *
           * @return  See above.
           */
          public Class getTypeClass(String type)
          {
             return ExpressionConversionWorker.getTypeClass(type);
          }

          /**
           * Detects and returns the compatibility class of a node. Nodes from same compatibility class can be
           * assigned/compared between them. An attempt to compare / assign nodes from different classes should
           * raise a compile (conversion) time error.
           *
           * @param   node
           *          The node whose class needs to be detected.
           *
           * @return  The compatibility class as a string.
           */
          public String getCompatibilityClass(Aast node)
          {
             String lcType = ExpressionConversionWorker.expressionType(node, true, false);
             if (lcType == null)
             {
                System.out.println("CompatibilityClass: [null] detected for " + node.dumpTree(true));
                return null;
             }

             lcType = lcType.toLowerCase();
             switch (lcType)
             {
                // isNumericType(String cls)
                case "numbertype":
                case "integer":
                case "recid":
                case "int64":
                case "decimal":
                   return "numbertype";

                // isDateType(String cls)
                case "date":
                case "datetime":
                case "datetimetz":
                   return "date";

                // isTextType(String cls)
                case "text":
                case "character":
                case "longchar":
                case "clob":
                   return "text";

                // isBinaryType(String cls)
                case "memptr":
                case "blob":
                case "raw":
                case "binarytype":
                   return "binarytype";

                // handle-related
                case "handle":
                case "comhandle":
                case "wrappedresource":
                   return "handle";

                default:
                   // objects
                   if (lcType.startsWith("object"))
                   {
                      return "object";
                   }

                   // others
                   return lcType;
             }
          }

          /**
           * Check if the operand represents any of the valid numeric classes.
           *
           * @param    cls
           *           The classname to test.
           *
           * @return   <code>true</code> if the classname is <code>integer</code>,
           *           <code>int64</code>, <code>decimal</code>, <code>recid</code> or
           *           <code>NumberType</code>.
           */
          public boolean isNumericType(String cls)
          {
             return ExpressionConversionWorker.isNumericType(cls);
          }

          /**
           * Check if the operand represents any of the valid date classes.
           *
           * @param    cls
           *           The classname to test.
           *
           * @return   <code>true</code> if the classname is <code>date</code>,
           *           <code>datetime</code> or <code>datetimetz</code>.
           */
          public boolean isDateType(String cls)
          {
             return ExpressionConversionWorker.isDateType(cls);
          }

          /**
           * Check if the given block is an internal top-level block (not external program).
           *
           * @param    ast
           *           The AST to check,
           *
           * @return   Returns true if this is an internal procedure, trigger, function, method, constructor, or
           *           destructor.
           *           Returns false for external procedure, a class or interface body.
           */
          public boolean isTopLevelInternal(Aast ast)
          {
             return ast.getType() == BLOCK && ast.getParent() != null && findTopLevel(ast) == ast.getParent();
          }
       }

       /**
        * Context-local work area.
        */
       private static class WorkArea
       {
          /** Nodes for which context analysis is active inside a *_POLY node. */
          private final Set<Long> recursionIds = new HashSet<>();

          /** Flag that determines if implicit INPUT node is currently being analyzed. */
          private boolean inImplicitInput = false;
       }
    }