FuzzyMethodLookup.java

/*
** Module   : FuzzyMethodLookup.java
** Abstract : Method resolution when there are multiple overloads which may match.
**
** Copyright (c) 2023-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 CA  20230928 Initial version. 
**     CA  20231007 Arguments for direct method calls which can't be emitted as direct java calls (as they 
**                  are considered 'dynamic poly') must be wrapped in a 'polyArg' to check the type.
** 002 SB  20231218 Changed isWideningTypeMatch so that for callee type 'object' the function always
**                  returns true. For type 'object' the caller's type can always be matched to a wider type.
** 003 CA  20250319 A method from a super-class can have the same name as current class, so calls to this kind
**                  of methods must not be resolved as a constructor for the current class.
** 004 CA  20250425 An unknown '?' literal can not match a BUFFER parameter.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.uast;

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

import com.goldencode.ast.Aast;

/**
 * Implements the rules to perform fuzzy method lookup. 
 */
public abstract class FuzzyMethodLookup
{
   /**
    * Get the class name where this method lookup starts from.
    * 
    * @return   See above.
    */
   protected abstract String getClassName();
   
   /**
    * Detect if the callee class is an instance of an implemented interface of the caller class.
    * 
    * @param    caller
    *           The caller type.
    * @param    callee
    *           The callee type.
    *           
    * @return   {@code true} if the given class is an implemented interface in this class. 
    */
   protected abstract boolean isImplemented(String caller, String callee);

   /**
    * Detect if the callee class is an instance of a parent class or an implemented interface of a parent
    * class, of the caller.
    * 
    * @param    caller
    *           The caller type.
    * @param    callee
    *           The callee type to check.
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *
    * @return   {@code true} if the given class is a parent class or an implemented interface of a parent
    *           class. 
    */
   protected abstract boolean isInheritedFrom(String caller, String callee, Runnable incrementer);

   /**
    * Reports if the class name is Progress.Lang.ParameterList and the method name is {@code setParameter()}.
    *
    * @param    method
    *           The method name.
    *
    * @return   {@code true} if this is a setParameter call.
    */
   protected abstract boolean isSetParam(String method);
   
   /**
    * Get the list of all possible methods that could match in this class AND its parent classes. The
    * actual types are not checked but the method name, access mode, static/instance and number of
    * parameters must all match.
    *
    * @param    name
    *           Method name.
    * @param    num
    *           Number of parameters.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up. Otherwise any method can be
    *           returned.
    * @param    internal
    *           {@code true} if the lookup is internal to the current class definition.
    * @param    first
    *           {@code true} if the first match should be immediately returned as the result.
    * @param    exist
    *           The set of existing methods already represented in the list.
    * @param    constructor
    *           Flag indicating this must resolve a constructor.
    *
    * @return   List of possible methods that match.  This may be an empty list if no match exists.
    */
   protected abstract List<MatchMetrics> candidates(String            name,
                                                    int               num,
                                                    int               access,
                                                    boolean           isStatic,
                                                    boolean           internal,
                                                    boolean           first,
                                                    Set<SignatureKey> exist,
                                                    boolean constructor);
   
   /**
    * Find the named method based on a fuzzy signature match. This method will search up the
    * parent hierarchy (recursively) if no fuzzy match is found in the current class.
    *
    * @param    name
    *           Resource name.
    * @param    caller
    *           Method call signature.
    * @param    access
    *           Access mode (<code>KW_PUBLIC</code>, <code>KW_PROTECTD</code> or
    *           <code>KW_PRIVATE</code>).
    * @param    isStatic
    *           If <code>true</code>, only static methods will be looked up.
    *           Otherwise any method can be returned.
    * @param    internal
    *           <code>true</code> if the lookup is internal to the current class definition.
    * @param    node
    *           The AST referencing this method.
    * @param    constructor
    *           Flag indicating this must resolve a constructor.
    *
    * @return   Resource found or <code>null</code> if no match exists.
    */
   protected MethodSearchResult fuzzyMethodLookup(String         name,
                                                  ParameterKey[] caller,
                                                  int            access,
                                                  boolean        isStatic,
                                                  boolean        internal,
                                                  Aast           node,
                                                  boolean        constructor)
   {
      boolean first = isSetParam(name);
      
      List<MatchMetrics> list = candidates(name, caller.length, access, isStatic, internal, first, null, 
                                           constructor);
      
      boolean debug = false;
      
      if (debug)
      {
         System.out.println("\n\n--------------------fuzzyMethodLookup START--------------------\n");
         
         System.out.printf("\n   CALLER %s\n   PARAMETERS: %d\n", 
                           (node == null ? "n/a" : node.dumpTree(true)), 
                           caller.length);
         
         for (int i = 0; i < caller.length; i++)
         {
            System.out.printf("      %s\n", caller[i]);
         }
         
         for (MatchMetrics m : list)
         {
            System.out.printf("DEBUG: %s\n", m.data);
         }
      }
            
      // quick out if there are no possible matches
      if (list.isEmpty())
      {
         if (debug)
            System.out.println("--------------------fuzzyMethodLookup QUICK OUT--------------------");
         
         return null;
      }
      
      // special case for ParameterList.setParameter()
      if (first && list.size() == 1)
      {
         return new MethodSearchResult(list.get(0).data, null);
      }

      String[] error = new String[1];
      
      boolean polyArgs = false;
      boolean[] dynamicaPolyArgs = new boolean[] { false };

      
      // TODO: constructors

      // PHASE 1: cut down the list of all matches to include only those which are potentially valid
      
      if (debug)
         System.out.println("\n\n---PHASE 1 START---\n");

      // process each parameter from left to right; the processing will vary by the parameter type; some
      // types (e.g. objects) have multi-step checks and other types are a simple comparison; at each
      // step of the way the list of possible candidates will be reduced until there are only candidates
      // left which COULD match all criteria; at that point if there are more than one then we have an
      // additional disambiguation step, a dynamic invocation scenario or there is some ambiguity (which
      // should not happen if the code compiles in the 4GL)
      
      // process the caller's parameters, left to right; for each caller parameter we examine the matching
      // parameter in the list of candidates, removing any candidates in the list that cannot match
      for (int i = 0; i < caller.length; i++)
      {
         final int idx = i;
         
         final Integer callerMode = caller[i].mode;
         
         // if the caller is passing Java types, then we match them as if they were 4GL types so the get
         // converted here; if they are already 4GL types then no change will happen
         final String  callerType = fromJava(caller[i].type);
         
         // these are all specific to the current parameter
         Predicate<MatchMetrics> exactMatch = (m) -> callerType.equals(fromJava(m.data.getSignature(idx).type)); 
         Predicate<MatchMetrics> isTable    = (m) -> m.data.getSignature(idx).type.startsWith("TEMP-TABLE");
         Predicate<MatchMetrics> isTH       = (m) -> "TABLE-HANDLE".equals(m.data.getSignature(idx).type);
         Predicate<MatchMetrics> isDataset  = (m) -> m.data.getSignature(idx).type.startsWith("DATASET <");
         Predicate<MatchMetrics> isDH       = (m) -> "DATASET-HANDLE".equals(m.data.getSignature(idx).type);
         Predicate<MatchMetrics> modeMatch  = (m) -> callerMode.equals(m.data.getSignature(idx).mode);
         Predicate<MatchMetrics> allowUnkn  = 
            (m) -> !"unknown".equals(callerType) || !m.data.getSignature(idx).type.startsWith("BUFFER <");
         
         list.removeIf(allowUnkn.negate());
            
         // exclude any candidates which cannot ever match due to a caller MODE constraint; a null for the
         // caller's mode is a wildcard that can match any mode in the parameter definition; this means that
         // when the caller's mode is null, no matches can be excluded
         if (callerMode != null)
         {
            list.removeIf(modeMatch.negate());
            
            if (debug)
               System.out.printf("PARM %d: mode %d\n", idx, list.size());
         }

         // TYPE-specific processing
         
         boolean poly = (callerType == null) || callerType.equals("BaseDataType");
         
         // TYPE wildcards: unknown value or BDT (POLY cases) match any TYPE
         if (poly || callerType.equals("unknown"))
         {
            list.stream().forEach((m) -> m.wildcard++);
            
            if (poly)
               polyArgs = true;
            
            // no cases can be excluded based on type; the list remains unchanged
            if (debug)
               System.out.printf("PARM %d: type wildcard %d\n", idx, list.size());
         }
         else
         {
            // table handles
            if (callerType.equals("TABLE-HANDLE") || 
                callerType.startsWith("TABLE <")  || 
                callerType.startsWith("TEMP-TABLE <"))
            {
               // at this point we can't know if we have a match, we can just remove anything that is not
               // a table-handle or table; we defer the matching to the next phase once all other removals
               // are done               
               list.removeIf(isTH.or(isTable).negate());
               list.stream().forEach((m) -> m.cvt++);
               
               if (debug)
                  System.out.printf("PARM %d: TH %d\n", idx, list.size());
            }
            
            // dataset handles
            else if (callerType.equals("DATASET-HANDLE") || callerType.startsWith("DATASET <"))
            {
               // at this point we can't know if we have a match, we can just remove anything that is not
               // a dataset-handle or dataset; we defer the matching to the next phase once all other
               // removals are done               
               list.removeIf(isDH.or(isDataset).negate());
               list.stream().forEach((m) -> m.cvt++);
               
               if (debug)
                  System.out.printf("PARM %d: DH %d\n", idx, list.size());
            }

            // buffers
            else if (callerType.startsWith("BUFFER"))
            {
               // only an exact match works (handled above); no fuzzy matching here
               list.removeIf(exactMatch.negate());
               list.stream().forEach((m) -> m.exact++);
               
               if (debug)
                  System.out.printf("PARM %d: buf %d\n", idx, list.size());
            }

            // must be one of the BaseDataType wrapper classes
            else
            {
               final int     extIdx    = callerType.indexOf("[");
               final int     extVal    = (extIdx == -1) ? 0 : getExtent(callerType);
               final String  basicType = (extIdx == -1) ? callerType : callerType.substring(0, extIdx);

               // extents
               processExtentMatches(list, extVal, idx);
               
               if (debug)
                  System.out.printf("PARM %d: after extent processing %d\n", idx, list.size());

               // primitive and OO types widening/narrowing
               // WARNING: this code is not idempotent, we modify the MemberMetrics instances of any that
               //          are matched; this is BAD BAD BAD, but the widening/narrowing calculation is very
               //          expensive so it makes no sense to duplicate that work
               Predicate<MatchMetrics> isTypeMatch = (mem) -> 
               {
                  String base = mem.data.getSignature(idx).scalarType();
                  
                  Runnable increment = () -> mem.ooLvl[idx]++;
                  
                  // exact match to the base portion of the type will match for any mode (and is the only
                  // possible match for input-output mode); for the rest of the cases we check the 
                  // parameter mode at the definition (the caller might be unspecified (wildcard) but the
                  // definition must be specified
                  if (basicType.equals(base))
                  {
                     // yes, this is not idempotent; get over it
                     mem.exact++;
                     
                     return true;
                  }
                  else if (mem.data.getSignature(idx).mode == ProgressParserTokenTypes.KW_INPUT)
                  {
                     boolean widen = isWideningTypeMatch(basicType, base, increment);
                     
                     if (widen)
                     {
                        // yes, this is not idempotent; get over it
                        mem.cvt++;
                     }
                     else if (caller[idx].fromExpression)
                     {
                        // a single case is handled at this time: a INT64 argument to an INTEGER parameter
                        if ("int64".equals(basicType) && "integer".equals(base))
                        {
                           mem.cvt++;
                           widen = true;
                        }
                     }
                     
                     // only INPUT arguments are allowed in this dynamic poly arg mode
                     dynamicaPolyArgs[0] = dynamicaPolyArgs[0] || caller[idx].dynamicPoly;
                     return widen || caller[idx].dynamicPoly;
                  }
                  else if (mem.data.getSignature(idx).mode == ProgressParserTokenTypes.KW_OUTPUT)
                  {
                     boolean narrow = isNarrowingTypeMatch(basicType, base, increment);
                     
                     if (narrow)
                     {
                        // yes, this is not idempotent; get over it
                        mem.cvt++;
                     }
                     
                     return narrow;
                  }
                  
                  return false;
               };
   
               // WARNING: because isTypeMatch is not idempotent we make sure to only process it once
               list.removeIf(isTypeMatch.negate());
               
               if (debug)
                  System.out.printf("PARM %d: BDT type match %d\n", idx, list.size());
            }
         }
      }

      if (debug)
      {
         System.out.println("\n\n---PHASE 1 END---\n\n");
         
         for (MatchMetrics m : list)
         {
            System.out.printf("DEBUG: %s\n", m);
         }
      }
      
      // PHASE 2: if there are > 1 candidates we need to disambiguate based on priorities
      
      // unknown value, wildcard modes cannot be used to disambiguate
      
      if (list.size() > 1 || ((polyArgs || dynamicaPolyArgs[0]) && list.size() == 1))
      {
         MethodSearchResult dyn = null;
         
         if (polyArgs || dynamicaPolyArgs[0])
         {
            if (debug)
               System.out.println("\n\n---------fuzzyMethodLookup DYNAMIC POLY!---------\n");
            
            // POLY arguments activates pseudo-dynamic mode, where the conversion emits the signature of the
            // expected method to be called, and the runtime double-checks the resolved method's signature 
            // against the expected method's signature
            MatchMetrics match = list.get(list.size() - 1);
            
            dyn = new MethodSearchResult(match.data, match.overrides, polyArgs);
            
            return dyn; 
         }
         
         dyn = new MethodSearchResult();

         // TABLE-HANDLE/DATASET-HANDLE arguments deferred processing; we don't calculate this earlier
         // because some possible matches that would have been seen then may have been dropped by
         // processing of later parameters; this means that we must handle the possible compile time and
         // runtime matches here
         final int[] numTB = new int[caller.length];
         final int[] numTH = new int[caller.length];
         final int[] numDS = new int[caller.length];
         final int[] numDH = new int[caller.length];

         for (int i = 0; i < caller.length; i++)
         {
            final int idx = i;
            
            String callerType = fromJava(caller[i].type);
            
            Predicate<MatchMetrics> isTH       = (m) -> "TABLE-HANDLE".equals(m.data.getSignature(idx).type);
            Predicate<MatchMetrics> isDH       = (m) -> "DATASET-HANDLE".equals(m.data.getSignature(idx).type);
            Predicate<MatchMetrics> exactMatch = (m) -> callerType.equals(fromJava(m.data.getSignature(idx).type)); 

            // table parameters
            if (callerType.startsWith("TEMP-TABLE"))
            {
               // check for any exact TYPE matches
               if (!processExactMatches(list, exactMatch))
               {
                  // allow a match to a table-handle if there is only 1 table-handle option
                  filterList(list, isTH, idx, callerType, error);
               }
               
               if (debug)
                  System.out.printf("PARM %d: TT %d\n", idx, list.size());
            }

            // dataset parameters
            else if (callerType.startsWith("DATASET <"))
            {
               // check for any exact TYPE matches
               if (!processExactMatches(list, exactMatch))
               {
                  // allow a match to a dataset-handle if there is only 1 dataset-handle option
                  filterList(list, isDH, idx, callerType, error);
               }
               
               if (debug)
                  System.out.printf("PARM %d: DS %d\n", idx, list.size());
            }
            
            if ("TABLE-HANDLE".equals(callerType))
            {
               Predicate<MatchMetrics> isTable = (m) -> m.data.getSignature(idx).type.startsWith("TEMP-TABLE");

               Consumer<MatchMetrics> trackT = (m) ->
               {
                  if (isTable.test(m))
                  {
                     numTB[idx]++;
                  }
                  else if (isTH.test(m))
                  {
                     numTH[idx]++;
                  }
               };

               list.forEach(trackT);
               
               if (numTH[idx] == 1 && numTB[idx] == 0)
               {
                  list.removeIf(isTH.negate());
                  break;
               }
               else if (numTH[idx] == 0 && numTB[idx] == 1)
               {
                  list.removeIf(isTable.negate());
                  break;
               }
               else if ((numTH[idx] + numTB[idx]) > 1)
               {
                  // dynamic mode
                  if (debug)
                     System.out.println("\n\n---------fuzzyMethodLookup DYNAMIC TABLE-HANDLE!---------\n");
                  
                  return dyn;
               }
               else
               {
                  // no matches
                  list.clear();
                  error[0] = String.format("No possible matches for parameter %d of type %s.",
                                           idx,
                                           callerType);
                  break;
               }
            }
            
            if ("DATASET-HANDLE".equals(callerType))
            {
               Predicate<MatchMetrics> isDataset = (m) -> m.data.getSignature(idx).type.startsWith("DATASET <");
            
               Consumer<MatchMetrics> trackD = (m) ->
               {
                  if (isDataset.test(m))
                  {
                     numDS[idx]++;
                  }
                  else if (isDH.test(m))
                  {
                     numDH[idx]++;
                  }
               };
               
               list.forEach(trackD);
            
               if (numDH[idx] == 1 && numDS[idx] == 0)
               {
                  list.removeIf(isDH.negate());
                  break;
               }
               else if (numDH[idx] == 0 && numDS[idx] == 1)
               {
                  list.removeIf(isDataset.negate());
                  break;
               }
               else if ((numDH[idx] + numDS[idx]) > 1)
               {
                  // dynamic mode
                  if (debug)
                     System.out.println("\n\n---------fuzzyMethodLookup DYNAMIC DATASET-HANDLE!---------\n");
                  
                  return dyn;
               }
               else
               {
                  // no matches
                  list.clear();
                  error[0] = String.format("No possible matches for parameter %d of type %s.",
                           idx,
                           callerType);
                  break;
               }
            }            
         }
      }

      // exact vs fuzzy weighting disambiguation if needed
      if (list.size() > 1)
      {
         // use the natural ordering of the metrics class to find the best match
         Collections.sort(list, Collections.reverseOrder());
         
         MatchMetrics best     = list.get(0);
         MatchMetrics runnerUp = list.get(1);
         
         // safety check; if it fails we report the ambiguity below
         if (best.compareTo(runnerUp) == 1)
         {
            // clear everything except the best match
            list = new LinkedList<>();
            list.add(best);
         }
      }
      
      if (list.size() == 1)
      {
         // "there can be only one"
         if (debug)
            System.out.println("\n\n----------------fuzzyMethodLookup HIGHLANDER!----------------\n");
         
         MatchMetrics match = list.get(0);
         
         return new MethodSearchResult(match.data, match.overrides);
      }
      else if (list.isEmpty())
      {
         if (debug)
         {
            // if we still have ambiguous matches, display any stored error
            if (error[0] != null)
            {
               System.out.println(error[0]);
            }

            System.out.println("\n\n----------------fuzzyMethodLookup NO MATCH SUCKA!----------------\n");
         }
         
         return null;
      }

      System.out.println("WARNING: more than one method def found using fuzzy lookup: " + name + 
                         " from class " + getClassName());
      
      System.out.printf("\n   CALLER %s\n   PARAMETERS: %d\n", node.dumpTree(true), caller.length);
      
      for (int i = 0; i < caller.length; i++)
      {
         System.out.printf("      %s\n", caller[i]);
      }
 
      Iterator<MatchMetrics> iter = list.iterator();
      int num = 0;
      
      while (iter.hasNext())
      {
         MatchMetrics dat = iter.next();
         
         System.out.printf("   MATCH %d: %s\n\n", num, dat.toString());
         num++;
      }
      
      if (debug)
         System.out.println("\n\n--------------------fuzzyMethodLookup BITTER END---------------------\n");

      // return the first one in the sorted list
      MatchMetrics match = list.get(0);
      
      return new MethodSearchResult(match.data, match.overrides);
   }

   /**
    * Convert the specified Java-style type to a legacy BDT compatible type, if possible.
    * 
    * @param    type
    *           The Java type, in <code>jobject&gt; extends&lt;</code> format.
    * 
    * @return   The converted type (if conversion was made) or the original type, otherwise.
    */
   private String fromJava(String type)
   {
      if (type == null)
      {
         return null;
      }
      
      if (type.startsWith("jobject<? extends"))
      {
         String jtype = type.substring("jobject<? extends ".length(), type.length() - 1);
         
         if (jtype.equals("java.lang.Integer") || 
             jtype.equals("java.lang.Short")   || 
             jtype.equals("java.lang.Byte"))
         {
            type = "integer";
         }
         else if (jtype.equals("java.lang.Double") || 
                  jtype.equals("java.lang.Float") || 
                  jtype.equals("java.math.BigDecimal"))
         {
            type = "decimal";
         }
         else if (jtype.equals("java.lang.Boolean"))
         {
            type = "logical";
         }
         else if (jtype.equals("java.lang.String"))
         {
            type = "character";
         }
         else if (jtype.equals("java.util.Date"))
         {
            type = "date";
         }
         else if (jtype.equals("java.sql.Timestamp"))
         {
            type = "datetime";
         }
         // TODO: byte[] to raw
         
      }
      
      return type;
   }
   
   /**
    * Check if the caller's type can be matched to a wider type in the callee's signature.
    * <p>
    * Primitive types widen as follows:
    * <pre>
    *  Caller Type       Callee Parameter Types
    * --------------     ----------------------
    * integer            int64, decimal
    * int64              decimal
    * character          longchar
    * date               datetime, datetime-tz
    * datetime           datetime-tz
    * </pre>
    * <p>
    * Object types widen by these rules:
    * <ol>
    *    <li> We check if the callee matches any of the interfaces implemented by the caller type's specific class.
    *    <li> We check if the callee matches any of the parent class hierarchy (and their implemented interfaces)
    *         of the caller type's specific class.
    *    <li> We check if the callee is Progress.Lang.Object.  This check has lowest priority.
    * </ol>
    *
    * @param    caller
    *           The caller's passed instance's data type.
    * @param    callee
    *           The candidate callee method's parameter data type.
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *
    * @return   {@code true} if a widening operation is possible.
    */
   private boolean isWideningTypeMatch(String caller, String callee, Runnable incrementer)
   {
      if (("int64".equals(caller) && "decimal".equals(callee))                                   ||
          ("integer".equals(caller) && ("int64".equals(callee) || "decimal".equals(callee)))     ||
          ("character".equals(caller) && "longchar".equals(callee))                              ||
          ("datetime".equals(caller) && "datetimetz".equals(callee))                             ||
          ("date".equals(caller) && ("datetime".equals(callee) || "datetimetz".equals(callee))))
      {
         // to properly compute the best match, we need the distance between the caller and callee's type.
         incrementer.run();
         if (("integer".equals(caller) && "decimal".equals(callee)) || 
             ("date".equals(caller) && "datetimetz".equals(callee)))
         {
            incrementer.run();
         }
         
         return true;
      }
      
      if (caller.startsWith("object") && callee.startsWith("object"))
      {
         // check if the target is in the interface list of the caller type's specific class
         if (isImplemented(caller, callee))
         {
            return true;
         }
         
         // check if the target matches the caller type's parent class hierarchy (including implemented
         // interfaces)
         if (isInheritedFrom(caller, callee, incrementer))
         {
            return true;
         }

         // lowest priority/last ditch check
         return callee.toLowerCase().indexOf("progress.lang.object") > 0;
      }
      
      if (callee.startsWith("Object"))
      {
         return true;
      }
      
      return false;
   }
   
   /**
    * Check if the callee's type can be written back to a wider caller's type, when viewed from the
    * caller to callee, this looks like a "narrowing" operation.
    * <p>
    * Primitive types narrow as follows:
    * <pre>
    *  Caller Type       Callee Parameter Types
    * --------------     ----------------------
    * int64              integer
    * decimal            integer, int64
    * longchar           character
    * datetime           date
    * datetime-tz        date, datetime
    * </pre>
    * <p>
    * Object types narrow by these rules:
    * <ol>
    *    <li> We check if the callee matches a child class of the caller type's specific class.  The
    *         nearest child class is matched with highest priority.
    * </ol>
    *
    * @param    caller
    *           The caller's passed instance's data type.
    * @param    callee
    *           The candidate callee method's parameter data type.
    * @param    incrementer
    *           Code to be executed to increment the inheritance level.
    *
    * @return   {@code true} if a narrowing operation is possible.
    */
   private boolean isNarrowingTypeMatch(String caller, String callee, Runnable incrementer)
   {
      if (("int64".equals(caller)      && "integer".equals(callee))                             ||
          ("decimal".equals(caller)    && ("integer".equals(callee) || "int64".equals(callee))) ||
          ("longchar".equals(caller)   && "character".equals(callee))                           ||
          ("datetime".equals(caller)   && "date".equals(callee))                                ||
          ("datetimetz".equals(caller) && ("date".equals(callee) || "datetime".equals(callee))))
      {
         // to properly compute the best match, we need the distance between the caller and callee's type.
         incrementer.run();
         if (("decimal".equals(caller) && "integer".equals(callee)) || 
             ("datetimetz".equals(caller) && "date".equals(callee)))
         {
            incrementer.run();
         }
         
         return true;
      }
          
      if (caller.startsWith("object") && callee.startsWith("object"))
      {
         // check if the target parameter is a child class of the caller (this explicitly ignores
         // implemented interfaces which is how the 4GL does it)
         return isInheritedFrom(callee, caller, incrementer);
      }
                
      return false;
   }
   
   /**
    * Check if there is only 1 or more possible match for the given predicate, if so then remove all 
    * candidates from the list except for the possible matches.
    * 
    * @param    list
    *           The list to edit.
    * @param    test
    *           The predicate to test.
    * @param    idx
    *           The parameter index for error messages.
    * @param    callerType
    *           The caller type for error messages.
    * @param    error
    *           A location to store an error message for the caller.
    */
   private boolean filterList(List<MatchMetrics>      list, 
                              Predicate<MatchMetrics> test,
                              int                     idx, 
                              String                  callerType,
                              String[]                error)
   {
      boolean result = false;
      
      List<MatchMetrics> matches = list.stream().filter(test).collect(Collectors.toList());

      int num = matches.size();

      if (num >= 1)
      {
         result = true;
         matches.stream().forEach((m) -> m.cvt++);
         list.removeIf(test.negate());
      }
      else
      {
         error[0] = String.format("No possible matches for parameter %d of type %s.", idx, callerType);
      }
      
      if (!result)
      {
         list.clear();
      }
      
      return result;
   }
   
   /**
    * Remove any non-exact matches from the given list.
    * 
    * @param    list
    *           The list to edit.
    * @param    exactMatch
    *           The test for an exact match.
    *           
    * @return   {@code true} if edits were made.
    */
   private boolean processExactMatches(List<MatchMetrics> list, Predicate<MatchMetrics> exactMatch)
   {
      // calculate the exact TYPE matches
      List<MatchMetrics> exacts = list.stream().filter(exactMatch).collect(Collectors.toList());
      
      if (!exacts.isEmpty())
      {
         exacts.stream().forEach((m) -> m.exact++);
         
         // there is at least 1 exact match, remove everything else
         list.removeIf(exactMatch.negate());
         
         return true;
      }

      return false;
   }
   
   /**
    * Remove any candidates from the list which do not match the caller's extent.  The following are
    * possible matches:
    * <p>
    * <ul>
    *    <li> Neither the caller nor the parameter definition have an extent specification.
    *    <li> Both the caller and the parameter definition have an indeterminate extent.
    *    <li> The caller has a fixed extent value and the parameter definition has the same fixed extent
    *         value.
    *    <li> The caller has a fixed extent value and the parameter definition has an indeterminate
    *         extent.  This is a fuzzy match (all the above criteria are the same as exact matches). 
    *
    * @param    list
    *           The list of candidates.
    * @param    callerExtVal
    *           Caller's extent value (-1 means indeterminate, 0 is scalar and any positive value is
    *           the fixed extent).
    * @param    idx
    *           Parameter number being processed.
    */
   private void processExtentMatches(List<MatchMetrics> list, int callerExtVal, int idx)
   {
      Predicate<MatchMetrics> test = null;
      Consumer<MatchMetrics>  mark = (m) -> m.extExact++;
      
      // no extent in caller
      if (callerExtVal == 0)
      {
         // no extent in the parameter definition
         test = (mem) -> mem.data.getSignature(idx).type.indexOf("[") == -1;
         
         // no marking needed here
         mark = (m) -> {};
      }
      // indeterminate extent in caller
      else if (callerExtVal == -1)
      {
         // indeterminate extent in the parameter definition
         test = (mem) -> mem.data.getSignature(idx).type.indexOf("[") != -1 && 
                         getExtent(mem.data.getSignature(idx).type) == -1;
      }
      // fixed extent in caller
      else
      {
         test = (mem) -> 
         {
            int extIdx = mem.data.getSignature(idx).type.indexOf("[");
            
            if (extIdx != -1)
            {
               // the parameter definition is an extent
               int parmExtVal = getExtent(mem.data.getSignature(idx).type);
               
               // fixed extents match exactly or the parm is indeterminate
               return callerExtVal == parmExtVal || parmExtVal == -1;
            }
            else
            {
               // the parameter definition is NOT an extent
               return false;
            }
         };

         // this will only be used for where we already know extIdx != -1 above which means it is an
         // extent at the parameter definition (it may be either fixed or indeterminate) 
         mark = (mem) -> 
         {
            // the parameter definition is an extent
            int parmExtVal = getExtent(mem.data.getSignature(idx).type);
            
            if (callerExtVal == parmExtVal)
            {
               // both are fixed extents match exactly
               mem.extExact++;
            }
            else if (parmExtVal == -1) 
            {
               // or the parm is indeterminate
               mem.extCvt++;
            }
         };
      }
      
      list.removeIf(test.negate());
      list.stream().forEach(mark);
   }
   
   /**
    * Get the extent value from a parameter's type.  If dynamic extent, return -1.
    * 
    * @param    sig
    *           The parameter's type.
    *           
    * @return   The extent value.
    */
   private int getExtent(String sig)
   {
      int idx1 = sig.indexOf("[");
      int idx2 = sig.indexOf("]");
      
      if ((idx1 + 1 == idx2) || (idx1 == -1)) // Matches [] and no brackets (at least left one)
      {
         return -1;
      }
      
      String x = sig.substring(idx1 + 1, idx2);
      return Integer.parseInt(x);
   }
}