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