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