Call.java

/*
** Module   : Call.java
** Abstract : The implementation of a CALL resource.
**
** Copyright (c) 2018-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------------Description----------------------------------------
** 001 CA  20181029 Created initial version.
** 002 CA  20181220 Changed setParameter to have int64 as first argument.
** 003 CA  20190219 Some minor fixes.
** 004 IAS 20190501 Added SECURITY-POLICY support.
**         20190506 Added AUDIT-POLICY, AUDIT-CONTROL support.
** 005 CA  20190628 Changes caused by moving InvokeConfig.invoke to ControlFlowOps.
** 006 OM  20190523 Unified parameter options handling for TempTables and DataSet parameters.
** 007 CA  20190710 Extracted inner classes to their own top-level type, so they can be reused by OO
**                  ParameterList.
** 008 CA  20200122 Javadoc fixes.
** 009 ME  20200810 Improvements in parameter processing including error handling and delayed validation for
**                  table/dataset handle input parameters. 
**     CA  20200924 Replaced Method.invoke with ReflectASM.
**     SVL 20201009 Reflected changes in FieldReference.
**     ME  20201009 Keep initial value for input/input-output parameters, 4GL uses the value at the time
**                  setParameter was called although the variable might mutate before invoke.
**     CA  20210216 Fixed an issue when setParameter passes an unknown literal as the value argument.
**     OM  20210309 Removed double display of error message number.
**     ME  20210507 Improve error handling for POLY conversion, OO parameter validation.
**     CA  20210525 Fixed a problem with Progress.Lang.Object arguments.
**     CA  20210609 Fixed OO property references used as arguments.
**                  Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                  initialized at the method's execution, and not at the caller's arguments.
**     CA  20220120 Do not used TypeFactory.object when the OO reference must not be tracked or registered.
**     ME  20220214 Fix variable class type check for block's inline variables.
**     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
**                  allow caching of the resolved target.
**     CA  20221006 UNIQUE-ID is kept as Java type instead of BDT.  Refs #6827
**     OM  20221124 Added special processing for buffer-fields passed as argument via
**                  CALL:SET-PARAMETER or to Progress.Lang.ParameterList:SetParameter.
**     OM  20221216 Reworked table and dataset handle parameters.
**     CA  20230104 Track if an output parameter is associated with a table field, so batch processing is done
**                  only in these cases, for output parameter assignment.
**     CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 011 CA  20230724 Further reduce context-local usage.
** 012 CA  20230802 Reset the OutputParameterAssigner after invoke finishes, as the parameters are copied 
**                  explicitly, without the OutputParameterAssigner support.
** 013 CA  20231031 Fixed unknown literal parameter.
** 014 CA  20231107 Fixed string representation of the call mode, which must not include any option.
** 015 CA  20240215 Fixed issues using CALL property/field references as arguments, associated with a 
**                  DATASET/TABLE-HANDLE parameter. 
** 016 CA  20240708 Allow RETURN ERROR to propagate to the caller for Progress.Lang.Class:Invoke and CALL:Invoke.
** 017 PBB 20250509 Added validation for SetParameter method when parameter is an extent.
** 018 PBB 20250522 Fixed part of the behavior related to SetParameter object method when giving a
**                  DYNAMIC-PROPERTY call as argument.
** 019 ES  20250522 Added setterAssignment method that validates and assign the value to variable instance.
*/

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

import java.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.library.*;
import com.goldencode.p2j.oo.lang._BaseObject_;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.util.UniqueIdGenerator.IdKind;
import com.goldencode.p2j.util.logging.*;

/**
 * Defines a CALL resources.
 */
public class Call
extends HandleChain
implements CallResource
{
   /** Constant mapping the legacy PROCEDURE-CALL-TYPE value. */
   public static final int PROCEDURE_CALL_TYPE = 1;
   
   /** Constant mapping the legacy FUNCTION-CALL-TYPE value. */
   public static final int FUNCTION_CALL_TYPE = 2;
   
   /** Constant mapping the legacy GET-ATTR-CALL-TYPE value. */
   public static final int GET_ATTR_CALL_TYPE = 3;
   
   /** Constant mapping the legacy SET-ATTR-CALL-TYPE value. */
   public static final int SET_ATTR_CALL_TYPE = 4;
   
   /** Constant mapping the legacy DLL-CALL-TYPE value. */
   public static final int DLL_CALL_TYPE = 5;

   /** Logger */
   static final CentralLogger LOG = CentralLogger.get(Call.class.getName());

   /** The call-site associated with the call's invocation. */
   private static final InvokeConfig CALL_SITE = new InvokeConfig();
   
   /** The first value of valid call types. */
   private static final int BEGIN_CALL_TYPE = PROCEDURE_CALL_TYPE;

   /** The last value of valid call types. */
   private static final int END_CALL_TYPE = DLL_CALL_TYPE;
   
   /** A cache of attributes and methods token types to their actual type. */
   private static final Map<Integer, Integer> ATTRIBUTES_AND_METHODS = 
                                              ProgressParser.getAttributesAndMethods();
   
   /** A cache of system handle keywords. */
   private static final Set<String> SYS_HANDLES = ProgressParser.getSysHandles();
   
   /** A cache of data types. */
   private static final Set<String> DATA_TYPES = new HashSet<>();
   
   private static Pattern dataTypePattern = Pattern.compile("^(CLASS\\s+)?([\\w|\\.]+)(\\s+EXTENT(\\s+\\d+)?)?$", Pattern.CASE_INSENSITIVE);
   
   static
   {
      DATA_TYPES.add("BLOB");
      DATA_TYPES.add("CHARACTER");
      DATA_TYPES.add("CLOB");
      DATA_TYPES.add("DATASET-HANDLE");
      DATA_TYPES.add("DATE");
      DATA_TYPES.add("DATETIME");
      DATA_TYPES.add("DATETIME-TZ");
      DATA_TYPES.add("DECIMAL");
      DATA_TYPES.add("HANDLE");
      DATA_TYPES.add("INT64");
      DATA_TYPES.add("INTEGER");
      DATA_TYPES.add("LOGICAL");
      DATA_TYPES.add("LONGCHAR");
      DATA_TYPES.add("MEMPTR");
      DATA_TYPES.add("RAW");
      DATA_TYPES.add("RECID");
      DATA_TYPES.add("ROWID");
      DATA_TYPES.add("TABLE-HANDLE");
   }
   
   /** The value of the ADM-DATA attribute. */
   private String admData = null;
   
   /** Corresponds to UNIQUE-ID attribute. */
   private Long uniqueID = null;
   
   /** Flag indicating if this resource has been deleted or not. */
   private boolean deleted = false;
   
   /** The value of the CALL-NAME attribute. */
   private String callName;
   
   /** The value of the CALL-TYPE attribute. */
   private int callType;
   
   /** The value of the IN-HANDLE attribute. */
   private handle inHandle;
   
   /** The value of the NUM-PARAMETERS attribute. */
   private int numParameters;
   
   /** The value of the RETURN-VALUE attribute. */
   private BaseDataType returnValue;
   
   /** The value of the RETURN-VALUE-DATA-TYPE attribute. */
   private String returnValueDataType;
   
   /** The value of the SERVER attribute. */
   private handle server;
   
   /** The value of the PERSISTENT attribute. */
   private boolean persistent;
   
   /** Flag indicating if this dynamic run will be emulated in SINGLE-RUN mode. */
   private boolean singleRun;
   
   /** Flag indicating if this dynamic run will be emulated in SINGLETON mode. */
   private boolean singleton;
   
   /** The value of the PROCEDURE-TYPE attribute. */
   private String procedureType;
   
   /** The value of the THREAD-SAFE attribute. */
   private boolean threadSafe;
   
   /** The value of the ASYNCHRONOUS attribute. */
   private boolean async;

   /** The value of the ASYNC-REQUEST-HANDLE attribute. */
   private handle asyncHandle;
   
   /** The value of the EVENT-PROCEDURE attribute. */
   private String eventProcedure;
   
   /** The value of the EVENT-PROCEDURE-CONTEXT attribute. */
   private handle eventProcedureContext;
   
   /** The value of the ORDINAL attribute. */
   private Integer ordinal;
   
   /** The value of the LIBRARY attribute. */
   private String library;
   
   /** The value of the RETURN-VALUE-DLL-TYPE attribute. */
   private String returnValueDllType;
   
   /** The value of the LIBRARY-CALLING-CONVENTION attribute. */
   private String libraryCallingConvention;
   
   /** 
    * The list of configured parameters.  Created when {@link #setNumParameters NUM-PARAMETERS} 
    * attribute is set.
    */
   private CallParameter[] parameters;
   
   /**
    * Create a new resource and {@link #clear() initialize} it with the default values.
    */
   private Call()
   {
      super(true, false);
      clear();
   }
   
   /**
    * Create a new resource and assign it to the specified handle.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    */
   public static void create(handle h)
   {
      create(h, (character) null);
   }

   /**
    * Create a new resource and assign it to the specified handle.
    * <p>
    * If a widget pool is not specified, then the resource will be added to the SESSION pool, and
    * not the closest unnamed pool.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    * @param    widgetPool
    *           The named widget pool where to save the widget. If <code>null</code>, the SESSION
    *           widget pool will be used.
    */
   public static void create(handle h, character widgetPool)
   {
      // validate widget pool before anything else
      // unnamed pools are not used here, if one not specified, the SESSION pool will be used
      if (widgetPool != null && !WidgetPool.validWidgetPool(widgetPool))
      {
         return;
      }
      
      Call c = new Call();
      h.assign(c);
      
      if (widgetPool != null)
      {
         c.addToPool(widgetPool);
      }
   }

   /**
    * Create a new resource and assign it to the specified handle.
    * <p>
    * If a widget pool is not specified, then the resource will be added to the SESSION pool, and
    * not the closest unnamed pool.
    * 
    * @param    h
    *           The handle where to save the resource.  Must be not-null.
    * @param    widgetPool
    *           The named widget pool where to save the widget. If <code>null</code>, the SESSION
    *           widget pool will be used.
    */
   public static void create(handle h, String widgetPool)
   {
      create(h, new character(widgetPool));
   }
   
   /**
    * Used for wrapping the last parameter of {@code CALL:SET-PARAMETER} method in order for the method to
    * properly detect whether it is a valid output parameter. In affirmative case, this static method will
    * return a {@code BaseDataTypeVariable}. Otherwise, the original object is returned.
    * <p>
    * This wrapper is injected only when the last parameter (the value) of {@code SET-PARAMETER} is detected
    * at conversion to be a {@code BUFFER-VALUE()} result.
    *
    * @param   aHandle
    *          A handle, possible of a buffer-field.
    *
    * @return  If the handle is indeed a valid BUFFER-FIELD, a custom-made {@code BaseDataTypeVariable} is
    *          created and returned. Otherwise, the original code is restored.
    */
   public static Object valueReference(handle aHandle)
   {
      if (aHandle.isType(LegacyResource.BUFFER_FIELD))
      {
         BufferFieldImpl field = (BufferFieldImpl) aHandle.getResource();
         return field.createFieldReference(null);
      }
      
      return aHandle.unwrapBufferField().value();
   }
   
   /**
    * Used for wrapping the last parameter of {@code CALL:SET-PARAMETER} method in order for the method to
    * properly detect whether it is a valid output parameter. In affirmative case, this static method will
    * return a {@code BaseDataTypeVariable}. Otherwise, the original object is returned.
    * <p>
    * This wrapper is injected only when the last parameter (the value) of {@code SET-PARAMETER} is detected
    * at conversion to be a {@code BUFFER-VALUE(k)} result.
    *
    * @param   aHandle
    *          A handle, possible of a buffer-field.
    * @param   extIndex
    *          The index for a possible extent field.
    *
    * @return  If the handle is indeed a valid BUFFER-FIELD, a custom-made {@code BaseDataTypeVariable} is
    *          created and returned. Otherwise, the original code is restored.
    */
   public static Object valueReference(handle aHandle, Integer extIndex)
   {
      if (aHandle.isType(LegacyResource.BUFFER_FIELD))
      {
         BufferFieldImpl field = (BufferFieldImpl) aHandle.getResource();
         return field.createFieldReference(extIndex);
      }
      
      return aHandle.unwrapBufferField().value(extIndex);
   }
   
   /**
    * Used for wrapping the last parameter of {@code CALL:SET-PARAMETER} method in order for the method to
    * properly detect whether it is a valid output parameter. In affirmative case, this static method will
    * return a {@code BaseDataTypeVariable}. Otherwise, the original object is returned.
    * <p>
    * This wrapper is injected only when the last parameter (the value) of {@code SET-PARAMETER} is detected
    * at conversion to be a {@code BUFFER-VALUE(k)} result.
    * 
    * @param   aHandle
    *          A handle, possible of a buffer-field.
    * @param   k
    *          The index for a possible extent field.
    *
    * @return  If the handle is indeed a valid BUFFER-FIELD, a custom-made {@code BaseDataTypeVariable} is
    *          created and returned. Otherwise, the original code is restored.
    */
   public static Object valueReference(handle aHandle, NumberType k)
   {
      return valueReference(aHandle, k.isUnknown() ? null : k.intValue());
   }
   
   /**
    * Get the value of the ADM-DATA attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getADMData()
   {
      return new character(admData);
   }

   /**
    * Set the value of the ADM-DATA attribute.
    * 
    * @param    value
    *           The new value.
    */
   @Override
   public void setADMData(String value)
   {
      this.admData = value;
   }

   /**
    * Set the value of the ADM-DATA attribute.
    * 
    * @param    value
    *           The new value.
    */
   @Override
   public void setADMData(character value)
   {
      this.admData = value.getValue();
   }

   /**
    * Gets the the unique ID number associated to this object by the
    * underlying system. Not the same as the handle value itself.
    * 
    * @return   The integer number of the children.
    */
   @Override
   public integer getUniqueID()
   {
      if (uniqueID == null)
      {
         uniqueID = UniqueIdGenerator.getUniqueId(IdKind.CALL);
      }
      
      return new integer(uniqueID);
   }

   /**
    * Reports if this object is valid for use.  
    *
    * @return   <code>true</code> if we are valid (can be used).
    */
   @Override
   public boolean valid()
   {
      return !deleted;
   }

   /**
    * Get the <code>persistent</code> attribute of this procedure instance.
    * 
    * @return   See above.
    */
   @Override
   public logical isPersistent()
   {
      return new logical(persistent);
   }

   /**
    * Set the <code>persistent</code> attribute of this CALL request.
    * 
    * @param    persistent
    *           The flag indicating how the call will be performed.
    */
   @Override
   public void setPersistent(boolean persistent)
   {
      if (singleRun || singleton)
      {
         int[] nums = { 17255, 3131 };
         String[] texts = 
         {
            "Cannot set PERSISTENT attribute to TRUE when PROCEDURE-TYPE attribute has " +
            "already been set to " + procedureType + " (17255)",
            "Unable to set attribute PERSISTENT in widget  of type CALL. (3131)"
         };

         ErrorManager.recordOrThrowError(nums, texts, false, true);
         return;
      }
      
      this.persistent = persistent;
      this.procedureType = "PERSISTENT";
   }

   /**
    * Set the <code>persistent</code> attribute of this CALL request.
    * 
    * @param    persistent
    *           The flag indicating how the call will be performed.
    */
   @Override
   public void setPersistent(logical persistent)
   {
      if (persistent.isUnknown())
      {
         unknownWarning("PERSISTENT");
         return;
      }
      
      setPersistent(persistent.booleanValue());
   }

   /**
    * Returns a handle to the app server in which the procedure is ran.
    * 
    * @return   See above
    */
   @Override
   public handle getServerHandle()
   {
      return server == null ? new handle() : new handle(server);
   }

   /**
    * Sets a handle for the app server in which the procedure will be ran.
    * 
    * @param    h
    *           The server handle.
    */
   @Override
   public void setServerHandle(handle h)
   {
      if (h.isUnknown())
      {
         unknownWarning("SERVER");
         return;
      }
      
      server = new handle(h);
   }

   /**
    * Get the <code>CALL-NAME</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getCallName()
   {
      return callName == null ? new character() : new character(callName);
   }

   /**
    * Set the <code>CALL-NAME</code> attribute of this CALL resource.
    * 
    * @param    callName
    *           The target's name (attribute, method, RUN's invoke target, native API).
    */
   @Override
   public void setCallName(character callName)
   {
      if (callName.isUnknown())
      {
         unknownWarning("CALL-NAME");
         return;
      }
      
      setCallName(callName.getValue());
   }

   /**
    * Set the <code>CALL-NAME</code> attribute of this CALL resource.
    * 
    * @param    callName
    *           The target's name (attribute, method, RUN's invoke target, native API).
    */
   @Override
   public void setCallName(String callName)
   {
      if (ordinal != null)
      {
         callOrdinalError("CALL-NAME");
         return;
      }
      
      this.callName = callName;
   }

   /**
    * Get the <code>CALL-TYPE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public integer getCallType()
   {
      return new integer(callType);
   }

   /**
    * Set the <code>CALL-TYPE</code> attribute of this CALL resource.
    * 
    * @param    callType
    *           One of the call-type constant values.
    */
   @Override
   public void setCallType(NumberType callType)
   {
      if (callType.isUnknown())
      {
         unknownWarning("CALL-TYPE");
         return;
      }
      
      setCallType(callType.intValue());
   }

   /**
    * Set the <code>CALL-TYPE</code> attribute of this CALL resource.
    * 
    * @param    callType
    *           One of the call-type constant values.
    */
   @Override
   public void setCallType(int callType)
   {
      if (!(callType >= BEGIN_CALL_TYPE && callType <= END_CALL_TYPE))
      {
         unableToSetAttribute(10066,
                              "CALL object CALL-TYPE must be an integer between 1 and 5",
                              "CALL-TYPE");
         return;
      }
      
      this.callType = callType;
   }

   /**
    * Check if the specified parameter is set.
    * 
    * @param    paramNum
    *           The 1-based parameter index.
    *           
    * @return   <code>true</code> if the parameter is set.
    */
   @Override
   public logical isParameterSet(NumberType paramNum)
   {
      if (paramNum.isUnknown())
      {
         // no warning is shown
         return new logical();
      }
      
      return isParameterSet(paramNum.intValue());
   }

   /**
    * Check if the specified parameter is set.
    * 
    * @param    paramNum
    *           The 1-based parameter index.
    *           
    * @return   <code>true</code> if the parameter is set.
    */
   @Override
   public logical isParameterSet(int paramNum)
   {
      if (parameters == null)
      {
         // no warning is shown
         return new logical();
      }

      if (paramNum < 1 || paramNum > numParameters)
      {
         // no warning is shown
         return new logical();
      }
      
      return new logical(parameters[paramNum - 1] != null);
   }

   /**
    * Get the <code>NUM-PARAMETERS</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public integer getNumParameters()
   {
      return new integer(numParameters);
   }

   /**
    * Set the <code>NUM-PARAMETERS</code> attribute of this CALL resource.
    * 
    * @param    num
    *           The number of parameters.
    */
   @Override
   public void setNumParameters(NumberType num)
   {
      if (num.isUnknown() || num.intValue() < 1)
      {
         if (num.isUnknown())
         {
            unknownWarning("NUM-PARAMETERS");
         }
         
         parameters = null;
         
         if (!num.isUnknown())
         {
            numParameters = num.intValue();
         }

         return;
      }
   
      numParameters = num.intValue();
      parameters = new CallParameter[numParameters];
   }

   /**
    * Set the <code>NUM-PARAMETERS</code> attribute of this CALL resource.
    * 
    * @param    num
    *           The number of parameters.
    */
   @Override
   public void setNumParameters(long num)
   {
      setNumParameters(new integer(num));
   }

   /**
    * Get the <code>IN-HANDLE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public handle getInHandle()
   {
      return inHandle == null ? new handle() : new handle(inHandle);
   }

   /**
    * Set the <code>IN-HANDLE</code> attribute of this CALL resource.
    * 
    * @param    h
    *           The external program where the target function/procedure will be invoked or the
    *           resource for which the attribute/method will be accessed.
    */
   @Override
   public void setInHandle(handle h)
   {
      if (h.isUnknown())
      {
         unknownWarning("IN-HANDLE");
         return;
      }
      
      inHandle = new handle(h);
   }

   /**
    * Set the <code>IN-HANDLE</code> attribute of this CALL resource.
    * 
    * @param    resource
    *           The external program where the target function/procedure will be invoked or the
    *           resource for which the attribute/method will be accessed.
    */
   @Override
   public void setInHandle(character resource)
   {
      setInHandle(resource.isUnknown() ? null : resource.getValue());
   }
   
   /**
    * Set the <code>IN-HANDLE</code> attribute of this CALL resource.
    * 
    * @param    resource
    *           The external program where the target function/procedure will be invoked or the
    *           resource for which the attribute/method will be accessed.
    */
   @Override
   public void setInHandle(String resource)
   {
      boolean valid = (resource != null);
      
      if (!valid)
      {
         unknownWarning("IN-HANDLE");
         return;
      }
      else
      {
         // the IN-HANDLE is reset now
         inHandle = null;

         String tokenText = ProgressAst._keyword(resource);
         
         valid = (tokenText != null && SYS_HANDLES.contains(tokenText));
         
         if (valid)
         {
            int tokenType = ProgressAst.lookupKeywordToken(resource);

            switch (tokenType)
            {
               case ProgressParserTokenTypes.KW_ACT_WIN:
                  inHandle = LogicalTerminal.activeWindow();
                  break;
               case ProgressParserTokenTypes.KW_AUD_CTRL:
                  inHandle = AuditControlManager.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_AUD_POL:
                  inHandle = AuditPolicyManager.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_CLIP:
                  inHandle = ClipboardManager.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_CODEBASE:
                  // TODO: assign this after FWD supports this resource
                  break;
               case ProgressParserTokenTypes.KW_COLR_TAB:
                  inHandle = ColorTable.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_COM_SELF:
                  // TODO: assign this after FWD supports this resource
                  break;
               case ProgressParserTokenTypes.KW_COMPILER:
                  // TODO: assign this after FWD supports this resource
                  break;
               case ProgressParserTokenTypes.KW_CUR_WIN:
                  inHandle = LogicalTerminal.currentWindow();
                  break;
               case ProgressParserTokenTypes.KW_DEBUGGER:
                  // TODO: assign this after FWD supports this resource
                  break;
               case ProgressParserTokenTypes.KW_DEF_WIN:
                  inHandle = LogicalTerminal.defaultWindow();
                  break;
               case ProgressParserTokenTypes.KW_DSL_MGR:
                  inHandle = LegacyLogOps.dsLogMgrHandle();
                  break;
               case ProgressParserTokenTypes.KW_ERR_STAT:
                  inHandle = ErrorManager.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_FIL_INFO:
                  inHandle = FileSystemOps.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_FOCUS:
                  inHandle = LogicalTerminal.focus();
                  break;
               case ProgressParserTokenTypes.KW_FONT_TAB:
                  inHandle = FontTable.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_LAST_EVT:
                  inHandle = KeyReader.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_LOG_MGR:
                  inHandle = LegacyLogOps.logMgrHandle();
                  break;
               case ProgressParserTokenTypes.KW_PROFILER:
                  inHandle = ProfilerUtils.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_RCOD_INF:
                  inHandle = RcodeInfoOps.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_SECUR_P:
                   inHandle = SecurityPolicyManager.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_SELF:
                  inHandle = LogicalTerminal._self();
                  break;
               case ProgressParserTokenTypes.KW_SESSION:
                  inHandle = SessionUtils.asHandle();
                  break;
               case ProgressParserTokenTypes.KW_SRC_PROC:
                  inHandle = ProcedureManager.sourceProcedure();
                  break;
               case ProgressParserTokenTypes.KW_TAR_PROC:
                  inHandle = ProcedureManager.targetProcedure();
                  break;
               case ProgressParserTokenTypes.KW_THIS_PRC:
                  inHandle = ProcedureManager.thisProcedure();
                  break;
               case ProgressParserTokenTypes.KW_WEB_CTX:
                  // TODO: assign this after FWD supports this resource
                  break;
            }
            
            valid = (inHandle != null);
         }
      }
      
      if (!valid)
      {
         int[] nums = new int[] { 10064, 3131 };
         String[] texts = new String[]
         {
            "Invalid character system handle expression  given to IN-HANDLE (must be a system " +
            "handle name like SESSION, FILE-INFO, etc.)",
            "Unable to set attribute IN-HANDLE in widget  of type CALL"
         };
         ErrorManager.recordOrThrowError(nums, texts, false);
         
         return;
      }
   }
   
   /**
    * Get the <code>PROCEDURE-TYPE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getProcedureType()
   {
      return procedureType == null ? new character() : new character(procedureType);
   }

   /**
    * Set the <code>PROCEDURE-TYPE</code> attribute of this CALL resource.
    * 
    * @param    procType
    *           The procedure's type, one of PERSISTENT, SINGLE-RUN or SINGLETON strings.
    */
   @Override
   public void setProcedureType(character procType)
   {
      if (procType.isUnknown())
      {
         // no error is raised
         procType = null;
         return;
      }
      
      setProcedureType(procType.getValue());
   }

   /**
    * Set the <code>PROCEDURE-TYPE</code> attribute of this CALL resource.
    * 
    * @param    procType
    *           The procedure's type, one of PERSISTENT, SINGLE-RUN or SINGLETON strings.
    */
   @Override
   public void setProcedureType(String procType)
   {
      String stype = procType.toUpperCase();
      if (!("PERSISTENT".equals(stype) || "SINGLE-RUN".equals(stype) || "SINGLETON".equals(stype)))
      {
         // Unable to set PROCEDURE-TYPE CALL attribute (15482)
         ErrorManager.recordOrShowError(15482, 
                                        "Unable to set PROCEDURE-TYPE CALL attribute (15482)", 
                                        false, false, false, true);
         return;
      }
      
      if (persistent && !"PERSISTENT".equals(stype))
      {
         int[] nums = { 17254, 3131 };
         String[] texts = 
         {
            "Cannot set PROCEDURE-TYPE attribute to " + stype + " when PERSISTENT attribute " +
            "has already been set to TRUE (17254)",
            "Unable to set attribute PROCEDURE-TYPE in widget  of type CALL. (3131)"
         };

         ErrorManager.recordOrThrowError(nums, texts, false, true);
         return;
      }
      
      this.procedureType = stype;
      this.persistent = "PERSISTENT".equals(stype);
      this.singleRun = "SINGLE-RUN".equals(stype);
      this.singleton = "SINGLETON".equals(stype);
   }

   /**
    * Get the <code>THREAD-SAFE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public logical isThreadSafe()
   {
      return new logical(threadSafe);
   }

   /**
    * Set the <code>THREAD-SAFE</code> attribute of this CALL resource.
    * 
    * @param    threadSafe
    *           The new state of this flag.
    */
   @Override
   public void setThreadSafe(logical threadSafe)
   {
      if (threadSafe.isUnknown())
      {
         unknownWarning("THREAD-SAFE");
         return;
      }
      
      setThreadSafe(threadSafe.booleanValue());
   }

   /**
    * Set the <code>THREAD-SAFE</code> attribute of this CALL resource.
    * 
    * @param    threadSafe
    *           The new state of this flag.
    */
   @Override
   public void setThreadSafe(boolean threadSafe)
   {
      this.threadSafe = threadSafe;
   }

   /**
    * Get the <code>RETURN-VALUE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public BaseDataType getReturnValue()
   {
      return returnValue == null ? new character() : returnValue;
   }

   /**
    * Get the <code>RETURN-VALUE-DATA-TYPE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getReturnValueDataType()
   {
      return returnValueDataType == null ? new character() : new character(returnValueDataType);
   }

   /**
    * Set the <code>RETURN-VALUE-DATA-TYPE</code> attribute of this CALL resource.
    * 
    * @param    dataType
    *           The return type for the next call.
    */
   @Override
   public void setReturnValueDataType(character dataType)
   {
      if (dataType.isUnknown())
      {
         unknownWarning("RETURN-VALUE-DATA-TYPE");
         return ;
      }
      
      setReturnValueDataType(dataType.getValue());
   }

   /**
    * Set the <code>RETURN-VALUE-DATA-TYPE</code> attribute of this CALL resource.
    * 
    * @param    dataType
    *           The return type for the next call.
    */
   @Override
   public void setReturnValueDataType(String dataType)
   {
      dataType = ProgressAst._keyword(dataType);
      if ("log".equalsIgnoreCase(dataType))
      {
         // we have a conflict with 'log' function
         dataType = "logical";
      }
      
      if (BaseDataType.fromTypeName(dataType) == null)
      {
         unableToSetAttribute(10067,
                              "Unable to set CALL object RETURN-VALUE-DATA-TYPE to " + dataType,
                              "RETURN-VALUE-DATA-TYPE");
         return;
      }
      
      this.returnValueDataType = dataType;
   }

   /**
    * Get the <code>LIBRARY</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getLibrary()
   {
      return library == null ? new character() : new character(library);
   }

   /**
    * Set the <code>LIBRARY</code> attribute of this CALL resource.
    * 
    * @param    libName
    *           The native library name.
    */
   @Override
   public void setLibrary(character libName)
   {
      if (libName.isUnknown())
      {
         unknownWarning("LIBRARY");
         return;
      }
      
      setLibrary(libName.getValue());
   }

   /**
    * Set the <code>LIBRARY</code> attribute of this CALL resource.
    * 
    * @param    libName
    *           The native library name.
    */
   @Override
   public void setLibrary(String libName)
   {
      if (this.library != null)
      {
         // this is reset if the library name was previously set
         callName = "";
      }
      
      this.library = libName;
   }

   /**
    * Get the <code>LIBRARY-CALLING-CONVENTION</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getLibraryCallingConvention()
   {
      return libraryCallingConvention == null ? new character() 
                                              : new character(libraryCallingConvention);
   }

   /**
    * Set the <code>LIBRARY-CALLING-CONVENTION</code> attribute of this CALL resource.
    * 
    * @param    libCall
    *           The library's calling convention, CDECL or STDCALL.
    */
   @Override
   public void setLibraryCallingConvention(character libCall)
   {
      if (libCall.isUnknown())
      {
         unknownWarning("LIBRARY-CALLING-CONVENTION");
         return;
      }
      
      setLibraryCallingConvention(libCall.getValue());
   }

   /**
    * Set the <code>LIBRARY-CALLING-CONVENTION</code> attribute of this CALL resource.
    * 
    * @param    libCall
    *           The library's calling convention, CDECL or STDCALL.
    */
   @Override
   public void setLibraryCallingConvention(String libCall)
   {
      libCall = libCall.toUpperCase();

      if (!("STDCALL".equals(libCall) || "CDECL".equals(libCall)))
      {
         // Unable to set LIBARARY-CALLING-COVENTION CALL attribute (15482)
         ErrorManager.recordOrShowError(15482, 
                                        "Unable to set LIBARARY-CALLING-COVENTION CALL attribute (15482)", 
                                        false, false, false, true);
         return;
      }
      
      libraryCallingConvention = libCall;
   }

   /**
    * Get the <code>ORDINAL</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public integer getOrdinal()
   {
      return ordinal == null ? new integer() : new integer(ordinal);
   }

   /**
    * Set the <code>ORDINAL</code> attribute of this CALL resource.
    * 
    * @param    ordinal
    *           The native API ordinal, if CALL-NAME is not specified.
    */
   @Override
   public void setOrdinal(NumberType ordinal)
   {
      if (ordinal.isUnknown())
      {
         unknownWarning("ORDINAL");
         return;
      }
      
      setOrdinal(ordinal.intValue());
   }

   /**
    * Set the <code>ORDINAL</code> attribute of this CALL resource.
    * 
    * @param    ordinal
    *           The native API ordinal, if CALL-NAME is not specified.
    */
   @Override
   public void setOrdinal(int ordinal)
   {
      if (callName != null)
      {
         callOrdinalError("ORDINAL");
         return;
      }

      this.ordinal = ordinal;
   }

   /**
    * Get the <code>RETURN-VALUE-DLL-TYPE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public character getReturnValueDllType()
   {
      return returnValueDllType == null ? new character() : new character(returnValueDllType);
   }

   /**
    * Set the <code>RETURN-VALUE-DLL-TYPE</code> attribute of this CALL resource.
    * 
    * @param    dataType
    *           The return type for the next native API call.
    */
   @Override
   public void setReturnValueDllType(character dataType)
   {
      if (dataType.isUnknown())
      {
         unknownWarning("RETURN-VALUE-DLL-TYPE");
         return;
      }
      
      setReturnValueDllType(dataType.getValue());
   }

   /**
    * Set the <code>RETURN-VALUE-DLL-TYPE</code> attribute of this CALL resource.
    * 
    * @param    dataType
    *           The return type for the next native API call.
    */
   @Override
   public void setReturnValueDllType(String dataType)
   {
      dataType = dataType.toLowerCase();
      
      if (!validDllType(dataType))
      {
         unableToSetAttribute(15483,
                              "Unable to set CALL object RETURN-VALUE-DLL-TYPE to " + dataType,
                              "RETURN-VALUE-DLL-TYPE");
         return;
      }
      
      this.returnValueDllType = dataType;
   }

   /**
    * Get the <code>ASYNCHRONOUS</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public logical isAsync()
   {
      return new logical(async);
   }

   /**
    * Set the <code>ASYNCHRONOUS</code> attribute of this CALL resource.
    * 
    * @param    l
    *           The new state of this flag.
    */
   @Override
   public void setAsync(logical l)
   {
      if (l.isUnknown())
      {
         unknownWarning("ASYNCHRONOUS");
         return;
      }
      
      setAsync(l.booleanValue());
   }

   /**
    * Set the <code>ASYNCHRONOUS</code> attribute of this CALL resource.
    * 
    * @param    l
    *           The new state of this flag.
    */
   @Override
   public void setAsync(boolean l)
   {
      this.async = l;
   }

   /**
    * Get the <code>ASYNC-REQUEST-HANDLE</code> attribute of this CALL resource.
    * 
    * @return   See above.
    */
   @Override
   public handle getAsyncHandle()
   {
      return new handle(asyncHandle);
   }

   /**
    * Get the <code>EVENT-PROCEDURE</code> attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public character getEventProcedure()
   {
      return eventProcedure == null ? new character() : new character(eventProcedure);
   }

   /**
    * Set the <code>EVENT-PROCEDURE</code> attribute of this async request.
    * 
    * @param    procName
    *           The procedure name.
    */
   @Override
   public void setEventProcedure(character procName)
   {
      if (procName.isUnknown())
      {
         unknownWarning("EVENT-PROCEDURE");
         return;
      }
      
      setEventProcedure(procName.getValue());
   }

   /**
    * Set the <code>EVENT-PROCEDURE</code> attribute of this async request.
    * 
    * @param    procName
    *           The procedure name.
    */
   @Override
   public void setEventProcedure(String procName)
   {
      this.eventProcedure = procName;
   }

   /**
    * Get the <code>EVENT-PROCEDURE-CONTEXT</code> attribute of this async request.
    * 
    * @return   See above.
    */
   @Override
   public handle getEventProcedureContext()
   {
      return eventProcedureContext == null ? new handle() : new handle(eventProcedureContext);
   }

   /**
    * Set the <code>EVENT-PROCEDURE-CONTEXT</code> attribute of this async request.
    * 
    * @param    h
    *           The procedure handle.
    */
   @Override
   public void setEventProcedureContext(handle h)
   {
      if (h.isUnknown())
      {
         unknownWarning("EVENT-PROCEDURE-CONTEXT");
         return;
      }
      
      eventProcedureContext = new handle(h);
   }

   /**
    * Returns the resource to its clear state.
    * 
    * @return  true if operation is successful
    */
   @Override
   public logical clear()
   {
      callName = null;
      callType = PROCEDURE_CALL_TYPE;
      inHandle = null;
      server = null;
      numParameters = 0;
      parameters = null;
      returnValue = null;
      returnValueDataType = null;
      persistent = false;
      procedureType = null;
      threadSafe = false;
      
      async = false;
      asyncHandle = new handle();
      eventProcedure = null;
      eventProcedureContext = null;
      
      ordinal = null;
      library = null;
      returnValueDllType = null;
      libraryCallingConvention = "STDCALL";
      
      return new logical(true);
   }

   /**
    * Execute this dynamic invoke, based on the current CALL configuration.
    * <p>
    * The dynamic invoke will be delegated to {@link #accessAttributeOrMethod} in case of 
    * {@link #GET_ATTR_CALL_TYPE} and {@link #SET_ATTR_CALL_TYPE} modes.
    * <p>
    * For {@link #PROCEDURE_CALL_TYPE} and {@link #FUNCTION_CALL_TYPE}, the call will be delegated
    * to {@link ControlFlowOps#invoke(InvokeConfig)}, which in turn will relies on the normal
    * {@link ControlFlowOps} APIs to perform the call.  If the {@link #PROCEDURE_CALL_TYPE} mode
    *  is used and the target is resolved as an <code>PROCEDURE ... EXTERNAL</code>, then this 
    *  will still be invoked using the RUN emulated call.
    * <p>
    * The {@link #DLL_CALL_TYPE} mode will rely on the 
    * {@link NativeInvoker#invoke(NativeAPIEntry, boolean, String, Class[], String, Object...)}:
    * this will use a synthetic {@link NativeAPIEntry}, which will be configured as if a 
    * <code>PROCEDURE ... EXTERNAL</code> was defined in the 4GL code, using the current 
    * {@link Call CALL's} configuration.  The actual invoke will bypass the {@link ControlFlowOps}
    * APIs for this mode.
    */
   @Override
   public void invoke()
   {
      if (callName == null && ordinal == null)
      {
         ErrorManager.recordOrThrowError(14351, 
                                         "CALL-NAME or ORDINAL must be specified before using " +
                                         "INVOKE with a CALL object", 
                                         false);
         return;
      }
      
      if (parameters != null)
      {
         for (int i = 0; i < parameters.length; i++)
         {
            if (parameters[i] == null)
            {
               ErrorManager.recordOrThrowError(10044, 
                                               "All parameters must be specified with " +
                                               "SET-PARAMETER before using INVOKE for " + callName, 
                                               false);
               return;
            }
         }
      }

      int ct = callType;
      if (ct == DLL_CALL_TYPE && (library == null || inHandle != null))
      {
         ct = PROCEDURE_CALL_TYPE;
      }
      
      switch (ct)
      {
         case FUNCTION_CALL_TYPE:
         case PROCEDURE_CALL_TYPE:
            // if inHandle is set, must be a procedure - but validate it at the CFO call
            break;
         
         case DLL_CALL_TYPE:
            // nothing to do
            break;

         case GET_ATTR_CALL_TYPE:
         case SET_ATTR_CALL_TYPE:
            // must be a valid resource
            if (inHandle == null || !inHandle._isValid())
            {
               ErrorManager.recordOrShowError(10091, 
                                              "CALL:INVOKE finds an invalid CALL:IN-HANDLE for " +
                                              "GET/SET-ATTR-CALL-TYPE.  It must be a valid 4gl " +
                                              "OBJECT HANDLE", 
                                              false);
               return;
            }
            
            break;
      }
      
      boolean silent = ErrorManager.isSilent();
      boolean function = false;
      switch (ct)
      {
         case FUNCTION_CALL_TYPE:
            function = true;
         case PROCEDURE_CALL_TYPE:
            InvokeConfig cfg = buildInvokeConfig(function);
            
            if (!silent)
            {
               // assume a silent mode, so that errors can be captured and re-raised
               ErrorManager.setSilent(true);
            }

            boolean ran = false;
            try
            {
               if (parameters != null)
               {
                  for (CallParameter cparam : parameters)
                  {
                     Object arg = cparam.getArgument();
                     if (arg instanceof AbstractExtentParameter)
                     {
                        AbstractParameter.getCurrentScope()
                                          .addExtentParameter((AbstractExtentParameter<?>) arg);
                     }
                     else if (arg instanceof AbstractSimpleParameter)
                     {
                        AbstractParameter.getCurrentScope()
                                         .addSimpleParameter((AbstractSimpleParameter) arg);
                     }
                  }
               }
               
               BaseDataType val = cfg.executeForResource(this);
               ran = true;
               
               if (!silent && ErrorManager.isPendingError())
               {
                  // raise a condition, this is from a RETURN ERROR - will be re-thrown in catch
                  throw new ErrorConditionException("return error");
               }
               
               
               // now, we need to copy back the output and input-output arguments
               if (parameters != null)
               {
                  for (CallParameter cparam : parameters)
                  {
                     if (cparam.mode.output || cparam.mode.inputOutput)
                     {
                        if (!cparam.copyOutput())
                        {
                           // if a parameter copy failed, stop processing
                           break;
                        }
                     }
                  }
               }
               
               // set return type
               if (function && val != null)
               {
                  returnValue = val;
                  returnValueDataType = val.getTypeName().toLowerCase();
               }
               else
               {
                  returnValue = null;
                  returnValueDataType = null;
               }
            }
            catch (LegacyErrorException lex)
            {
               throw lex;
            }
            catch (ErrorConditionException ece)
            {
               ErrorManager.setSilent(silent);

               if (ran || ControlFlowOps.hadInvalidArguments())
               {
                  // just re-throw it, this was an invalid argument issue
                  throw ece;
               }

               boolean pendingError = ErrorManager.isPendingError();
               ErrorManager.clearPending();

               int[] nums =
               {
                  ece.getProgressErrorCode(),
                  5729,
                  10089
               };
               String[] texts =
               {
                  ece.getMessage(),
                  "Incompatible datatypes found during runtime conversion",
                  "Unable to update output parameter for dynamic INVOKE"
               };
               
               if (function || !pendingError)
               {
                  ErrorManager.recordOrShowError(nums, texts, false, false, false, true);
               }
               else
               {
                  ErrorManager.recordOrThrowError(nums, texts, false, true);
               }
            }
            finally
            {
               ErrorManager.setSilent(silent);
               
               OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();
               if (opa != null)
               {
                  opa.abort();
               }
            }
            break;
            
         case DLL_CALL_TYPE:
            boolean hasRetParam = returnValueDllType != null;
            Object[] args = new Object[numParameters + (hasRetParam ? 1 : 0)];
            Class<?>[] signInput = new Class[numParameters + (hasRetParam ? 1 : 0)];
            List<Parameter> nativeParameters = new ArrayList<>();
            boolean noInvoke = false;
            
            for (int i = 0; i < numParameters; i++)
            {
               CallParameter cparam = parameters[i];
               
               if (!(cparam instanceof NativeCallParameter || 
                     cparam instanceof ExtentCallParameter))
               {
                  // in this case, the run is aborted - it will only look if the entry exists in
                  // the library; no actual invoke is performed
                  noInvoke = true;
               }
               
               Object arg = cparam.getArgument();
               if (arg instanceof AbstractExtentParameter)
               {
                  AbstractParameter.getCurrentScope()
                                    .addExtentParameter((AbstractExtentParameter<?>) arg);
               }
               else if (arg instanceof AbstractSimpleParameter)
               {
                  AbstractParameter.getCurrentScope()
                                   .addSimpleParameter((AbstractSimpleParameter) arg);
               }

               args[i] = arg;
               signInput[i] = args[i].getClass();
               
               CallMode cmode = parameters[i].mode;
               String smode = cmode.input ? "INPUT" 
                                          : (cmode.output ? "OUTPUT" : "INPUT-OUTPUT");
               Parameter param = new Parameter("call-param" + i, cparam.dataType, smode);
               nativeParameters.add(param);
            }
            
            if (hasRetParam)
            {
               // add a new parameter with mode "RETURN", last
               Parameter param = new Parameter("CALL-RET", returnValueDllType, "RETURN");
               nativeParameters.add(param);
               
               BaseDataType retParam = null;
               switch (returnValueDllType)
               {
                  case "byte":
                  case "short":
                  case "long":
                  case "unsigned-long":
                  case "unsigned-short":
                     retParam = new integer();
                     break;
                  case "int64":
                     retParam = new int64();
                     break;
                  case "character":
                     retParam = new character();
                     break;
                  case "double":
                  case "float":
                     retParam = new decimal();
                     break;
                  case "memptr":
                     retParam = new memptr();
                     break;
               }
               
               args[numParameters] = retParam;
               signInput[numParameters] = retParam.getClass();
            }

            NativeAPIEntry nativeEntry = new NativeAPIEntry(callName, callName);
            nativeEntry.setLibraryName(library);
            nativeEntry.setPersistent(persistent);
            if (ordinal != null)
            {
               nativeEntry.setOrdinal(ordinal);
            }
            nativeEntry.setThreadSafe(threadSafe);
            nativeEntry.setCallingConvention(CallingConvention.valueOf(libraryCallingConvention));
            nativeEntry.setParameterList(nativeParameters);
            nativeEntry.initialize();
            
            String modes = nativeEntry.getParameterModes();

            boolean failed = false;
            OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();
            try
            {
               NativeInvoker.invoke(nativeEntry, noInvoke, "DYNAMIC CALL", signInput, modes, args);
               failed = ErrorManager.isPendingError();
            }
            catch (Exception exc)
            {
               failed = true;
               throw exc;
            }
            finally
            {
               if (opa != null)
               {
                  if (failed)
                  {
                     opa.abort();
                  }
                  else
                  {
                     opa.processAssignments(callName, false);
                  }
               }
            }
            
            if (returnValueDllType != null)
            {
               returnValue = (BaseDataType) args[args.length - 1];
               returnValueDataType = returnValue.getTypeName().toLowerCase();
            }
            else
            {
               returnValue = null;
               returnValueDataType = null;
            }

            break;

         case GET_ATTR_CALL_TYPE:
            accessAttributeOrMethod(false);
            break;
         
         case SET_ATTR_CALL_TYPE:
            accessAttributeOrMethod(true);
            break;
      }
   }
   
   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(int64 parmNum, character dataType, character ioMode, Object value)
   {
      if (numParameters <= 0)
      {
         // show error, do not raise
         // Cannot SET-PARAMETER before specifying NUM-PARAMETERS for the CALL object. (10051)
         ErrorManager.recordOrShowError(10051, 
                                        "Cannot SET-PARAMETER before specifying NUM-PARAMETERS " +
                                        "for the CALL object", 
                                        false, false, false);
         return new logical(false);
      }
      
      if (parmNum.isUnknown())
      {
         // SET-PARAMETER index must be 1 based and less than or equal to1. (10052)
         ErrorManager.recordOrThrowError(10052, 
                                         "SET-PARAMETER index must be 1 based and less than or " +
                                         "equal to" + numParameters, 
                                         false);
         return new logical(false);
      }
      
      int pnum = parmNum.intValue();
      if (pnum < 1 || pnum > numParameters)
      {
         // SET-PARAMETER index must be 1 based and less than or equal to1. (10052)
         ErrorManager.recordOrThrowError(10052, 
                                         "SET-PARAMETER index must be 1 based and less than or " +
                                         "equal to" + numParameters, 
                                         false);
         return new logical(false);
      }
      
      CallMode cmode = CallMode.buildMode(ioMode);
      if (cmode == null)
      {
         ErrorManager.recordOrThrowError(10057, 
                                         "SET-PARAMETER input-output mode must be one of " +
                                         "INPUT, OUTPUT, INPUT-OUTPUT, OUTPUT-APPEND, enclosed " +
                                         "in quotes or in an expression.. (10057)", 
                                         false,
                                         true);
         return new logical(false);
      }
      
      if (!cmode.input && !validOutputParameter(pnum, value))
      {
         return new logical(false);
      }
      
      if (dataType.isUnknown())
      {
         ErrorManager.recordOrThrowError(10053, 
                                         "SET-PARAMETER datatype ? is not supported for CALL object", 
                                         false);
         return new logical(false);
      }
      
      CallParameter param = createParameter(dataType.getValue(), cmode, value, library != null, false);
      
      if (param == null)
      {
         return new logical(false);
      }
      
      parameters[pnum - 1] = param;
      
      return new logical(true);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(long parmNum, character dataType, character ioMode, Object value)
   {
      return setParameter(new integer(parmNum), dataType, ioMode, value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(int64 parmNum, String dataType, character ioMode, Object value)
   {
      return setParameter(parmNum, new character(dataType), ioMode, value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(long parmNum, String dataType, character ioMode, Object value)
   {
      return setParameter(new integer(parmNum), new character(dataType), ioMode, value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(int64 parmNum, character dataType, String ioMode, Object value)
   {
      return setParameter(parmNum, dataType, new character(ioMode), value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(long parmNum, character dataType, String ioMode, Object value)
   {
      return setParameter(new integer(parmNum), dataType, new character(ioMode), value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(int64 parmNum, String dataType, String ioMode, Object value)
   {
      return setParameter(parmNum, new character(dataType), new character(ioMode), value);
   }

   /**
    * Set the specified parameter for this dynamic invocation.
    * 
    * @param    parmNum
    *           The 1-based parameter index.
    * @param    dataType
    *           The parameter's data type.
    * @param    ioMode
    *           The parameter's mode (INPUT, OUTPUT, INPUT-OUTPUT, with -APPEND or -BY-REFERENCE
    *           suffix).
    * @param    value
    *           The argument's value.   Can be only a variable/field-ref or extent, if mode is
    *           OUTPUT or INPUT-OUTPUT.
    *           
    * @return   <code>true</code> if the parameter was set successfully. 
    */
   @Override
   public logical setParameter(long parmNum, String dataType, String ioMode, Object value)
   {
      return setParameter(new integer(parmNum), 
                          new character(dataType),
                          new character(ioMode),
                          value);
   }
   
   /**
    * Check if this resource supports the NAME attribute.
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean hasName()
   {
      return false;
   }

   /**
    * Check if this resource supports the NAME attribute in read-only mode.
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean hasNameReadOnly()
   {
      return false;
   }

   /**
    * Check if this resource can be reported as chained.
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean isChained()
   {
      return false;
   }

   /**
    * Check if this resource supports the NEXT-SIBLING attribute.
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean hasNextSibling()
   {
      return false;
   }

   /**
    * Check if this resource supports the PREV-SIBLING attribute.
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean hasPrevSibling()
   {
      return false;
   }

   /**
    * Check if this resource has as parent the specified resource.
    *
    * @param    parent
    *           The parent for which the first child is needed.  If <code>null</code>, the first
    *           resource with no parent set is returned.
    *           
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean hasParent(HandleChain parent)
   {
      return false;
   }

   /**
    * Worker to be implemented by each resource.  Called by {@link #delete()}.
    * 
    * @return   <code>true</code> if the resource was deleted.
    */
   @Override
   protected boolean resourceDelete()
   {
      deleted = true;
      
      // release all references
      clear();
      
      ControlFlowOps.invalidateCallSiteCache(this);
      
      return true;
   }

   /**
    * Check if the resource may be implicitly deleted. 
    * 
    * @return   Always <code>false</code>.
    */
   @Override
   protected boolean implicitDeletion()
   {
      return false;
   }

   /**
    * Check if the specified data-type is valid to be used via {@link #setParameter SET-PARAMETER},
    * for a native library API call.
    * 
    * @param    dataType
    *           The data type to be validated.
    *           
    * @return   <code>true</code> if this is a valid data type.
    */
   public static boolean validDllType(String dataType)
   {
      dataType = ProgressAst._keyword(dataType);
      
      return dataType.equals("byte")          ||
             dataType.equals("character")     ||
             dataType.equals("double")        ||
             dataType.equals("float")         ||
             dataType.equals("int64")         ||
             dataType.equals("long")          ||
             dataType.equals("memptr")        ||
             dataType.equals("short")         ||
             dataType.equals("unsigned-long") ||
             dataType.equals("unsigned-short");
   }
   
   /**
    * Verify if the given parameter value can be used in OUTPUT or INPUT-OUTPUT mode.  In these
    * modes, only variables, temp-table fields or subscripted vars/fields are allowed.
    * 
    * @param    pnum
    *           The parameter number.
    * @param    value
    *           The parameter's value.
    *           
    * @return   <code>true</code> if the value can be used.
    */
   public static boolean validOutputParameter(int pnum, Object value)
   {
      boolean isVar = (value instanceof BaseDataTypeVariable);
      if (isVar)
      {
         value = ((BaseDataTypeVariable) value).get();
      }
      
      if (value instanceof BaseDataType)
      {
         BaseDataType bdt = (BaseDataType) value;
         if (isVar)
         {
            return true;
         }
      }
      else if (value instanceof PropertyReference)
      {
         PropertyReference pref = (PropertyReference) value;
         // TODO: other validation?
         return true;
      }
      else if (value instanceof FieldReference)
      {
         FieldReference fref = (FieldReference) value;
         if (!(fref.getParentBuffer() instanceof TemporaryBuffer))
         {
            String msg = "Buffer for output parameter " + pnum + " not available during SET-PARAMETER";
            ErrorManager.recordOrThrowError(10062, msg, false, false);
            return false;
         }
         
         Buffer buf = (Buffer) fref.getParentBuffer().getDMOProxy();
         if (!buf._available())
         {
            // 4GL abends... we just continue
            
            String msg = "Buffer for output parameter " + pnum + " not available during SET-PARAMETER";
            ErrorManager.recordOrThrowError(10062, msg, false, false);
            return false;
         }
         
         // docs state that undoable temp-tables are not allowed, but looks like they are
         return true;
      }
      else if (value.getClass().isArray())
      {
         return true;
      }
      else if (value instanceof AbstractExtentParameter)
      {
         return true;
      }
      else if (value instanceof TableParameter || value instanceof DataSetParameter)
      {
         return true;
      }

      ErrorManager.recordOrThrowError(10060, 
                                      "SET-PARAMETER output parameter " +  pnum + 
                                      " must be a program variable or temp-table field " +
                                      "(subscript expressions are ok)", false);
      return false;
   }
   
   /**
    * Create a new parameter with the specified details, if at all possible.
    * <p>
    * If validation fails (invalid data type, mode or value combinations), then a <code>null</code>
    * value will be returned.
    * 
    * When calling external dynamic/shared library routine parameter data type is validated for 
    * supported external data types.
    * 
    * SetParameter method in ParametersList does an extra validation on input value against data type.
    *  
    * @param    dataType
    *           The data type to be used.
    * @param    mode
    *           The parameter's  mode.
    * @param    value
    *           The parameter's value.
    * @param    library
    *           Flag set to true if a dynamic/shared library routine is invoked.
    * @param    validateType
    *           Optional data type validation
    * @return   A new {@link CallParameter instance} with the parameter configuration.
    */
   public static CallParameter createParameter(
         String dataType, CallMode mode, Object value, boolean library, boolean validateType)
   {
      boolean isVar = (value instanceof BaseDataTypeVariable);
      if (isVar)
      {
         value = ((BaseDataTypeVariable) value).get();
      }
      
      boolean isClass = false;
      boolean isExtent = false;
      
      Matcher dataTypeMatch = dataTypePattern.matcher(dataType);
      if (dataTypeMatch.matches())
      {
         isClass = dataTypeMatch.group(1) != null;
         dataType = dataTypeMatch.group(2);
         isExtent = dataTypeMatch.group(3) != null;
      }
      
      if (!"DATASET".equalsIgnoreCase(dataType) && !"DATASET-HANDLE".equalsIgnoreCase(dataType) && 
          !"TABLE".equalsIgnoreCase(dataType)   && !"TABLE-HANDLE".equalsIgnoreCase(dataType))
      {
         dataType = resolveDataType(dataType);
      }
      
      if (library)
      {
         if (!validDllType(dataType))
         {
            // if LIBRARY is set, then only lib-specific datatypes can be set...
            String msg = "SET-PARAMETER datatype " + dataType + 
                         " is not supported for WINDOWS DLL or UNIX library CALL object";
            ErrorManager.recordOrThrowError(15481, msg, false);
            return null;
         }
      }
      else
      {
         // dataset and table parameter
         if (value instanceof DataSetParameter || value instanceof TableParameter || 
             "DATASET".equalsIgnoreCase(dataType) || "TABLE".equalsIgnoreCase(dataType)|| 
             "DATASET-HANDLE".equalsIgnoreCase(dataType) || "TABLE-HANDLE".equalsIgnoreCase(dataType))
         {
            Class<?> valueCls = value == null ? null : value.getClass();
            Object argVal = value;
            if (valueCls == FieldReference.class)
            {
               valueCls = ((FieldReference) value).getType();
               argVal = ((FieldReference) value).get();
            }
            else if (valueCls == PropertyReference.class)
            {
               valueCls = ((PropertyReference) value).getType();
               argVal = ((PropertyReference) value).get();
            }
            
            if (isExtent                                                                                  ||
                (!"DATASET".equalsIgnoreCase(dataType)       && value instanceof DataSetParameter)        ||
                (!"TABLE".equalsIgnoreCase(dataType)         && value instanceof TableParameter)          ||
                ("DATASET".equalsIgnoreCase(dataType)        && !(value instanceof DataSetParameter))     ||
                ("TABLE".equalsIgnoreCase(dataType)          && !(value instanceof TableParameter))       ||
                ("DATASET-HANDLE".equalsIgnoreCase(dataType) && !handle.class.isAssignableFrom(valueCls)) ||
                ("TABLE-HANDLE".equalsIgnoreCase(dataType)   && !handle.class.isAssignableFrom(valueCls)))
            {
               String msg = "If either the 2nd or 4th parameters to SetParameter are TABLE or DATASET types," +
                            " then they both must be.";
               ErrorManager.recordOrThrowError(15563, msg, false);
               return null;
            }
            
            return new CallParameter(dataType, isClass, mode, argVal);
         }
         
         Class<? extends BaseDataType> cls = !isClass && dataType != null
                  && DATA_TYPES.contains(dataType.toUpperCase())
                           ? BaseDataType.fromTypeName(dataType)
                           : ObjectOps.resolveClass(dataType);
         isClass = cls != null && _BaseObject_.class.isAssignableFrom(cls);
         Runnable invalidType = () -> {
            ErrorManager.recordOrThrowError(10059,
                     "Unable to convert SET-PARAMETER value to datatype passed", false);
         };
         
         Runnable incompatibleType = () -> {
            int[] nums = { 5729, 10059 };

            String[] texts = { "Incompatible datatypes found during runtime conversion",
                               "Unable to convert SET-PARAMETER value to datatype passed" };

            ErrorManager.recordOrThrowError(nums, texts, false, false);
         };

         if (cls == null)
         {
            invalidType.run();
         }

         if (value != null)
         {
            Class valueCls = value.getClass();
            BaseDataType val = value instanceof BaseDataType ? (BaseDataType) value : null;
            
            if (valueCls == unknown.class)
            {
               valueCls = cls;
            }
            
            boolean isValExtent = valueCls.isArray() || value instanceof AbstractExtentParameter;
            
            //TODO: Warning: the following condition is the subject of a conflict in the behavior
            // of 2 similar but different methods. What works for one fails for the other.
            // The methods are SetParameter for objects and SetParameter for handles.

            // Validate parameter and throw the coresponding error if the declared datatype is not
            // an extent but the value is
            if (!isExtent && isValExtent)
            {
               String errMsg = ("SetParameter requires that the EXTENT keyword must " +
                        "FOLLOW the datatype when the value is an array, " +
                        "but not when the value is a scalar");
               ErrorManager.recordOrThrowError(15308, errMsg, false);
               return null;
            }

            if (value instanceof FieldReference) 
            {
               FieldReference fieldRef = (FieldReference) value;
               isValExtent = fieldRef.getExtent() != null && fieldRef.getIndex() == -1;
               valueCls = fieldRef.getType();
               
               if (isValExtent && !fieldRef.hasIndex())
               {
                  // actually the error occurs when the called procedure returns, but this validation
                  // fails with NPE because the (output) value is evaluated now.
                  // because of that some information are not available here
                  Nameable caller = (Nameable) ProcedureManager.thisProcedure().getResource();
                  String msg = "Array parameter with extent  from procedure " + caller.name().toJavaType() + 
                               " is mismatched with parameter in procedure " +/*"callName" +*/" with extent N";
                  ErrorManager.recordOrThrowError(11428, msg, false);
                  return null;
               }
               
               val = fieldRef.get();
            }
            else if (value instanceof PropertyReference)
            {
               PropertyReference propRef = (PropertyReference) value;
               
               isValExtent = propRef.getExtent() != null && propRef.getIndex() == null;
               
               valueCls = propRef.getType();
            }
            else if (valueCls.isArray())
            {
               valueCls = valueCls.getComponentType();
            }
            else if (value instanceof object)
            {
               object ref = (object) value;
               if (ref._isValid())
               {
                  valueCls = ((object) value).ref.getClass();
               }
               else
               {
                  valueCls = _BaseObject_.class;
               }
            }
            else if (value instanceof BaseDataType && value.getClass() != unknown.class)
            {
               valueCls = ((BaseDataType) value).getType().getCls();
            }
            
            if ((valueCls.isAssignableFrom(memptr.class)) && !"MEMPTR".equalsIgnoreCase(dataType)
                     || ("MEMPTR".equalsIgnoreCase(dataType) && !(valueCls.isAssignableFrom(memptr.class))))
            {
               ErrorManager.recordOrThrowError(10058,
                        "MEMPTR parameters may not be converted to any other " + 
                        "type in dynamic invoke or SetParameter",
                        false);
               return null;
            }
            else if (("TABLE-HANDLE".equalsIgnoreCase(dataType) || "DATASET-HANDLE".equalsIgnoreCase(dataType)) &&
                     !(valueCls.isAssignableFrom(handle.class)))
            {
               invalidType.run();
            }
            
            if (!isValExtent && (!_BaseObject_.class.isAssignableFrom(cls) || !object.class.isAssignableFrom(valueCls))) {
               // run-time data type conversion seems to be attempted first even if data types does not match unless is extent
               // oddly when class parameter and value is object the assignment is not done, only compatibility check
               try
               {
                  BaseDataType inst = _BaseObject_.class.isAssignableFrom(cls) 
                                          ? new object(cls)
                                          : BaseDataType.generateUnknown(cls);
                  if (!inst.isIncompatibleTypesOnConversion(val))
                     inst.assign(val);
               }
               catch (ErrorConditionException ece)
               {
                  ErrorManager.addError(10059, "Unable to convert SET-PARAMETER value to datatype passed", true, false);
                  throw ece;
               }
               catch (Exception ex)
               {
                  LOG.log(Level.SEVERE, "Exception encountered when instantiating a value", ex);
               }
            }

            if (validateType)
            {
               boolean isCompatible = false;
               
               // numeric data types are compatible to some extent
               // decimal accepts int/int64
               // int64 accepts int
               // recid is not accepted by neither decimal/int/int64
               if (cls.equals(decimal.class))
               {
                  isCompatible = !valueCls.equals(recid.class) && NumberType.class.isAssignableFrom(valueCls);
               } 
               else if (cls.equals(int64.class))
               {
                  isCompatible = !valueCls.equals(recid.class) && int64.class.isAssignableFrom(valueCls);
               }
               else if (NumberType.class.isAssignableFrom(cls))
               {
                  // break int64->integer->recid hierarchy
                  isCompatible = valueCls.equals(cls);
               }
               else if (cls.equals(longchar.class))
               {
                  // longchar accepts also character but not clob
                  isCompatible = !valueCls.equals(clob.class) && Text.class.isAssignableFrom(valueCls);
               }
               else if (_BaseObject_.class.isAssignableFrom(cls))
               {
                  // value not an object
                  if (!(value instanceof object || value instanceof unknown))
                  {
                     isCompatible = false;
                  }
                  // value is object array
                  else if (isValExtent) 
                  {
                     isCompatible = isAssignableFrom((object[]) value, cls);
                  }
                  else
                  {
                     if (value instanceof unknown)
                     {
                        object objVal = new object();
                        objVal.assign(value);
                        isCompatible = true;
                     }
                     else
                     {
                        object oVal = (object) value;
                        isCompatible = cls.isAssignableFrom(oVal.isUnknown() ? oVal.type() : oVal.ref().getClass());
                     }
                  }
               }
               else 
               {
               // date->datetime->datetimetz hierarchy works
                  isCompatible = valueCls.isAssignableFrom(cls);
               }
               
               if (!isCompatible) 
               {
                  ErrorManager.recordOrThrowError(15315,
                           "SetParameter found incompatible data types used in dynamic parameter",
                           false);
                  return null;
               }
               
               if (isExtent && !isValExtent)
               {
                  ErrorManager.recordOrThrowError(15308,
                           "SetParameter requires that the EXTENT keyword must FOLLOW the datatype when " +
                           "the value is an array, but not when the value is a scalar",
                           false);
                  return null;
               }
               
            }
         }

      }

      if (isExtent)
      {
         return new ExtentCallParameter(dataType, isClass, mode, value);
      }

      CallParameter cparam = library ? new NativeCallParameter(dataType, mode, value)
               : new CallParameter(dataType, isClass, mode, value);

      return cparam;
   }
   
   /**
    * Validates and assign the value to variable instance.
    * <p>
    * If validation fails (invalid data type or value combinations), an error is
    * thrown.
    *  
    * @param    variable
    *           Instance of the field.
    * @param    value
    *           The parameter's value.
    * @param    dataType
    *           Data type of the variable to be set.
    * @param    isExtent
    *           Flag indicating that a variable instance is of type extent.
    * @param    validateType
    *           Optional data type validation.
    */
   public static void setterAssignment(Object variable,
                                      Object value,
                                      String dataType,
                                      boolean isExtent)
   {
      boolean isVar = (value instanceof BaseDataTypeVariable);
      if (isVar)
      {
         value = ((BaseDataTypeVariable) value).get();
      }
      
      if (!"DATASET".equalsIgnoreCase(dataType) && !"DATASET-HANDLE".equalsIgnoreCase(dataType) && 
          !"TABLE".equalsIgnoreCase(dataType)   && !"TABLE-HANDLE".equalsIgnoreCase(dataType))
      {
         dataType = resolveDataType(dataType);
      }
         
      Class<? extends BaseDataType> cls = (Class<? extends BaseDataType>) variable.getClass();
      Runnable invalidType = () -> {
            ErrorManager.recordOrThrowError(5678,
                     "Unable to do run-time conversion of datatypes", false);
      };
         
      if (cls == null)
      {
         invalidType.run();
         return;
      }

      if (value != null)
      {
         Class valueCls = value.getClass();
         BaseDataType val = value instanceof BaseDataType ? (BaseDataType) value : null;
            
         if (valueCls == unknown.class)
         {
            valueCls = cls;
         }
            
         boolean isValExtent = valueCls.isArray() || value instanceof AbstractExtentParameter;
            
         if (value instanceof FieldReference) 
         {
            FieldReference fieldRef = (FieldReference) value;
            isValExtent = fieldRef.getExtent() != null && fieldRef.getIndex() == -1;
            valueCls = fieldRef.getType();
               
            if (isValExtent && !fieldRef.hasIndex())
            {
               // actually the error occurs when the called procedure returns, but this validation
               // fails with NPE because the (output) value is evaluated now.
               // because of that some information are not available here
               Nameable caller = (Nameable) ProcedureManager.thisProcedure().getResource();
               String msg = "Array parameter with extent  from procedure " + caller.name().toJavaType() + 
                            " is mismatched with parameter in procedure " +/*"callName" +*/" with extent N";
               ErrorManager.recordOrThrowError(11428, msg, false);
               return;
            }
               
            val = fieldRef.get();
         }
         else if (value instanceof PropertyReference)
         {
            PropertyReference propRef = (PropertyReference) value;
               
            isValExtent = propRef.getExtent() != null && propRef.getIndex() == null;
               
            valueCls = propRef.getType();
         }
         else if (valueCls.isArray())
         {
            valueCls = valueCls.getComponentType();
         }
         else if (value instanceof object)
         {
            object ref = (object) value;
            if (ref._isValid())
            {
               valueCls = ((object) value).ref.getClass();
            }
            else
            {
               valueCls = _BaseObject_.class;
            }
         }
         else if (value instanceof BaseDataType && value.getClass() != unknown.class)
         {
            valueCls = ((BaseDataType) value).getType().getCls();
         }
            
         if ((valueCls.isAssignableFrom(memptr.class)) && !"MEMPTR".equalsIgnoreCase(dataType)
                     || ("MEMPTR".equalsIgnoreCase(dataType) && !(valueCls.isAssignableFrom(memptr.class))))
         {
            ErrorManager.recordOrThrowError(10058,
                        "MEMPTR parameters may not be converted to any other " + 
                        "type in dynamic invoke or SetParameter",
                        false);
            return;
         }
         else if (("TABLE-HANDLE".equalsIgnoreCase(dataType) || "DATASET-HANDLE".equalsIgnoreCase(dataType)) &&
                  !(valueCls.isAssignableFrom(handle.class)))
         {
            invalidType.run();
            return;
         }
            
         if (!isValExtent && (!_BaseObject_.class.isAssignableFrom(cls) || !object.class.isAssignableFrom(valueCls))) 
         {
            // run-time data type conversion seems to be attempted first even if data types does not match unless is extent
            // oddly when class parameter and value is object the assignment is not done, only compatibility check
            try
            {
               BaseDataType inst = (BaseDataType) variable;
               if (!inst.isIncompatibleTypesOnConversion(val))
               {
                  inst.assign(val);               
               }
            }
            catch (ErrorConditionException ece)
            {
               invalidType.run();
               return;
            }
            catch (Exception ex)
            {
               LOG.log(Level.SEVERE, "Exception encountered when instantiating a value", ex);
            }
         }
      }
   }
   
   /**
    * Check if all values in the array/extent are compatible with the given data type.
    * 
    * @param values The values array (extent).
    * @param cls The data type to validate values against.
    * @return True is all values in the array are compatible with the data type.
    */
   private static boolean isAssignableFrom(object<? extends _BaseObject_>[] values, Class cls)
   {
      boolean isEmpty = true;
      boolean isAssignable = true;
      
      for (object<? extends _BaseObject_> value : values)
      {
         if (!value.isUnknown()) {
            isEmpty = false;
            // check the actual instance class, object type might be an invalid superclass like P.L.O
            isAssignable = cls.isAssignableFrom(value.ref.getClass());
            
            if (!isAssignable)
               break;
         }
      }
      
      if (!isEmpty)
         return isAssignable;
      
      Class valueCls = unknown.class;
      
      if (values.length > 0)
      {
         valueCls = values[0].type();
      }
      else if (ArrayAssigner.isDynamicArray(values))
      {
         valueCls = ArrayAssigner.getObjectType(values);
      }
      
      return cls.isAssignableFrom(valueCls);
   }

   /**
    * Worker method for the {@link #GET_ATTR_CALL_TYPE} and {@link #SET_ATTR_CALL_TYPE} modes.
    * <p>
    * It will try to access the attribute or method, using the current configuration.
    * 
    * @param    setter
    *           When <code>true</code>, we are in {@link #SET_ATTR_CALL_TYPE} mode, and only 
    *           writable attributes are accessed; otherwise, all readable attributes and methods.
    */
   private void accessAttributeOrMethod(boolean setter)
   {
      // first, check if this is a valid attribute/method token
      int tokenType = ProgressAst.lookupKeywordToken(callName);
      if (!ATTRIBUTES_AND_METHODS.containsKey(tokenType))
      {
         ErrorManager.recordOrShowError(10092, 
                                        "CALL:INVOKE finds an invalid CALL:CALL-NAME for " +
                                        "GET/SET-ATTR-CALL-TYPE.  It must be a valid 4gl widget " +
                                        "attribute or method", 
                                        false, false, false);
         return;
      }

      if (setter && numParameters != 1)
      {
         ErrorManager.recordOrShowError(10094, 
                                        "SET-ATTR-CALL-TYPE requires exactly 1 SET-PARAMETER " +
                                        "which describes the value to be set", 
                                        false, false, false);
         return;
      }

      Runnable invalidAttr = () ->
      {
         ErrorManager.recordOrShowError(4052, 
                                        "**" + callName + " is not a " + 
                                        (setter ? "settable" : "queryable") + 
                                        " attribute for " + inHandle.unwrap().name().getValue() + 
                                        " widget", 
                                        false, false, false); 
      };
      
      // finally, check if this is a valid attribute or method name, in this resource
      if (!((!setter && HandleOps.canQuery(inHandle, callName).booleanValue()) ||
            (setter && HandleOps.canSet(inHandle, callName).booleanValue())))
      {
         invalidAttr.run(); 
         return;
      }
      
      if (tokenType == ProgressParser.KW_INVOKE)
      {
         ErrorManager.recordOrShowError(10050, 
                                        "INVOKE method must occur as a stand-alone statement: " +
                                        "hCall:INVOKE.  It cannot occur inside an expression " +
                                        "or on either side of an equal sign",
                                        false, false, false);
         return;
      }
      
      if (!setter && tokenType == ProgressParser.KW_HANDLE)
      {
         // in this case, we can only read, and just return the resource
         returnValueDataType = "handle";
         
         returnValue = new handle(inHandle);
         return;
      }
      
      // for getter, the parameters are optional: find either with no parameters or with exact match
      String methodName = HandleOps.findLegacy(inHandle, callName, true, setter);
      if (methodName == null && !setter)
      {
         // if is not a setter, look for methods, too
         methodName = HandleOps.findLegacy(inHandle, callName, false, false);
      }
      
      if (methodName == null)
      {
         invalidAttr.run(); 
         return;
      }
      
      // reset this now (we may be accessing RETURN-VALUE from this same handle, don't leak prev
      // call's value).
      returnValue = null;
      
      if (returnValueDataType == null)
      {
         int ttype = ATTRIBUTES_AND_METHODS.get(tokenType);
         returnValueDataType = ExpressionConversionWorker.simpleExpressionType(ttype);
      }

      WrappedResource target = inHandle.getResource();
      Class<? extends WrappedResource> tclass = target.getClass();
      Method mthd = null;
      
      if (!setter)
      {
         try
         {
            // this is a no-param method call or an attribute getter - check if we can resolve
            // a no-argument method
            mthd = tclass.getMethod(methodName);
         }
         catch (Exception e)
         {
            // do nothing, fallback
         }
      }

      BaseDataType val = null;
      if (mthd != null)
      {
         try
         {
            // if a no-argument method was found, then invoke it
            val = (BaseDataType) Utils.invoke(mthd, target);
         }
         catch (Exception e)
         {
            LOG.log(Level.SEVERE, 
                    "Problem calling " + callName + " from " + inHandle.unwrap().name(), 
                    e);
            
            val = null;
         }
      }
      else
      {
         // now, look for a method call, attribute getter with an index arg or attribute setter
         
         List<Class<? extends BaseDataType>> argTypes = new ArrayList<>();

         // using METH_INT forces to check for a method's signature
         ParmType[] lptypes = SignatureHelper.getSignature(tokenType, ProgressParser.METH_INT);
         if (lptypes != null)
         {
            // this is a method call or an attribute getter which receives an extent arg
            if (parameters != null && lptypes.length <= parameters.length)
            {
               // arguments are validated this way: 
               // - if an OUTPUT parameter is found, then abort
               // - if the expected method signature is found, then continue with the call 
               boolean allParamsInput = true;
               for (int i = 0; i < lptypes.length; i++)
               {
                  if (parameters[i].mode.output)
                  {
                     // when an OUTPUT parameter is found, abort the call
                     allParamsInput = false;
                     break;
                  }
                  
                  Class<? extends BaseDataType> ptype = 
                     (Class<? extends BaseDataType>) lptypes[i].getParamType();
                  argTypes.add(ptype);
               }
               
               if (!allParamsInput)
               {
                  // nothing to do 
                  return;
               }
            }
            else
            {
               // we can't match with a method signature (not enough arguments)
               // abort the call, no invoke will be performed and no error will be shown
               return;
            }
         }
         else
         {
            // now, we are in an attribute setter - the argument type is resolved from the 
            // attribute's data type
            if (parameters != null && parameters.length == 1 && !parameters[0].mode.output)
            {
               argTypes.add(BaseDataType.fromTypeName(returnValueDataType));
            }
            else
            {
               // incorrect number of arguments or data type, abort
               return;
            }
         }
         
         // look for a method with the specified data types
         Class<? extends BaseDataType>[] ptypes = argTypes.toArray(new Class[0]);
         
         try
         {
            mthd = tclass.getMethod(methodName, ptypes);
         }
         catch (Exception e)
         {
            mthd = null;
         }
         
         if (mthd == null)
         {
            // if still not found, search for a method with the same name and number of arguments,
            // but having compatible data types
            for (Method m1 : tclass.getMethods())
            {
               if (!m1.getName().equals(methodName))
               {
                  continue;
               }
               
               Class<?>[] m1types = m1.getParameterTypes();
               if (m1types.length != ptypes.length)
               {
                  continue;
               }
               
               boolean ok = true;
               for (int i = 0; i < ptypes.length; i++)
               {
                  if (!m1types[i].isAssignableFrom(ptypes[i]))
                  {
                     ok = false;
                     break;
                  }
               }
               
               if (ok)
               {
                  mthd = m1;
                  break;
               }
            }
         }
         
         if (mthd != null && ptypes.length == numParameters)
         {
            // found a good method, convert the arguments to the specified data types and call it
            Object[] args = new Object[ptypes.length];

            for (int i = 0; i < ptypes.length; i++)
            {
               args[i] = parameters[i].getArgument();
               
               if (args[i] == null)
               {
                  LOG.log(Level.WARNING, 
                          "Argumenmt " + (i + 1) + " should have been prepared before INVOKE");

                  return;
               }
            }
            try
            {
               val = (BaseDataType) Utils.invoke(mthd, target, args);
            } 
            catch (Exception e)
            {
               LOG.log(Level.SEVERE, 
                        "Problem calling " + callName + " from " + inHandle.unwrap().name(), 
                        e);

               val = null;
            }
         }
         else
         {
            invalidAttr.run(); 
            return;
         }
      }
      
      if (!BaseDataType.class.isAssignableFrom(mthd.getReturnType()))
      {
         // there is an indeterminate behavior here: in some cases of uninitialized attributes
         // (like PRIVATE-DATA and PROCEDURE-TYPE), the data-type for the returned (unknown) 
         // value is not predictable.  we don't duplicate this, just take the attr/method 
         // return type and use that
         returnValueDataType = null;
      }
      else if (returnValueDataType == null && val != null)
      {
         returnValueDataType = ((BaseDataType) val).getTypeName().toLowerCase();
      }

      if (returnValueDataType != null)
      {
         // this may be set explicitly by the user (or prior call usage) 
         // above we compute the implicit data-type
         BaseDataType res = BaseDataType.generateUnknown(BaseDataType.fromTypeName(returnValueDataType));
   
         if (val != null)
         {
            res.assign(val);
         }
         
         returnValue = res;
      }
      else
      {
         returnValue = new unknown();
      }
   }
   
   /**
    * Show a warning that the given attribute is unknown.
    * 
    * @param    attr
    *           The attribute name.
    */
   private void unknownWarning(String attr)
   {
      String msg = "**Unable to assign UNKNOWN value to attribute " + attr + " on " + type() + 
                   "widget. (4083)";
      ErrorManager.displayWarning(4083, msg, false);
   }
   
   /**
    * Raise an ERROR condition when both the ORDINAL and CALL-NAME attributes are being set.
    * 
    * @param    attr
    *           The attribute being set.
    */
   private void callOrdinalError(String attr)
   {
      int[] nums = { 15566, 3131 };
      String[] texts = 
      {
         "Cannot set CALL-NAME and ORDINAL attributes at the same time",
         "Unable to set attribute " + attr + " in widget  of type CALL"
      };
      
      ErrorManager.recordOrThrowError(nums, texts, false);
   }
   
   /**
    * Raise an error that the attribute can't be set.
    * 
    * @param    err1
    *           The first error code.
    * @param    msg1
    *           The first error message.
    * @param    attr
    *           The attribute's name.
    */
   private void unableToSetAttribute(int err1, String msg1, String attr)
   {
      String[] errMsg =
      {
         msg1,
         "Unable to set attribute " + attr + " in widget  of type CALL"
      };
      
      int[] errorCodes = { err1, 3131 };
      ErrorManager.recordOrThrowError(errorCodes, errMsg, false, false);
   }
   
   /**
    * Build an {@link InvokeConfig configuration} to emulate a RUN or dynamic function call, using
    * this CALL's configuration.
    * <p>
    * This can be later used to perform the actual {@link ControlFlowOps#invoke(InvokeConfig) invoke}.
    *  
    * @param    function
    *           Flag indicating if this is a function call.
    *           
    * @return   The new configuration.
    */
   private InvokeConfig buildInvokeConfig(boolean function)
   {
      InvokeConfig cfg = CALL_SITE.clone().setTarget(callName);
      
      cfg.setFunction(function)
         .setInHandle(inHandle)
         .setPersistent(persistent && inHandle == null)
         .setSingleRun(singleRun && inHandle == null)
         .setSingleton(singleton && inHandle == null);

      // some adjustments related to CALL handle
      if (cfg.isPersistent() || cfg.isSingleRun() || cfg.isSingleton())
      {
         // the persistent procedure will be saved in 'IN-HANDLE' argument
         inHandle = new handle();
         cfg.setProcedureHandle(inHandle);
      }

      if (server != null)
      {
         cfg.setServerHandle(server);
      }
         
      cfg.setAsynchronous(async)
         .setAsyncHandle(asyncHandle);
      if (eventProcedure != null)
      {
         cfg.setEventProcedure(eventProcedure);
      }
      if (eventProcedureContext != null)
      {
         cfg.setEventProcedureContext(eventProcedureContext);
      }

      if (numParameters > 0)
      {
         String modes = "";
         Object[] args = new Object[numParameters];
         
         for (int i = 0; i < numParameters; i++)
         {
            CallParameter cparam = parameters[i];
            
            modes = modes + cparam.mode.asString();
            
            args[i] = cparam.getArgument();
         }
         
         cfg.setModes(modes)
            .setArguments(args);
      }
      
      return cfg;
   }
   
   private static String resolveDataType(String dataType)
   {
      String primitiveType = ProgressAst._keyword(dataType);

      if (primitiveType != null && (DATA_TYPES.contains(primitiveType.toUpperCase())
               || validDllType(primitiveType)))
      {
         return "log".equalsIgnoreCase(primitiveType) ? "logical" : primitiveType;
      }

      // if not a primitive data type check a valid OO class
      Class<_BaseObject_> cls = ObjectOps.resolveClass(dataType);

      return cls != null ? dataType : null;
   }
   
   /**
    * Given a decimal value, convert it to a byte representation; this representation is assumed
    * as an 'in memory representation' of the value, usable for converting to another data type,
    * where:
    * <ul>
    * <li>rightmost bit is "0x8#", where "#" is the number of fractional digits in hex</li>
    * <li>concatenate the fractional and non-fractional digits</li>
    * <li>if there is an odd number of digits, pad with 'F'</li>
    * </ul>
    *  
    * @param    val
    *           The decimal value.
    *           
    * @return   The byte representation.
    */
   static byte[] decimalBytes(decimal val)
   {
      String sval = ((decimal) val).toStringMessage();
      int dot = sval.indexOf('.');
      String fract = dot < 0 ? "" : sval.substring(dot + 1);
      String dec = dot < 0 ? sval : sval.substring(0, dot);
      String all = dec + fract;
      
      int blen = 1 + (all.length() / 2) + (all.length() % 2);
      byte[] bytes = new byte[blen];
      bytes[0] = (byte) ((0x8 << 4) + fract.length());
      int i = 1;
      while (!all.isEmpty())
      {
         String chars = all.length() == 1 ? (all + "F") : all.substring(0,  2);
         bytes[i++] = (byte) (Integer.parseInt(chars, 16) & 0xFF);
         
         all = all.length() == 1 ? all.substring(1) : all.substring(2);
      }
      
      return bytes;
   }
   
   /**
    * A wrapper for extent parameters (without a subscript).
    */
   private static class ExtentCallParameter
   extends CallParameter
   {
      /**
       * Create a new wrapper for a parameter which will be used for a native OS API call.
       * 
       * @param    dataType
       *           The parameter's data type.
       * @param    isClass
       *           Flag indicating that the datatype is a legacy OO class.
       * @param    mode
       *           The call mode.
       * @param    value
       *           The parameter's value.
       */
      public ExtentCallParameter(String dataType, boolean isClass, CallMode mode, Object value)
      {
         super(dataType, isClass, mode, value);
         
         convertTo(null);
         
         OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();
         if (opa != null)
         {
            opa.abort();
         }
      }

      /**
       * Convert this parameter's {@link CallParameter#value value} to be usable as an argument.
       * <p>
       * Once converted, it will be cached in the {@link #argument} variable.
       * 
       * @return   The {@link #argument}.
       */
      @Override
      public Object convertTo(Class<?> cls)
      {
         if (argument != null)
         {
            return argument;
         }

         if (initialValue instanceof FieldReference)
         {
            FieldReference fr = (FieldReference) initialValue;
            cls = fr.getType();
            
            if (mode.output)
            {
               argument = new OutputExtentField((DataModelObject) fr.getDMO(), fr.getProperty());
            }
            else if (mode.inputOutput)
            {
               argument = new InputOutputExtentField((DataModelObject) fr.getDMO(), fr.getProperty());
            }
            else
            {
               argument = new FieldReference((DataModelObject) fr.getDMO(), 
                                             fr.getProperty(), 
                                             fr.isUpperCase(),
                                             (NumberType) null,
                                             true).getExtentValues();
            }
            
            return argument;
         }
         else if (initialValue instanceof AbstractExtentParameter)
         {
            // we already have an extent parameter

            if (mode.input)
            {
               // in this case, just get the value
               argument = ((AbstractExtentParameter) initialValue).getVariableSafe();
            }
            else if (mode.output && initialValue instanceof InputOutputExtentParameter)
            {
               argument = new OutputExtentParameter<BaseDataType>()
               {
                  @Override
                  public BaseDataType[] getVariable()
                  {
                     return ((AbstractExtentParameter) initialValue).getVariableSafe();
                  }

                  @Override
                  public void setVariable(BaseDataType[] reference)
                  {
                     ((AbstractExtentParameter) initialValue).setVariable(reference);
                  }

                  @Override
                  protected boolean isTableField()
                  {
                     return ((AbstractExtentParameter) initialValue).isTableField();
                  }
               };
            }
            
            return argument != null ? argument : super.convertTo(cls);
         }

         // final case is a simple array reference

         if (!mode.input)
         {
            // only INPUT is allowed - the other modes require conversion rules
            throw new IllegalStateException("OUTPUT and INPUT-OUTPUT extent vars need to be " +
                                            "passed as AbstractExtentParameter instances!");
         }

         argument = (BaseDataType[]) value;

         return argument;
      }
      
      /**
       * Extent arguments are automatically copied via {@link AbstractExtentParameter} wrappers,
       * nothing to do.
       * 
       * @return    Always <code>true</code>.
       */
      public boolean copyOutput()
      {
         // nothing to do, is done automatically by the AbstractExtentParameter wrappers
         return true;
      }
   }
   
   /**
    * A wrapper for parameters which will be passed to a direct native OS API call.
    */
   private static class NativeCallParameter
   extends CallParameter
   {
      /**
       * Create a new wrapper for a parameter which will be used for a native OS API call.
       * 
       * @param    dataType
       *           The parameter's data type.
       * @param    mode
       *           The call mode.
       * @param    value
       *           The parameter's value.
       */
      public NativeCallParameter(String dataType, CallMode mode, Object value)
      {
         super(dataType, false, mode, value);
      }

      /**
       * Convert this parameter's {@link CallParameter#value value} to be usable as an argument.
       * <p>
       * Once converted, it will be cached in the {@link #argument} variable.
       * 
       * @return   The {@link #argument}.
       */
      public Object convertTo(Class<?> cls)
      {
         if (argument != null)
         {
            return argument;
         }

         if (initialValue instanceof BaseDataType || initialValue instanceof FieldReference)
         {
            // this handles both output and input-output modes
            argument = initialValue;
            return argument;
         }

         if (mode.input)
         {
            switch (dataType)
            {
               case "byte":
               case "short":
               case "long":
               case "unsigned-long":
               case "unsigned-short":
                  if (initialValue instanceof Number)
                  {
                     argument = new integer(((Number) initialValue).longValue());
                  }
                  else
                  {
                     argument = initialValue == null ? new integer() : new integer(initialValue.toString());
                  }
                  break;
               case "int64":
                  if (initialValue instanceof Number)
                  {
                     argument = new int64(((Number) initialValue).longValue());
                  }
                  else
                  {
                     argument = initialValue == null ? new int64() : new int64(initialValue.toString());
                  }
                  break;
               case "character":
                  argument = initialValue == null ? new character() : new character(initialValue.toString());
                  break;
               case "double":
               case "float":
                  if (initialValue instanceof Number)
                  {
                     argument = new decimal(((Number) initialValue).doubleValue());
                  }
                  else
                  {
                     argument = initialValue == null ? new decimal() : new decimal(initialValue.toString());
                  }
                  break;
            }
         }

         return argument;
      }
   }
   
}