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