Merge branch '2.3-gae' into 2.3-gae-tools
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
index 3a05af0..77126cc 100644
--- a/.settings/org.eclipse.jdt.core.prefs
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -197,7 +197,7 @@
 org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
 org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
-org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.indentation.size=8
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
 org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
diff --git a/ivy.xml b/ivy.xml
index dbea293..994146d 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -125,7 +125,7 @@
     <!-- test -->
     
     <!-- Note: Ant doesn't contain junit.jar anymore, so we add it to conf "test" too. -->
-    <dependency org="junit" name="junit" rev="3.7" conf="build.test->default; test->default" />
+    <dependency org="junit" name="junit" rev="4.11" conf="build.test->default; test->default" />
     
     <!-- docs -->
     
diff --git a/src/main/java/freemarker/ext/beans/ArgumentTypes.java b/src/main/java/freemarker/ext/beans/ArgumentTypes.java
new file mode 100644
index 0000000..b13f842
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ArgumentTypes.java
@@ -0,0 +1,542 @@
+/*
+ * Copyright (c) 2003 The Visigoth Software Society. All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ *    any, must include the following acknowledgement:
+ *       "This product includes software developed by the
+ *        Visigoth Software Society (http://www.visigoths.org/)."
+ *    Alternately, this acknowledgement may appear in the software itself,
+ *    if and wherever such third-party acknowledgements normally appear.
+ *
+ * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
+ *    project contributors may be used to endorse or promote products derived
+ *    from this software without prior written permission. For written
+ *    permission, please contact visigoths@visigoths.org.
+ *
+ * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
+ *    nor may "FreeMarker" or "Visigoth" appear in their names
+ *    without prior written permission of the Visigoth Software Society.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Visigoth Software Society. For more
+ * information on the Visigoth Software Society, please see
+ * http://www.visigoths.org/
+ */
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import freemarker.template.Version;
+import freemarker.template.utility.ClassUtil;
+
+
+/**
+ * The argument types of a method call; usable as cache key.
+ * 
+ * @author Attila Szegedi
+ */
+final class ArgumentTypes {
+    
+    private static final int BIG_MANTISSA_LOSS_PRICE = 40000;
+
+    /**
+     * The types of the arguments; for varags this contains the exploded list (not the array). 
+     */
+    private final Class[] types;
+    
+    private final boolean bugfixed;
+    
+    /**
+     * @param args The actual arguments. A varargs argument should be present exploded, no as an array.
+     * @param bugfixed Introduced in 2.3.21, sets this object to a mode that works well with {@link BeansWrapper}-s
+     *      created with {@link Version} 2.3.21 or higher.
+     */
+    ArgumentTypes(Object[] args, boolean bugfixed) {
+        int l = args.length;
+        types = new Class[l];
+        for(int i = 0; i < l; ++i) {
+            Object obj = args[i];
+            types[i] = obj == null
+                    ? (bugfixed ? Null.class : Object.class)
+                    : obj.getClass();
+        }
+        this.bugfixed = bugfixed;
+    }
+    
+    public int hashCode() {
+        int hash = 0;
+        for(int i = 0; i < types.length; ++i) {
+            hash ^= types[i].hashCode();
+        }
+        return hash;
+    }
+    
+    public boolean equals(Object o) {
+        if(o instanceof ArgumentTypes) {
+            ArgumentTypes cs = (ArgumentTypes)o;
+            if(cs.types.length != types.length) {
+                return false;
+            }
+            for(int i = 0; i < types.length; ++i) {
+                if(cs.types[i] != types[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or
+     *         {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. 
+     */
+    MaybeEmptyCallableMemberDescriptor getMostSpecific(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg)
+    {
+        LinkedList/*<CallableMemberDescriptor>*/ applicables = getApplicables(memberDescs, varArg);
+        if(applicables.isEmpty()) {
+            return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
+        }
+        if(applicables.size() == 1) {
+            return (CallableMemberDescriptor) applicables.getFirst();
+        }
+        
+        LinkedList/*<CallableMemberDescriptor>*/ maximals = new LinkedList();
+        for (Iterator applicablesIter = applicables.iterator(); applicablesIter.hasNext();)
+        {
+            CallableMemberDescriptor applicable = (CallableMemberDescriptor) applicablesIter.next();
+            boolean lessSpecific = false;
+            for (Iterator maximalsIter = maximals.iterator(); 
+                maximalsIter.hasNext();)
+            {
+                CallableMemberDescriptor maximal = (CallableMemberDescriptor) maximalsIter.next();
+                final int cmpRes = compareParameterListPreferability(applicable.paramTypes, maximal.paramTypes, varArg); 
+                if (cmpRes > 0) {
+                    maximalsIter.remove();
+                } else if (cmpRes < 0) {
+                    lessSpecific = true;
+                }
+            }
+            if(!lessSpecific) {
+                maximals.addLast(applicable);
+            }
+        }
+        if(maximals.size() > 1) {
+            return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
+        }
+        return (CallableMemberDescriptor) maximals.getFirst();
+    }
+
+    /**
+     * Tells if among the parameter list of two methods, which one fits this argument list better.
+     * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured,
+     * what the result will be is undefined.
+     * 
+     * <p>This method behaves differently in {@code bugfixed}-mode (used when a {@link BeansWrapper} is created with
+     * incompatible improvements set to 2.3.21 or higher). Below we describe the bugfixed behavior only. 
+     *  
+     * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop.
+     * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter
+     * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter
+     * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability.
+     * 
+     * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how
+     * specific their types are. For example, {@code String} is more specific than {@link Object} (because
+     * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive
+     * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more
+     * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets
+     * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of
+     * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter
+     * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will
+     * be used to calculate the conversion "price", and the parameter type with lowest price wins.   
+     * 
+     * @param paramTypes1 The parameter types of one of the competing methods
+     * @param paramTypes2 The parameter types of the other competing method
+     * @param varArg Whether these competing methods are varargs methods. 
+     * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred,
+     *        0 if there's no decision 
+     */
+    int compareParameterListPreferability(Class[] paramTypes1, Class[] paramTypes2, boolean varArg) {
+        final int argTypesLen = types.length; 
+        final int paramTypes1Len = paramTypes1.length;
+        final int paramTypes2Len = paramTypes2.length;
+        //assert varArg || paramTypes1Len == paramTypes2Len;
+        
+        if (bugfixed) {
+            int paramList1WeakWinCnt = 0;
+            int paramList2WeakWinCnt = 0;
+            int paramList1WinCnt = 0;
+            int paramList2WinCnt = 0;
+            int paramList1StrongWinCnt = 0;
+            int paramList2StrongWinCnt = 0;
+            int firstWinerParamList = 0;
+            for (int i = 0; i < argTypesLen; i++) {
+                final Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+                final Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+                
+                final int winerParam;  // 1 => paramType1; -1 => paramType2; 0 => draw
+                if (paramType1 == paramType2) {
+                    winerParam = 0;
+                } else {
+                    final Class argType = types[i];
+                    final boolean argIsNum = Number.class.isAssignableFrom(argType);
+                    
+                    final int numConvPrice1;
+                    if (argIsNum && ClassUtil.isNumerical(paramType1)) {
+                        final Class nonPrimParamType1 = paramType1.isPrimitive()
+                                ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; 
+                        numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1);
+                    } else {
+                        numConvPrice1 = Integer.MAX_VALUE;
+                    }
+                    // numConvPrice1 is Integer.MAX_VALUE if either:
+                    // - argType and paramType1 aren't both numerical
+                    // - FM doesn't know some of the numerical types, or the conversion between them is not allowed    
+                    
+                    final int numConvPrice2;
+                    if (argIsNum && ClassUtil.isNumerical(paramType2)) {
+                        final Class nonPrimParamType2 = paramType2.isPrimitive()
+                                ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2; 
+                        numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2);
+                    } else {
+                        numConvPrice2 = Integer.MAX_VALUE;
+                    }
+                    
+                    if (numConvPrice1 == Integer.MAX_VALUE) {
+                        if (numConvPrice2 == Integer.MAX_VALUE) {  // No numerical conversions anywhere
+                            final int r = compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+                            if (r > 0) {
+                                winerParam = 1;
+                                if (r > 1) {
+                                    paramList1WinCnt++;
+                                } else {
+                                    paramList1WeakWinCnt++;
+                                }
+                            } else if (r < 0) {
+                                winerParam = -1;
+                                if (r < -1) {
+                                    paramList2WinCnt++;
+                                } else {
+                                    paramList2WeakWinCnt++;
+                                }
+                            } else {
+                                winerParam = 0;
+                            }
+                        } else {     // No num. conv. of param1, num. conv. of param2
+                            winerParam = -1;
+                            paramList2WinCnt++;
+                        }
+                    } else if (numConvPrice2 == Integer.MAX_VALUE) {  // Num. conv. of param1, not of param2
+                        winerParam = 1;
+                        paramList1WinCnt++;
+                    } else {  // Num. conv. of both param1 and param2
+                        if (numConvPrice1 != numConvPrice2) {
+                            if (numConvPrice1 < numConvPrice2) {
+                                winerParam = 1;
+                                if (numConvPrice1 < BIG_MANTISSA_LOSS_PRICE && numConvPrice2 > BIG_MANTISSA_LOSS_PRICE) {
+                                    paramList1StrongWinCnt++;
+                                } else {
+                                    paramList1WinCnt++;
+                                }
+                            } else {
+                                winerParam = -1;
+                                if (numConvPrice2 < BIG_MANTISSA_LOSS_PRICE && numConvPrice1 > BIG_MANTISSA_LOSS_PRICE) {
+                                    paramList2StrongWinCnt++;
+                                } else {
+                                    paramList2WinCnt++;
+                                }
+                            }
+                        } else {
+                            winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0);
+                            if (winerParam == 1) paramList1WeakWinCnt++;
+                            else if (winerParam == -1) paramList2WeakWinCnt++;
+                        }
+                    }
+                }  // when paramType1 != paramType2
+                
+                if (firstWinerParamList == 0 && winerParam != 0) {
+                    firstWinerParamList = winerParam; 
+                }
+            }  // for each parameter types
+            
+            if (paramList1StrongWinCnt != paramList2StrongWinCnt) {
+                return paramList1StrongWinCnt - paramList2StrongWinCnt;
+            } else if (paramList1WinCnt != paramList2WinCnt) {
+                return paramList1WinCnt - paramList2WinCnt;
+            } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) {
+                return paramList1WeakWinCnt - paramList2WeakWinCnt;
+            } else if (firstWinerParamList != 0) {  // paramList1WinCnt == paramList2WinCnt
+                return firstWinerParamList;
+            } else { // still undecided
+                if (varArg) {
+                    if (paramTypes1Len == paramTypes2Len) {
+                        // If we had a 0-length varargs array in both methods, we also compare the types at the
+                        // index of the varargs parameter, like if we had a single varargs argument. However, this
+                        // time we don't have an argument type, so we can only decide based on type specificity:
+                        if (argTypesLen == paramTypes1Len - 1) {
+                            Class paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true);
+                            Class paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true);
+                            if (ClassUtil.isNumerical(paramType1) && ClassUtil.isNumerical(paramType2)) {
+                                int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2);
+                                if (r != 0) return r;
+                                // falls through
+                            }
+                            return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
+                        } else {
+                            return 0;
+                        }
+                    } else {
+                        // The method with more fixed parameters wins:
+                        return paramTypes1Len - paramTypes2Len;
+                    }
+                } else {
+                    return 0;
+                }
+            }
+        } else { // non-bugfixed (backward-compatible) mode
+            boolean paramTypes1HasAMoreSpecific = false;
+            boolean paramTypes2HasAMoreSpecific = false;
+            for(int i = 0; i < paramTypes1Len; ++i) {
+                Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
+                Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
+                if(paramType1 != paramType2) {
+                    paramTypes1HasAMoreSpecific = 
+                        paramTypes1HasAMoreSpecific
+                        || MethodUtilities.isMoreOrSameSpecificParameterType(paramType1, paramType2, false, 0) != 0;
+                    paramTypes2HasAMoreSpecific = 
+                        paramTypes2HasAMoreSpecific
+                        || MethodUtilities.isMoreOrSameSpecificParameterType(paramType2, paramType1, false, 0) != 0;
+                }
+            }
+            
+            if(paramTypes1HasAMoreSpecific) {
+                return paramTypes2HasAMoreSpecific ? 0 : 1;
+            } else if(paramTypes2HasAMoreSpecific) {
+                return -1;
+            } else {
+                return 0;
+            }
+        }
+    }
+    
+    /**
+     * Trivial comparison type specificities; unaware of numerical conversions. 
+     * 
+     * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if
+     *     the difference is only in primitive VS non-primitive, more otherwise.
+     */
+    private int compareParameterListPreferability_cmpTypeSpecificty(final Class paramType1, final Class paramType2) {
+        // The more specific (smaller) type wins.
+        
+        final Class nonPrimParamType1 = paramType1.isPrimitive()
+                ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; 
+        final Class nonPrimParamType2 = paramType2.isPrimitive()
+                ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
+                
+        if (nonPrimParamType1 == nonPrimParamType2) {
+            if (nonPrimParamType1 != paramType1) {
+                if (nonPrimParamType2 != paramType2) {
+                    return 0;  // identical prim. types; shouldn't ever be reached
+                } else {
+                    return 1;  // param1 is prim., param2 is non prim.
+                }
+            } else if (nonPrimParamType2 != paramType2) {
+                return -1;  // param1 is non-prim., param2 is prim.
+            } else {
+                return 0;  // identical non-prim. types
+            }
+        } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) {
+            return 2;
+        } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) {
+            return -2;
+        } else  {
+            return 0;  // unrelated types
+        }
+    }
+
+    private static Class getParamType(Class[] paramTypes, int paramTypesLen, int i, boolean varArg) {
+        return varArg && i >= paramTypesLen - 1
+                ? paramTypes[paramTypesLen - 1].getComponentType()
+                : paramTypes[i];
+    }
+    
+    /**
+     * Returns all methods that are applicable to actual
+     * parameter types represented by this ArgumentTypes object.
+     */
+    LinkedList/*<CallableMemberDescriptor>*/ getApplicables(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg) {
+        LinkedList/*<CallableMemberDescriptor>*/ list = new LinkedList();
+        for (Iterator it = memberDescs.iterator(); it.hasNext();) {
+            CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) it.next();
+            if(isApplicable(memberDesc, varArg)) {
+                list.add(memberDesc);
+            }
+        }
+        return list;
+    }
+    
+    /**
+     * Returns true if the supplied method is applicable to actual
+     * parameter types represented by this ArgumentTypes object.
+     */
+    private boolean isApplicable(CallableMemberDescriptor memberDesc, boolean varArg) {
+        final Class[] paramTypes = memberDesc.paramTypes; 
+        final int cl = types.length;
+        final int fl = paramTypes.length - (varArg ? 1 : 0);
+        if(varArg) {
+            if(cl < fl) {
+                return false;
+            }
+        } else {
+            if(cl != fl) {
+                return false;
+            }
+        }
+        for(int i = 0; i < fl; ++i) {
+            if(!isMethodInvocationConvertible(paramTypes[i], types[i])) {
+                return false;
+            }
+        }
+        if(varArg) {
+            Class varArgParamType = paramTypes[fl].getComponentType();
+            for(int i = fl; i < cl; ++i) {
+                if(!isMethodInvocationConvertible(varArgParamType, types[i])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Determines whether a type represented by a class object is
+     * convertible to another type represented by a class object using a 
+     * method invocation conversion, treating object types of primitive 
+     * types as if they were primitive types (that is, a Boolean actual 
+     * parameter type matches boolean primitive formal type). This behavior
+     * is because this method is used to determine applicable methods for 
+     * an actual parameter list, and primitive types are represented by 
+     * their object duals in reflective method calls.
+     * @param formal the parameter type to which the actual 
+     * parameter type should be convertible; possibly a primitive type
+     * @param actual the argument type; not a primitive type, maybe {@link Null}.
+     * @return true if either formal type is assignable from actual type, 
+     * or the formal type is a primitive type and the actual type is its corresponding object
+     * type or an object type of a primitive type that can be converted to
+     * the formal type.
+     */
+    private boolean isMethodInvocationConvertible(final Class formal, final Class actual) {
+        // Check for identity or widening reference conversion
+        if(formal.isAssignableFrom(actual)) {
+            return true;
+        } else if (actual == Null.class && bugfixed) {
+            return !formal.isPrimitive();
+        } else if (bugfixed) {
+            final Class formalNP;
+            if (formal.isPrimitive()) {
+                formalNP = ClassUtil.primitiveClassToBoxingClass(formal);
+                if (actual == formalNP) {
+                    // Character and char, etc.
+                    return true;
+                }
+            } else {
+                formalNP = formal;
+            }
+            if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) {
+                return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) != Integer.MAX_VALUE;
+            } else {
+                return false;
+            }
+        } else { // if !bugfixed
+            // This non-bugfixed (backward-compatibile) branch:
+            // - Doesn't convert *to* non-primitive numerical types (unless the argument is a BigDecimal).
+            //   (This is like in Java language, which also doesn't coerce to non-primitive numerical types.) 
+            // - Doesn't support BigInteger conversions
+            // - Doesn't support NumberWithFallbackType-s. Those are only produced in bugfixed mode anyway. 
+            if(formal.isPrimitive()) {
+                // Check for boxing with widening primitive conversion. Note that 
+                // actual parameters are never primitives.
+                // It doesn't do the same with boxing types... that was a bug.
+                if(formal == Boolean.TYPE) {
+                    return actual == Boolean.class;
+                } else if (formal == Double.TYPE && 
+                        (actual == Double.class || actual == Float.class || 
+                         actual == Long.class || actual == Integer.class || 
+                         actual == Short.class || actual == Byte.class)) {
+                     return true;
+                } else if (formal == Integer.TYPE && 
+                        (actual == Integer.class || actual == Short.class || 
+                         actual == Byte.class)) {
+                     return true;
+                } else if (formal == Long.TYPE && 
+                        (actual == Long.class || actual == Integer.class || 
+                         actual == Short.class || actual == Byte.class)) {
+                     return true;
+                } else if (formal == Float.TYPE && 
+                        (actual == Float.class || actual == Long.class || 
+                         actual == Integer.class || actual == Short.class || 
+                         actual == Byte.class)) {
+                     return true;
+                } else if (formal == Character.TYPE) {
+                    return actual == Character.class;
+                } else if(formal == Byte.TYPE && actual == Byte.class) {
+                    return true;
+                } else if(formal == Short.TYPE &&
+                   (actual == Short.class || actual == Byte.class)) {
+                    return true;
+                } else if (BigDecimal.class.isAssignableFrom(actual) && ClassUtil.isNumerical(formal)) {
+                    // Special case for BigDecimals as we deem BigDecimal to be
+                    // convertible to any numeric type - either object or primitive.
+                    // This can actually cause us trouble as this is a narrowing 
+                    // conversion, not widening. 
+                    return true;
+                } else {
+                    return false;
+                }
+            } else {
+                return false;
+            }
+        }
+    }
+    
+    /**
+     * Symbolizes the class of null (it's missing from Java).
+     */
+    private static class Null {
+        
+        // Can't be instantiated
+        private Null() { }
+        
+    }
+    
+}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index 4463756..a9e7a90 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -514,7 +514,11 @@
      */
     public Version getIncompatibleImprovements() {
         return incompatibleImprovements;
-    }    
+    }
+    
+    boolean is2321Bugfixed() {
+        return getIncompatibleImprovements().intValue() >= 2003021;
+    }
     
     /**
      * Returns the default instance of the wrapper. This instance is used
@@ -652,29 +656,52 @@
     public Object unwrap(TemplateModel model, Class hint) 
     throws TemplateModelException
     {
-        final Object obj = unwrapInternal(model, hint);
+        final Object obj = tryUnwrap(model, hint);
         if(obj == CAN_NOT_UNWRAP) {
           throw new TemplateModelException("Can not unwrap model of type " + 
               model.getClass().getName() + " to type " + hint.getName());
         }
         return obj;
     }
+
+    /**
+     * Same as {@link #tryUnwrap(TemplateModel, Class, int)} with 0 last argument.
+     */
+    Object tryUnwrap(TemplateModel model, Class hint) throws TemplateModelException
+    {
+        return tryUnwrap(model, hint, 0);
+    }
     
-    Object unwrapInternal(TemplateModel model, Class hint) 
+    /**
+     * @param targetNumTypes Used when unwrapping for overloaded methods and so the hint is too generic; 0 otherwise.
+     *        This will be ignored if the hint is already a concrete numerical type. (With overloaded methods the hint
+     *        is often {@link Number} or {@link Object}, because the unwrapping has to happen before choosing the
+     *        concrete overloaded method.)
+     * @return {@link #CAN_NOT_UNWRAP} or the unwrapped object.
+     */
+    Object tryUnwrap(TemplateModel model, Class hint, int targetNumTypes) 
     throws TemplateModelException
     {
-        return unwrap(model, hint, null);
+        Object res = tryUnwrap(model, hint, null);
+        if (targetNumTypes != 0
+                && (targetNumTypes & OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT) != 0
+                && res instanceof Number) {
+            return OverloadedNumberUtil.addFallbackType((Number) res, targetNumTypes);
+        } else {
+            return res;
+        }
     }
 
-    private Object unwrap(TemplateModel model, Class hint, Map recursionStops) 
+    private Object tryUnwrap(TemplateModel model, Class hint, Map recursionStops) 
     throws TemplateModelException
     {
         if(model == null || model == nullModel) {
             return null;
         }
         
-        boolean isBoolean = Boolean.TYPE == hint;
-        boolean isChar = Character.TYPE == hint;
+        if (is2321Bugfixed() && hint.isPrimitive()) {
+            hint = ClassUtil.primitiveClassToBoxingClass(hint);            
+        }
         
         // This is for transparent interop with other wrappers (and ourselves)
         // Passing the hint allows i.e. a Jython-aware method that declares a
@@ -687,13 +714,9 @@
                 return adapted;
             }
             // Attempt numeric conversion 
-            if(adapted instanceof Number && ((hint.isPrimitive() && !isChar && 
-                    !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
-                Number number = convertUnwrappedNumber(hint,
-                        (Number)adapted);
-                if(number != null) {
-                    return number;
-                }
+            if(adapted instanceof Number && ClassUtil.isNumerical(hint)) {
+                Number number = forceUnwrappedNumberToType((Number) adapted, hint, is2321Bugfixed());
+                if(number != null) return number;
             }
         }
         
@@ -703,10 +726,8 @@
                 return wrapped;
             }
             // Attempt numeric conversion 
-            if(wrapped instanceof Number && ((hint.isPrimitive() && !isChar && 
-                    !isBoolean) || NUMBER_CLASS.isAssignableFrom(hint))) {
-                Number number = convertUnwrappedNumber(hint,
-                        (Number)wrapped);
+            if(wrapped instanceof Number && ClassUtil.isNumerical(hint)) {
+                Number number = forceUnwrappedNumberToType((Number) wrapped, hint, is2321Bugfixed());
                 if(number != null) {
                     return number;
                 }
@@ -718,6 +739,7 @@
         // select the appropriate interface in multi-interface models when we
         // know what is expected as the return type.
 
+        // Java 5: Also should check for CharSequence at the end
         if(STRING_CLASS == hint) {
             if(model instanceof TemplateScalarModel) {
                 return ((TemplateScalarModel)model).getAsString();
@@ -727,18 +749,17 @@
         }
 
         // Primitive numeric types & Number.class and its subclasses
-        if((hint.isPrimitive() && !isChar && !isBoolean) 
-                || NUMBER_CLASS.isAssignableFrom(hint)) {
+        if(ClassUtil.isNumerical(hint)) {
             if(model instanceof TemplateNumberModel) {
-                Number number = convertUnwrappedNumber(hint, 
-                        ((TemplateNumberModel)model).getAsNumber());
+                Number number = forceUnwrappedNumberToType(
+                        ((TemplateNumberModel)model).getAsNumber(), hint, is2321Bugfixed());
                 if(number != null) {
                     return number;
                 }
             }
         }
         
-        if(isBoolean || BOOLEAN_CLASS == hint) {
+        if(Boolean.TYPE == hint || BOOLEAN_CLASS == hint) {
             if(model instanceof TemplateBooleanModel) {
                 return ((TemplateBooleanModel)model).getAsBoolean() 
                 ? Boolean.TRUE : Boolean.FALSE;
@@ -795,8 +816,7 @@
                 try {
                     int size = seq.size();
                     for (int i = 0; i < size; i++) {
-                        Object val = unwrap(seq.get(i), componentType, 
-                                recursionStops);
+                        Object val = tryUnwrap(seq.get(i), componentType, recursionStops);
                         if(val == CAN_NOT_UNWRAP) {
                             return CAN_NOT_UNWRAP;
                         }
@@ -812,7 +832,7 @@
         }
         
         // Allow one-char strings to be coerced to characters
-        if(isChar || hint == CHARACTER_CLASS) {
+        if(Character.TYPE == hint || hint == CHARACTER_CLASS) {
             if(model instanceof TemplateScalarModel) {
                 String s = ((TemplateScalarModel)model).getAsString();
                 if(s.length() == 1) {
@@ -879,57 +899,64 @@
         return CAN_NOT_UNWRAP;
     }
 
-    private static Number convertUnwrappedNumber(Class hint, Number number)
-    {
-        if(hint == Integer.TYPE || hint == Integer.class) {
-            return number instanceof Integer ? (Integer)number : 
-                new Integer(number.intValue());
-        }
-        if(hint == Long.TYPE || hint == Long.class) {
-            return number instanceof Long ? (Long)number : 
-                new Long(number.longValue());
-        }
-        if(hint == Float.TYPE || hint == Float.class) {
-            return number instanceof Float ? (Float)number : 
-                new Float(number.floatValue());
-        }
-        if(hint == Double.TYPE 
-                || hint == Double.class) {
-            return number instanceof Double ? (Double)number : 
-                new Double(number.doubleValue());
-        }
-        if(hint == Byte.TYPE || hint == Byte.class) {
-            return number instanceof Byte ? (Byte)number : 
-                new Byte(number.byteValue());
-        }
-        if(hint == Short.TYPE || hint == Short.class) {
-            return number instanceof Short ? (Short)number : 
-                new Short(number.shortValue());
-        }
-        if(hint == BigInteger.class) {
-            return number instanceof BigInteger ? number : 
-                new BigInteger(number.toString());
-        }
-        if(hint == BigDecimal.class) {
-            if(number instanceof BigDecimal) {
-                return number;
+    /**
+     * Converts a number to the target type aggressively (possibly with overflow or significant loss of precision).
+     * @param n Non-{@code null}
+     * @return {@code null} if the conversion has failed.
+     */
+    static Number forceUnwrappedNumberToType(final Number n, final Class targetType, boolean bugfixed) {
+        // We try to order the conditions by decreasing probability.
+        if (targetType == n.getClass()) {
+            return n;
+        } else if (targetType == Integer.TYPE || targetType == Integer.class) {
+            return n instanceof Integer ? (Integer) n : new Integer(n.intValue());
+        } else if (targetType == Long.TYPE || targetType == Long.class) {
+            return n instanceof Long ? (Long) n : new Long(n.longValue());
+        } else if (targetType == Double.TYPE || targetType == Double.class) {
+            return n instanceof Double ? (Double) n : new Double(n.doubleValue());
+        } else if(targetType == BigDecimal.class) {
+            if(n instanceof BigDecimal) {
+                return n;
+            } else if (n instanceof BigInteger) {
+                return new BigDecimal((BigInteger) n);
+            } else if (n instanceof Long) {
+                // Because we can't represent long accurately as double
+                return BigDecimal.valueOf(n.longValue());
+            } else {
+                return new BigDecimal(n.doubleValue());
             }
-            if(number instanceof BigInteger) {
-                return new BigDecimal((BigInteger)number);
+        } else if (targetType == Float.TYPE || targetType == Float.class) {
+            return n instanceof Float ? (Float) n : new Float(n.floatValue());
+        } else if (targetType == Byte.TYPE || targetType == Byte.class) {
+            return n instanceof Byte ? (Byte) n : new Byte(n.byteValue());
+        } else if (targetType == Short.TYPE || targetType == Short.class) {
+            return n instanceof Short ? (Short) n : new Short(n.shortValue());
+        } else if (targetType == BigInteger.class) {
+            if (n instanceof BigInteger) {
+                return n;
+            } else if (bugfixed) {
+                if (n instanceof OverloadedNumberUtil.IntegerBigDecimal) {
+                    return ((OverloadedNumberUtil.IntegerBigDecimal) n).bigIntegerValue();
+                } else if (n instanceof BigDecimal) {
+                    return ((BigDecimal) n).toBigInteger(); 
+                } else {
+                    return BigInteger.valueOf(n.longValue()); 
+                }
+            } else {
+                // This is wrong, because something like "123.4" will cause NumberFormatException instead of flooring.
+                return new BigInteger(n.toString());
             }
-            if(number instanceof Long) {
-                // Because we can't represent long accurately as a 
-                // double
-                return new BigDecimal(number.toString());
+        } else {
+            final Number oriN = n instanceof OverloadedNumberUtil.NumberWithFallbackType
+                    ? ((OverloadedNumberUtil.NumberWithFallbackType) n).getSourceNumber() : n; 
+            if (targetType.isInstance(oriN)) {
+                // Handle nonstandard Number subclasses as well as directly java.lang.Number.
+                return oriN;
+            } else {
+                // Fails
+                return null;
             }
-            return new BigDecimal(number.doubleValue());
         }
-        // Handle nonstandard Number subclasses as well as directly 
-        // java.lang.Number too
-        if(hint.isInstance(number)) {
-            return number;
-        }
-        return null;
     }
     
     /**
@@ -1726,6 +1753,7 @@
      * Converts any {@link BigDecimal}s in the passed array to the type of
      * the corresponding formal argument of the method.
      */
+    // Unused?
     public static void coerceBigDecimals(AccessibleObject callable, Object[] args)
     {
         Class[] formalTypes = null;
@@ -1775,7 +1803,7 @@
             }
         }
     }
-    
+
     public static Object coerceBigDecimal(BigDecimal bd, Class formalType) {
         // int is expected in most situations, so we check it first
         if(formalType == Integer.TYPE || formalType == Integer.class) {
@@ -1798,8 +1826,9 @@
         }
         else if(BIGINTEGER_CLASS.isAssignableFrom(formalType)) {
             return bd.toBigInteger();
+        } else {
+            return bd;
         }
-        return bd;
     }
 
     private static ClassBasedModelFactory createEnumModels(BeansWrapper wrapper) {
diff --git a/src/main/java/freemarker/ext/beans/ClassString.java b/src/main/java/freemarker/ext/beans/ClassString.java
deleted file mode 100644
index 305238f..0000000
--- a/src/main/java/freemarker/ext/beans/ClassString.java
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Copyright (c) 2003 The Visigoth Software Society. All rights
- * reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in
- *    the documentation and/or other materials provided with the
- *    distribution.
- *
- * 3. The end-user documentation included with the redistribution, if
- *    any, must include the following acknowledgement:
- *       "This product includes software developed by the
- *        Visigoth Software Society (http://www.visigoths.org/)."
- *    Alternately, this acknowledgement may appear in the software itself,
- *    if and wherever such third-party acknowledgements normally appear.
- *
- * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the 
- *    project contributors may be used to endorse or promote products derived
- *    from this software without prior written permission. For written
- *    permission, please contact visigoths@visigoths.org.
- *
- * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
- *    nor may "FreeMarker" or "Visigoth" appear in their names
- *    without prior written permission of the Visigoth Software Society.
- *
- * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED.  IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
- * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
- * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
- * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Visigoth Software Society. For more
- * information on the Visigoth Software Society, please see
- * http://www.visigoths.org/
- */
-package freemarker.ext.beans;
-
-import java.math.BigDecimal;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-
-
-/**
- * @author Attila Szegedi
- */
-final class ClassString
-{
-    private final Class[] classes;
-    private final boolean bugfixed;
-    
-    ClassString(Object[] objects, boolean bugfixed) {
-        int l = objects.length;
-        classes = new Class[l];
-        for(int i = 0; i < l; ++i) {
-            Object obj = objects[i];
-            classes[i] = obj == null
-                    ? (bugfixed ? Null.class : Object.class)
-                    : obj.getClass();
-        }
-        this.bugfixed = bugfixed;
-    }
-    
-    public int hashCode() {
-        int hash = 0;
-        for(int i = 0; i < classes.length; ++i) {
-            hash ^= classes[i].hashCode();
-        }
-        return hash;
-    }
-    
-    public boolean equals(Object o) {
-        if(o instanceof ClassString) {
-            ClassString cs = (ClassString)o;
-            if(cs.classes.length != classes.length) {
-                return false;
-            }
-            for(int i = 0; i < classes.length; ++i) {
-                if(cs.classes[i] != classes[i]) {
-                    return false;
-                }
-            }
-            return true;
-        }
-        return false;
-    }
-    
-    /**
-     * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. 
-     */
-    MaybeEmptyCallableMemberDescriptor getMostSpecific(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg)
-    {
-        LinkedList/*<CallableMemberDescriptor>*/ applicables = getApplicables(memberDescs, varArg);
-        if(applicables.isEmpty()) {
-            return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
-        }
-        if(applicables.size() == 1) {
-            return (CallableMemberDescriptor) applicables.getFirst();
-        }
-        
-        LinkedList/*<CallableMemberDescriptor>*/ maximals = new LinkedList();
-        for (Iterator applicablesIter = applicables.iterator(); applicablesIter.hasNext();)
-        {
-            CallableMemberDescriptor applicable = (CallableMemberDescriptor) applicablesIter.next();
-            boolean lessSpecific = false;
-            for (Iterator maximalsIter = maximals.iterator(); 
-                maximalsIter.hasNext();)
-            {
-                CallableMemberDescriptor maximal = (CallableMemberDescriptor) maximalsIter.next();
-                switch(compareParameterTypesSpecificity(applicable.paramTypes, maximal.paramTypes, varArg)) {
-                    case 1: {
-                        maximalsIter.remove();
-                        break;
-                    }
-                    case -1: {
-                        lessSpecific = true;
-                        break;
-                    }
-                }
-            }
-            if(!lessSpecific) {
-                maximals.addLast(applicable);
-            }
-        }
-        if(maximals.size() > 1) {
-            return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
-        }
-        return (CallableMemberDescriptor) maximals.getFirst();
-    }
-    
-    private int compareParameterTypesSpecificity(Class[] paramTypes1, Class[] paramTypes2, boolean varArg) {
-        final int paramTypes1Len = paramTypes1.length;
-        final int paramTypes2Len = paramTypes2.length;
-        //assert varArg || cl1 == cl2;
-        
-        if (bugfixed) {
-            int currentWinner = 0;
-            int currentHighScore = 0;
-            for(int i = 0; i < paramTypes1Len; ++i) {
-                Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
-                Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
-                if(paramType1 != paramType2) {
-                    int score;
-                    
-                    score = MethodUtilities.isMoreOrSameSpecificParameterType(
-                            paramType1, paramType2, true,
-                            currentWinner == -1 ? currentHighScore - 1 : currentHighScore);
-                    if (score > currentHighScore) {
-                        currentHighScore = score;
-                        currentWinner = 1;
-                    } else if (score == currentHighScore && currentWinner == -1) {
-                        currentWinner = 0;
-                    }
-
-                    score = MethodUtilities.isMoreOrSameSpecificParameterType(
-                            paramType2, paramType1, true,
-                            currentWinner == 1 ? currentHighScore - 1 : currentHighScore);
-                    if (score > currentHighScore) {
-                        currentHighScore = score;
-                        currentWinner = -1;
-                    } else if (score == currentHighScore && currentWinner == 1) {
-                        currentWinner = 0;
-                    }
-                }
-            }
-            return currentWinner;
-        } else {
-            boolean paramTypes1HasAMoreSpecific = false;
-            boolean paramTypes2HasAMoreSpecific = false;
-            for(int i = 0; i < paramTypes1Len; ++i) {
-                Class paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
-                Class paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
-                if(paramType1 != paramType2) {
-                    paramTypes1HasAMoreSpecific = 
-                        paramTypes1HasAMoreSpecific
-                        || MethodUtilities.isMoreOrSameSpecificParameterType(paramType1, paramType2, false, 0) != 0;
-                    paramTypes2HasAMoreSpecific = 
-                        paramTypes2HasAMoreSpecific
-                        || MethodUtilities.isMoreOrSameSpecificParameterType(paramType2, paramType1, false, 0) != 0;
-                }
-            }
-            
-            if(paramTypes1HasAMoreSpecific) {
-                return paramTypes2HasAMoreSpecific ? 0 : 1;
-            } else if(paramTypes2HasAMoreSpecific) {
-                return -1;
-            } else {
-                return 0;
-            }
-        }
-    }
-    
-    private static Class getParamType(Class[] paramTypes, int paramTypesLen, int i, boolean varArg) {
-        return varArg && i >= paramTypesLen - 1
-                ? paramTypes[paramTypesLen - 1].getComponentType()
-                : paramTypes[i];
-    }
-    
-    /**
-     * Returns all methods that are applicable to actual
-     * parameter classes represented by this ClassString object.
-     */
-    LinkedList/*<CallableMemberDescriptor>*/ getApplicables(List/*<CallableMemberDescriptor>*/ memberDescs, boolean varArg) {
-        LinkedList/*<CallableMemberDescriptor>*/ list = new LinkedList();
-        for (Iterator it = memberDescs.iterator(); it.hasNext();) {
-            CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) it.next();
-            if(isApplicable(memberDesc, varArg)) {
-                list.add(memberDesc);
-            }
-        }
-        return list;
-    }
-    
-    /**
-     * Returns true if the supplied method is applicable to actual
-     * parameter classes represented by this ClassString object.
-     */
-    private boolean isApplicable(CallableMemberDescriptor memberDesc, boolean varArg) {
-        final Class[] formalTypes = memberDesc.paramTypes; 
-        final int cl = classes.length;
-        final int fl = formalTypes.length - (varArg ? 1 : 0);
-        if(varArg) {
-            if(cl < fl) {
-                return false;
-            }
-        } else {
-            if(cl != fl) {
-                return false;
-            }
-        }
-        for(int i = 0; i < fl; ++i) {
-            if(!isMethodInvocationConvertible(formalTypes[i], classes[i])) {
-                return false;
-            }
-        }
-        if(varArg) {
-            Class varArgType = formalTypes[fl].getComponentType();
-            for(int i = fl; i < cl; ++i) {
-                if(!isMethodInvocationConvertible(varArgType, classes[i])) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Determines whether a type represented by a class object is
-     * convertible to another type represented by a class object using a 
-     * method invocation conversion, treating object types of primitive 
-     * types as if they were primitive types (that is, a Boolean actual 
-     * parameter type matches boolean primitive formal type). This behavior
-     * is because this method is used to determine applicable methods for 
-     * an actual parameter list, and primitive types are represented by 
-     * their object duals in reflective method calls.
-     * @param formal the formal parameter type to which the actual 
-     * parameter type should be convertible
-     * @param actual the actual parameter type.
-     * @return true if either formal type is assignable from actual type, 
-     * or formal is a primitive type and actual is its corresponding object
-     * type or an object type of a primitive type that can be converted to
-     * the formal type.
-     */
-    private boolean isMethodInvocationConvertible(Class formal, Class actual) {
-        // Check for identity or widening reference conversion
-        if(formal.isAssignableFrom(actual)) {
-            return true;
-        }
-        
-        if (actual == Null.class && bugfixed) {
-            return !formal.isPrimitive();
-        }
-        
-        // Check for boxing with widening primitive conversion. Note that 
-        // actual parameters are never primitives.
-        // FIXME: Why we don't do the same with boxing types too?
-        if(formal.isPrimitive()) {
-            if(formal == Boolean.TYPE) {
-                return actual == Boolean.class;
-            } else if (formal == Double.TYPE && 
-                    (actual == Double.class || actual == Float.class || 
-                     actual == Long.class || actual == Integer.class || 
-                     actual == Short.class || actual == Byte.class)) {
-                 return true;
-            } else if (formal == Integer.TYPE && 
-                    (actual == Integer.class || actual == Short.class || 
-                     actual == Byte.class)) {
-                 return true;
-            } else if (formal == Long.TYPE && 
-                    (actual == Long.class || actual == Integer.class || 
-                     actual == Short.class || actual == Byte.class)) {
-                 return true;
-            } else if (formal == Float.TYPE && 
-                    (actual == Float.class || actual == Long.class || 
-                     actual == Integer.class || actual == Short.class || 
-                     actual == Byte.class)) {
-                 return true;
-            } else if (formal == Character.TYPE) {
-                return actual == Character.class;
-            } else if(formal == Byte.TYPE && actual == Byte.class) {
-                return true;
-            } else if(formal == Short.TYPE &&
-               (actual == Short.class || actual == Byte.class)) {
-                return true;
-            } else if (BigDecimal.class.isAssignableFrom(actual) && isNumerical(formal)) {
-                // Special case for BigDecimals as we deem BigDecimal to be
-                // convertible to any numeric type - either object or primitive.
-                // This can actually cause us trouble as this is a narrowing 
-                // conversion, not widening. 
-                return true;
-            } else {
-                return false;
-            }
-        }
-        return false;
-    }
-    
-    private static boolean isNumerical(Class type) {
-        return Number.class.isAssignableFrom(type)
-                || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE;
-    }
- 
-    /**
-     * Symbolizes the class of null (it's missing from Java).
-     */
-    private static class Null {
-        
-        // Can't be instantiated
-        private Null() { }
-        
-    }
-    
-}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/MethodUtilities.java b/src/main/java/freemarker/ext/beans/MethodUtilities.java
index abcf4e6..3c07324 100644
--- a/src/main/java/freemarker/ext/beans/MethodUtilities.java
+++ b/src/main/java/freemarker/ext/beans/MethodUtilities.java
@@ -69,7 +69,7 @@
     /**
      * Determines whether the type given as the 1st argument is convertible to the type given as the 2nd argument
      * for method call argument conversion. This follows the rules of the Java reflection-based method call, except
-     * that since we don't have the value here, a boxed calls is never seens as convertable to a primtivie type. 
+     * that since we don't have the value here, a boxed class is never seen as convertible to a primitive type. 
      * 
      * @return 0 means {@code false}, non-0 means {@code true}.
      *         That is, 0 is returned less specificity or incomparable specificity, also when if
diff --git a/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java b/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
index 7b55ee7..2aa493a 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedFixArgsMethods.java
@@ -51,7 +51,6 @@
  */
 package freemarker.ext.beans;
 
-import java.lang.reflect.Member;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -63,8 +62,7 @@
  * Stores the non-varargs methods for a {@link OverloadedMethods} object.
  * @author Attila Szegedi
  */
-class OverloadedFixArgsMethods extends OverloadedMethodsSubset
-{
+class OverloadedFixArgsMethods extends OverloadedMethodsSubset {
     
     OverloadedFixArgsMethods(BeansWrapper beansWrapper) {
         super(beansWrapper);
@@ -74,7 +72,7 @@
         return memberDesc.paramTypes;
     }
     
-    void afterWideningUnwrappingHints(Class[] paramTypes) {
+    void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
         // Do nothing
     }
 
@@ -89,15 +87,24 @@
         if(unwrappingHintsByParamCount.length <= argCount) {
             return EmptyMemberAndArguments.NO_SUCH_METHOD;
         }
-        Class[] unwarppingArgumentTypes = unwrappingHintsByParamCount[argCount];
-        if(unwarppingArgumentTypes == null) {
+        Class[] unwarppingHints = unwrappingHintsByParamCount[argCount];
+        if(unwarppingHints == null) {
             return EmptyMemberAndArguments.NO_SUCH_METHOD;
         }
         
         Object[] pojoArgs = new Object[argCount];
+        
+        int[] possibleNumericalTypes = getPossibleNumericalTypes(argCount);
+        if (possibleNumericalTypes == ALL_ZEROS_ARRAY) {
+            possibleNumericalTypes = null;
+        }
+
         Iterator it = tmArgs.iterator();
         for(int i = 0; i < argCount; ++i) {
-            Object pojo = w.unwrapInternal((TemplateModel)it.next(), unwarppingArgumentTypes[i]);
+            Object pojo = w.tryUnwrap(
+                    (TemplateModel) it.next(),
+                    unwarppingHints[i],
+                    possibleNumericalTypes != null ? possibleNumericalTypes[i] : 0);
             if(pojo == BeansWrapper.CAN_NOT_UNWRAP) {
                 return EmptyMemberAndArguments.NO_SUCH_METHOD;
             }
@@ -107,7 +114,16 @@
         MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, false);
         if(maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
             CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
-            BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+            if (bugfixed) {
+                if (possibleNumericalTypes != null) {
+                    // Note that overloaded method selection has already accounted for overflow errors when the method
+                    // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+                    // BigDecimal is allowed to overflow for backward-compatibility.
+                    forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.paramTypes, possibleNumericalTypes);
+                }
+            } else {
+                BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+            }
             return new MemberAndArguments(memberDesc.member, pojoArgs);
         } else {
             return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc);
diff --git a/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java b/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
index 2cb282b..ccc18e7 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedMethodsSubset.java
@@ -62,18 +62,32 @@
 
 import freemarker.template.TemplateModelException;
 import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
 
 /**
  * @author Attila Szegedi
  */
 abstract class OverloadedMethodsSubset {
+    
+    /** 
+     * Used for an optimization trick to substitute an array of whatever size that contains only 0-s. Since this array
+     * is 0 long, this means that the code that reads the int[] always have to check if the int[] has this value, and
+     * then it has to act like if was all 0-s.  
+     */
+    static final int[] ALL_ZEROS_ARRAY = new int[0];
 
     private Class[/*number of args*/][/*arg index*/] unwrappingHintsByParamCount;
     
+    /**
+     * Tells what numerical types occur at a given parameter position with a bit field, also if the
+     * unwrapping will do proper conversion. See {@link OverloadedNumberUtil#FLAG_INTEGER}, etc.
+     */
+    private int[/*number of args*/][/*arg index*/] possibleNumericalTypesByParamCount;
+    
     // TODO: This can cause memory-leak when classes are re-loaded. However, first the genericClassIntrospectionCache
     // and such need to be fixed in this regard. 
     // Java 5: Use ConcurrentHashMap:
-    private final Map/*<ClassString, MaybeEmptyCallableMemberDescriptor>*/ argTypesToMemberDescCache = new HashMap();
+    private final Map/*<ArgumentTypes, MaybeEmptyCallableMemberDescriptor>*/ argTypesToMemberDescCache = new HashMap();
     
     private final List/*<CallableMemberDescriptor>*/ memberDescs = new LinkedList();
     
@@ -81,56 +95,73 @@
     protected final boolean bugfixed;
     
     OverloadedMethodsSubset(BeansWrapper beansWrapper) {
-        bugfixed = beansWrapper.getIncompatibleImprovements().intValue() >= 2003021;
+        bugfixed = beansWrapper.is2321Bugfixed();
     }
     
     void addCallableMemberDescriptor(CallableMemberDescriptor memberDesc) {
-        final int paramCount = memberDesc.paramTypes.length;
-        
         memberDescs.add(memberDesc);
         
-        final Class[] preprocessedParamTypes = preprocessParameterTypes(memberDesc);
+        // Warning: Do not modify this array, or put it into unwrappingHintsByParamCount by reference, as the arrays
+        // inside that are modified!
+        final Class[] prepedParamTypes = preprocessParameterTypes(memberDesc);
+        final int paramCount = prepedParamTypes.length;  // Must be the same as the length of the original param list
         
-        final Class[] unwrappingHints = (Class[]) preprocessedParamTypes.clone();
         // Merge these unwrapping hints with the existing table of hints:
         if (unwrappingHintsByParamCount == null) {
             unwrappingHintsByParamCount = new Class[paramCount + 1][];
-            unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+            unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
         } else if (unwrappingHintsByParamCount.length <= paramCount) {
             Class[][] newUnwrappingHintsByParamCount = new Class[paramCount + 1][];
             System.arraycopy(unwrappingHintsByParamCount, 0, newUnwrappingHintsByParamCount, 0,
                     unwrappingHintsByParamCount.length);
             unwrappingHintsByParamCount = newUnwrappingHintsByParamCount;
-            unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+            unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
         } else {
-            Class[] prevUnwrappingHints = unwrappingHintsByParamCount[paramCount]; 
-            if (prevUnwrappingHints == null) {
-                unwrappingHintsByParamCount[paramCount] = unwrappingHints;
+            Class[] unwrappingHints = unwrappingHintsByParamCount[paramCount]; 
+            if (unwrappingHints == null) {
+                unwrappingHintsByParamCount[paramCount] = (Class[]) prepedParamTypes.clone();
             } else {
-                for(int i = 0; i < prevUnwrappingHints.length; ++i) {
+                for (int paramIdx = 0; paramIdx < prepedParamTypes.length; paramIdx++) {
                     // For each parameter list length, we merge the argument type arrays into a single Class[] that
-                    // stores the most specific common types of each position. Hence we will possibly use a too generic
+                    // stores the most specific common types for each position. Hence we will possibly use a too generic
                     // hint for the unwrapping. For correct behavior, for each overloaded methods its own parameter
                     // types should be used as a hint. But without unwrapping the arguments, we couldn't select the
                     // overloaded method. So this is a circular reference problem. We could try selecting the
                     // method based on the wrapped value, but that's quite tricky, and the result of such selection
-                    // is not cacheable (the TM types are not enough as cache key then. So we just use this
+                    // is not cacheable (the TM types are not enough as cache key then). So we just use this
                     // compromise.
-                    prevUnwrappingHints[i] = getCommonSupertypeForUnwrappingHint(
-                            prevUnwrappingHints[i], unwrappingHints[i]);
+                    unwrappingHints[paramIdx] = getCommonSupertypeForUnwrappingHint(
+                            unwrappingHints[paramIdx], prepedParamTypes[paramIdx]);
                 }
             }
         }
+
+        int[] paramNumericalTypes = ALL_ZEROS_ARRAY;
+        if (bugfixed) {
+            // Fill possibleNumericalTypesByParamCount (if necessary)
+            for (int paramIdx = 0; paramIdx < paramCount; paramIdx++) {
+                final int numType = OverloadedNumberUtil.classToTypeFlags(prepedParamTypes[paramIdx]);
+                if (numType != 0) {  // It's a numerical type
+                    if (paramNumericalTypes == ALL_ZEROS_ARRAY) {
+                        paramNumericalTypes = new int[paramCount];
+                    }
+                    paramNumericalTypes[paramIdx] = numType;
+                }
+            }
+            mergeInNumericalTypes(paramCount, paramNumericalTypes);
+        }
         
-        afterWideningUnwrappingHints(bugfixed ? preprocessedParamTypes : unwrappingHints);
+        afterWideningUnwrappingHints(
+                bugfixed ? prepedParamTypes : unwrappingHintsByParamCount[paramCount],
+                paramNumericalTypes);
     }
     
     Class[][] getUnwrappingHintsByParamCount() {
         return unwrappingHintsByParamCount;
     }
     
-    MaybeEmptyCallableMemberDescriptor getMemberDescriptorForArgs(Object[] args, boolean varArg) {
-        ClassString argTypes = new ClassString(args, bugfixed);
+    final MaybeEmptyCallableMemberDescriptor getMemberDescriptorForArgs(Object[] args, boolean varArg) {
+        ArgumentTypes argTypes = new ArgumentTypes(args, bugfixed);
         MaybeEmptyCallableMemberDescriptor memberDesc;
         synchronized(argTypesToMemberDescCache) {
             memberDesc = (MaybeEmptyCallableMemberDescriptor) argTypesToMemberDescCache.get(argTypes);
@@ -147,19 +178,18 @@
     }
     
     abstract Class[] preprocessParameterTypes(CallableMemberDescriptor memberDesc);
-    abstract void afterWideningUnwrappingHints(Class[] paramTypes);
+    abstract void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes);
     
     abstract MaybeEmptyMemberAndArguments getMemberAndArguments(List/*<TemplateModel>*/ tmArgs, 
             BeansWrapper w) throws TemplateModelException;
 
     /**
      * Returns the most specific common class (or interface) of two parameter types for the purpose of unwrapping.
-     * This is trickier then finding the most specific overlapping superclass of two classes, because:
+     * This is trickier than finding the most specific overlapping superclass of two classes, because:
      * <ul>
      *   <li>It considers primitive classes as the subclasses of the boxing classes.</li>
-     *   <li>It considers widening numerical conversion as if the narrower type is subclass.</li>
      *   <li>If the only common class is {@link Object}, it will try to find a common interface. If there are more
-     *       of them, it will start removing those that are known to be uninteresting as unwrapping hint.</li>
+     *       of them, it will start removing those that are known to be uninteresting as unwrapping hints.</li>
      * </ul>
      * 
      * @param c1 Parameter type 1
@@ -180,7 +210,7 @@
                 c1WasPrim = false;
             }
             
-            // c1 primitive class to boxing class:
+            // c2 primitive class to boxing class:
             final boolean c2WasPrim; 
             if (c2.isPrimitive()) {
                 c2 = ClassUtil.primitiveClassToBoxingClass(c2);
@@ -190,13 +220,15 @@
             }
     
             if (c1 == c2) {
-                // If it was like int and Integer, boolean and Boolean, etc.
+                // If it was like int and Integer, boolean and Boolean, etc., we return the boxing type (as that's the
+                // less specific, because it allows null.)
                 // (If it was two equivalent primitives, we don't get here, because of the 1st line of the method.) 
                 return c1;
             } else if (Number.class.isAssignableFrom(c1) && Number.class.isAssignableFrom(c2)) {
-                // We don't want the unwrapper to convert to a numerical super-type as it's not yet known what the
-                // actual number type of the chosen method will be. (Especially as fixed point to floating point can be
-                // lossy.)
+                // We don't want the unwrapper to convert to a numerical super-type [*] as it's not yet known what the
+                // actual number type of the chosen method will be. We will postpone the actual numerical conversion
+                // until that, especially as some conversions (like fixed point to floating point) can be lossy.
+                // * Numerical super-type: Like long > int > short > byte.  
                 return Number.class;
             } else if (c1WasPrim || c2WasPrim) {
                 // At this point these all stand:
@@ -218,7 +250,9 @@
             }
         }
         
-        // If buxfixed is true and any of the classes was a primitive type, we never reach this point.
+        // We never get to this point if buxfixed is true and any of these stands:
+        // - One of classes was a primitive type
+        // - One of classes was a numerical type (either boxing type or primitive)
         
         Set commonTypes = MethodUtilities.getAssignables(c1, c2);
         commonTypes.retainAll(MethodUtilities.getAssignables(c2, c1));
@@ -237,15 +271,18 @@
             Class clazz = (Class)commonTypesIter.next();
             for (Iterator maxIter = max.iterator(); maxIter.hasNext();) {
                 Class maxClazz = (Class)maxIter.next();
-                if(MethodUtilities.isMoreOrSameSpecificParameterType(maxClazz, clazz, bugfixed, 0) != 0) {
+                if(MethodUtilities.isMoreOrSameSpecificParameterType(maxClazz, clazz, false /*bugfixed [1]*/, 0) != 0) {
                     // clazz can't be maximal, if there's already a more specific or equal maximal than it.
                     continue listCommonTypes;
                 }
-                if(MethodUtilities.isMoreOrSameSpecificParameterType(clazz, maxClazz, bugfixed, 0) != 0) {
+                if(MethodUtilities.isMoreOrSameSpecificParameterType(clazz, maxClazz, false /*bugfixed [1]*/, 0) != 0) {
                     // If it's more specific than a currently maximal element,
                     // that currently maximal is no longer a maximal.
                     maxIter.remove();
                 }
+                // 1: We don't use bugfixed at the "[1]"-marked points because it's slower and doesn't make any
+                //    difference here as it's ensured that nor c1 nor c2 is primitive or numerical. The bugfix has only
+                //    affected the treatment of primitives and numerical types. 
             }
             // If we get here, no current maximal is more specific than the
             // current class, so clazz is a new maximal so far.
@@ -288,4 +325,112 @@
         return (Class) max.get(0);
     }
     
+    /**
+     * Gets the numerical type "flags" of each parameter positions, or {@code null} if there's no method with this
+     * parameter count or if we aren't in pre-2.3.21 mode, {@link #ALL_ZEROS_ARRAY} if there were no numerical
+     * parameters. The returned {@code int}-s are on or more {@link OverloadedNumberUtil}} {@code FLAG_...} constants
+     * binary "or"-ed together.  
+     */
+    final protected int[] getPossibleNumericalTypes(int paramCount) {
+        return possibleNumericalTypesByParamCount != null && possibleNumericalTypesByParamCount.length > paramCount
+                ? possibleNumericalTypesByParamCount[paramCount]
+                : null;
+    }
+
+    /**
+     * @param dstParamCount The parameter count for which we want to merge in the possible numerical types 
+     * @param srcNumTypesByParamIdx If shorter than {@code dstParamCount}, it's last item will be repeated until
+     *        dstParamCount length is reached. If longer, the excessive items will be ignored.
+     *        Maybe {@link #ALL_ZEROS_ARRAY}. Cant'be a 0-length array. Can't be {@code null}.
+     */
+    final protected void mergeInNumericalTypes(int dstParamCount, int[] srcNumTypesByParamIdx) {
+        NullArgumentException.check("srcNumTypesByParamIdx", srcNumTypesByParamIdx);
+        if (dstParamCount == 0) return;
+        
+        // Ensure that possibleNumericalTypesByParamCount[dstParamCount] exists:
+        if (possibleNumericalTypesByParamCount == null) {
+            possibleNumericalTypesByParamCount = new int[dstParamCount + 1][];
+        } else if (possibleNumericalTypesByParamCount.length <= dstParamCount) {
+            if (srcNumTypesByParamIdx == null) return;
+            
+            int[][] newNumericalTypeByParamCount = new int[dstParamCount + 1][];
+            System.arraycopy(possibleNumericalTypesByParamCount, 0, newNumericalTypeByParamCount, 0,
+                    possibleNumericalTypesByParamCount.length);
+            possibleNumericalTypesByParamCount = newNumericalTypeByParamCount;
+        }
+        
+        final int srcParamCount = srcNumTypesByParamIdx.length;
+        
+        int[] dstNumTypesByParamIdx = possibleNumericalTypesByParamCount[dstParamCount];
+        if (dstNumTypesByParamIdx == null) {
+            // This is the first method added with this number of params => no merging
+            
+            if (srcNumTypesByParamIdx != ALL_ZEROS_ARRAY) {
+                dstNumTypesByParamIdx = new int[dstParamCount];
+                for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+                    dstNumTypesByParamIdx[paramIdx]
+                            = srcNumTypesByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1];
+                }
+            } else {
+                dstNumTypesByParamIdx = ALL_ZEROS_ARRAY;
+            }
+            
+            possibleNumericalTypesByParamCount[dstParamCount] = dstNumTypesByParamIdx;
+        } else {
+            // dstNumTypesByParamIdx != null, so we need to merge into it.
+            
+            if (srcNumTypesByParamIdx == dstNumTypesByParamIdx) {
+                // Used to occur when both are ALL_ZEROS_ARRAY
+                return;
+            }
+            
+            // As we will write dstNumTypesByParamIdx, it can't remain ALL_ZEROS_ARRAY anymore. 
+            if (dstNumTypesByParamIdx == ALL_ZEROS_ARRAY && dstParamCount > 0) {
+                dstNumTypesByParamIdx = new int[dstParamCount];
+                possibleNumericalTypesByParamCount[dstParamCount] = dstNumTypesByParamIdx;
+            }
+            
+            for (int paramIdx = 0; paramIdx < dstParamCount; paramIdx++) {
+                final int srcParamNumTypes
+                        = srcNumTypesByParamIdx != ALL_ZEROS_ARRAY
+                            ? srcNumTypesByParamIdx[paramIdx < srcParamCount ? paramIdx : srcParamCount - 1]
+                            : 0;
+                final int dstParamNumTypes = dstNumTypesByParamIdx[paramIdx];
+                if (dstParamNumTypes != srcParamNumTypes) {
+                    dstNumTypesByParamIdx[paramIdx]
+                            = dstParamNumTypes | srcParamNumTypes | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT; 
+                }
+                // Note that if a parameter is non-numerical, its paraNumTypes is 0. So if we have a non-number and
+                // a some kind of number (non-0), the merged result will be marked with FLAG_WIDENED_UNWRAPPING_HINT.
+                // (I.e., the same happens as with two different numerical types.)
+            }
+        }
+    }
+    
+    protected void forceNumberArgumentsToParameterTypes(
+            Object[] args, Class[] paramTypes, int[] unwrappingNumTypesByParamIndex) {
+        final int paramTypesLen = paramTypes.length;
+        final int argsLen = args.length;
+        for (int argIdx = 0; argIdx < argsLen; argIdx++) {
+            final int paramTypeIdx = argIdx < paramTypesLen ? argIdx : paramTypesLen - 1;
+            final int unwrappingNumTypes = unwrappingNumTypesByParamIndex[paramTypeIdx];
+            
+            // Forcing the number type can only be interesting if there are numerical parameter types on that index,
+            // and the unwrapping was not to an exact numerical type.
+            if (unwrappingNumTypes != 0
+                    && (unwrappingNumTypes & OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT) != 0) {
+                final Object arg = args[argIdx];
+                // If arg isn't a number, we can't do any conversions anyway, regardless of the param type.
+                if (arg instanceof Number) {
+                    final Class targetType = paramTypes[paramTypeIdx];
+                    final Number convertedArg = BeansWrapper.forceUnwrappedNumberToType(
+                            (Number) arg, targetType, bugfixed);
+                    if (convertedArg != null) {
+                        args[argIdx] = convertedArg;
+                    }
+                }
+            }
+        }
+    }
+    
 }
diff --git a/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java b/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java
new file mode 100644
index 0000000..77c673e
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/OverloadedNumberUtil.java
@@ -0,0 +1,1248 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NumberUtil;
+
+/**
+ * Everything related to coercion to ambiguous numerical types.  
+ */
+class OverloadedNumberUtil {
+
+    // Can't be instantiated
+    private OverloadedNumberUtil() { }
+    
+    /** The unwrapping hint will not be a specific numerical type. */
+    static final int FLAG_WIDENED_UNWRAPPING_HINT = 1;
+    
+    static final int FLAG_BYTE = 4;
+    static final int FLAG_SHORT = 8;
+    static final int FLAG_INTEGER = 16;
+    static final int FLAG_LONG = 32;
+    static final int FLAG_FLOAT = 64;
+    static final int FLAG_DOUBLE = 128;
+    static final int FLAG_BIG_INTEGER = 256;
+    static final int FLAG_BIG_DECIMAL = 512;
+    static final int FLAG_UNKNOWN_TYPE = 1024;
+    
+    static final int MASK_KNOWN_INTEGERS = FLAG_BYTE | FLAG_SHORT | FLAG_INTEGER | FLAG_LONG | FLAG_BIG_INTEGER;
+    static final int MASK_KNOWN_NONINTEGERS = FLAG_FLOAT | FLAG_DOUBLE | FLAG_BIG_DECIMAL;
+    static final int MASK_ALL_KNOWN_TYPES = MASK_KNOWN_INTEGERS | MASK_KNOWN_NONINTEGERS;
+    static final int MASK_ALL_TYPES = MASK_ALL_KNOWN_TYPES | FLAG_UNKNOWN_TYPE;
+
+    /** The highest long that can be stored in double without precision loss: 2**53. */
+    private static final long MAX_DOUBLE_OR_LONG = 9007199254740992L;
+    /** The lowest long that can be stored in double without precision loss: -(2**53). */
+    private static final long MIN_DOUBLE_OR_LONG = -9007199254740992L;
+    private static final int MAX_DOUBLE_OR_LONG_LOG_2 = 53;
+    
+    /** The highest long that can be stored in float without precision loss: 2**24. */
+    private static final int MAX_FLOAT_OR_INT = 16777216;
+    /** The lowest long that can be stored in float without precision loss: -(2**24). */
+    private static final int MIN_FLOAT_OR_INT = -16777216;
+    private static final int MAX_FLOAT_OR_INT_LOG_2 = 24;
+
+    static boolean hasCommonFlags(Class pClass, int numberTypeFlags) {
+         return (OverloadedNumberUtil.classToTypeFlags(pClass) & numberTypeFlags) != 0;
+    }
+
+    static int classToTypeFlags(Class pClass) {
+        if (pClass.isPrimitive()) {
+            if (pClass == Integer.TYPE) return FLAG_INTEGER;
+            else if (pClass == Long.TYPE) return FLAG_LONG;
+            else if (pClass == Double.TYPE) return FLAG_DOUBLE;
+            else if (pClass == Float.TYPE) return FLAG_FLOAT;
+            else if (pClass == Byte.TYPE) return FLAG_BYTE;
+            else if (pClass == Short.TYPE) return FLAG_SHORT;
+            else return 0;
+        } else if (Number.class.isAssignableFrom(pClass)) {
+            if (pClass == Integer.class) return FLAG_INTEGER;
+            else if (pClass == Long.class) return FLAG_LONG;
+            else if (pClass == Double.class) return FLAG_DOUBLE;
+            else if (pClass == Float.class) return FLAG_FLOAT;
+            else if (pClass == Byte.class) return FLAG_BYTE;
+            else if (pClass == Short.class) return FLAG_SHORT;
+            else if (BigDecimal.class.isAssignableFrom(pClass)) return FLAG_BIG_DECIMAL;
+            else if (BigInteger.class.isAssignableFrom(pClass)) return FLAG_BIG_INTEGER;
+            else return FLAG_UNKNOWN_TYPE;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Attaches the lowest alternative number type to the parameter number via {@link NumberWithFallbackType}, if
+     * that's useful according the possible target number types. This transformation is applied on the method call
+     * argument list before overloaded method selection.
+     * 
+     * <p>Note that as of this writing, this method is only used when
+     * {@link BeansWrapper#getIncompatibleImprovements()} >= 2.3.21.
+     * 
+     * <p>Why's this needed, how it works: Overloaded methods selection only selects methods where the <em>type</em>
+     * (not the value!) of the argument is "smaller" or the same as the parameter type. This is similar to how it's in
+     * the Java language. That it only decides based on the parameter type is important because this way
+     * {@link OverloadedMethodsSubset} can cache method lookup decisions using the types as the cache key. Problem is,
+     * since you don't declare the exact numerical types in FTL, and FTL has only a single generic numeric type
+     * anyway, what Java type a {@link TemplateNumberModel} uses internally is often seen as a technical detail of which
+     * the template author can't always keep track of. So we investigate the <em>value</em> of the number too,
+     * then coerce it down without overflow to a type that will match the most overloaded methods. (This
+     * is especially important as FTL often stores numbers in {@link BigDecimal}-s, which will hardly ever match any
+     * method parameters.) We could simply return that number, like {@code Byte(0)} for an {@code Integer(0)},
+     * however, then we would lose the information about what the original type was. The original type is sometimes
+     * important, as in ambiguous situations the method where the there's an exact type match should be selected (like,
+     * when someone wants to select an overload explicitly with {@code m(x?int)}). Also, if an overload wins where
+     * the parameter type at the position of the number is {@code Number} or {@code Object} (or {@code Comparable}
+     * etc.), it's expected that we pass in the original value (an {@code Integer} in this example), especially if that
+     * value is the return value of another Java method. That's why we use
+     * {@link NumberWithFallbackType} numerical classes like {@link IntegerOrByte}, which represents both the original
+     * type and the coerced type, all encoded into the class of the value, which is used as the overloaded method lookup
+     * cache key.
+     *  
+     * <p>See also: <tt>src\main\misc\overloadedNumberRules\prices.ods</tt>.
+     * 
+     * @param num the number to coerce
+     * @param targetNumTypes a bit-set of {@link #FLAG_INTEGER} and such.
+     * 
+     * @returns The original number or a {@link NumberWithFallbackType}, depending on the actual value and the types
+     *     indicated in the {@code targetNumTypes} parameter.
+     */
+    static Number addFallbackType(Number num, int targetNumTypes) {
+        // Java 5: use valueOf where possible
+        final Class numClass = num.getClass();
+        if (numClass == BigDecimal.class) {
+            // For now we only support the backward-compatible mode that doesn't prevent roll overs and magnitude loss.
+            // However, we push the overloaded selection to the right direction, so we will at least indicate if the
+            // number has decimals.
+            BigDecimal n = (BigDecimal) num; 
+            if ((targetNumTypes & MASK_KNOWN_INTEGERS) != 0 && (targetNumTypes & MASK_KNOWN_NONINTEGERS) != 0
+                    && NumberUtil.isBigDecimalInteger(n) /* <- can be expensive */) {
+                return new IntegerBigDecimal(n);
+            } else {
+                // Either it was a non-integer, or it didn't mater what it was, as we don't have both integer and
+                // non-integer target types. 
+                return n;
+            }
+        } else if (numClass == Integer.class) {
+            int pn = num.intValue();
+            // Note that we try to return the most specific type (i.e., the numerical type with the smallest range), but
+            // only among the types that are possible targets. Like if the only target is int and the value is 1, we
+            // will return Integer 1, not Byte 1, even though byte is automatically converted to int so it would
+            // work too. Why we avoid unnecessarily specific types is that they generate more overloaded method lookup
+            // cache entries, since the cache key is the array of types of the argument values. So we want as few
+            // permutations as possible. 
+            if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new IntegerOrByte((Integer) num, (byte) pn);
+            } else if ((targetNumTypes & FLAG_SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+                return new IntegerOrShort((Integer) num, (short) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == Long.class) {
+            final long pn = num.longValue(); 
+            if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new LongOrByte((Long) num, (byte) pn);
+            } else if ((targetNumTypes & FLAG_SHORT) != 0 && pn <= Short.MAX_VALUE && pn >= Short.MIN_VALUE) {
+                return new LongOrShort((Long) num, (short) pn);
+            } else if ((targetNumTypes & FLAG_INTEGER) != 0 && pn <= Integer.MAX_VALUE && pn >= Integer.MIN_VALUE) {
+                return new LongOrInteger((Long) num, (int) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == Double.class) {
+            final double doubleN = num.doubleValue();
+            
+            // Can we store it in an integer type?
+            checkIfWholeNumber: do {
+                if ((targetNumTypes & MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+                
+                // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+                if (doubleN > MAX_DOUBLE_OR_LONG || doubleN < MIN_DOUBLE_OR_LONG) break checkIfWholeNumber;
+                
+                long longN = num.longValue(); 
+                double diff = doubleN - longN;
+                boolean exact;  // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+                if (diff == 0) {
+                    exact = true;
+                } else if (diff > 0) {
+                    if (diff < 0.000001) {
+                        exact = false;
+                    } else if (diff > 0.999999) {
+                        exact = false;
+                        longN++;
+                    } else {
+                        break checkIfWholeNumber;
+                    }
+                } else {  // => diff < 0
+                    if (diff > -0.000001) {
+                        exact = false;
+                    } else if (diff < -0.999999) {
+                        exact = false;
+                        longN--;
+                    } else {
+                        break checkIfWholeNumber;
+                    }
+                }
+                
+                // If we reach this, it can be treated as a whole number.
+                
+                if ((targetNumTypes & FLAG_BYTE) != 0 && longN <= Byte.MAX_VALUE && longN >= Byte.MIN_VALUE) {
+                    return new DoubleOrByte((Double) num, (byte) longN);
+                } else if ((targetNumTypes & FLAG_SHORT) != 0 && longN <= Short.MAX_VALUE && longN >= Short.MIN_VALUE) {
+                    return new DoubleOrShort((Double) num, (short) longN);
+                } else if ((targetNumTypes & FLAG_INTEGER) != 0 && longN <= Integer.MAX_VALUE && longN >= Integer.MIN_VALUE) {
+                    final int intN = (int) longN; 
+                    // Java 5: remove the "? (Number)" and ": (Number)" casts
+                    return (targetNumTypes & FLAG_FLOAT) != 0 && intN >= MIN_FLOAT_OR_INT && intN <= MAX_FLOAT_OR_INT
+                                    ? (Number) new DoubleOrIntegerOrFloat((Double) num, intN)
+                                    : (Number) new DoubleOrInteger((Double) num, intN);
+                } else if ((targetNumTypes & FLAG_LONG) != 0) {
+                    if (exact) {
+                        return new DoubleOrLong((Double) num, longN);
+                    } else {
+                        // We don't deal with non-exact numbers outside the range of int, as we already reach
+                        // ULP 2.384185791015625E-7 there.
+                        if (longN >= Integer.MIN_VALUE && longN <= Integer.MAX_VALUE) {
+                            return new DoubleOrLong((Double) num, longN);
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    }
+                }
+                // This point is reached if the double value was out of the range of target integer type(s). 
+                // Falls through!
+            } while (false);
+            // If we reach this that means that it can't be treated as a whole number.
+            
+            if ((targetNumTypes & FLAG_FLOAT) != 0 && doubleN >= -Float.MAX_VALUE && doubleN <= Float.MAX_VALUE) {
+                return new DoubleOrFloat((Double) num);
+            } else {
+                // Simply Double:
+                return num;
+            }
+        } else if (numClass == Float.class) {
+            final float floatN = num.floatValue();
+            
+            // Can we store it in an integer type?
+            checkIfWholeNumber: do {
+                if ((targetNumTypes & MASK_KNOWN_INTEGERS) == 0) break checkIfWholeNumber;
+                
+                // There's no hope to be 1-precise outside this region. (Although problems can occur even inside it...)
+                if (floatN > MAX_FLOAT_OR_INT || floatN < MIN_FLOAT_OR_INT) break checkIfWholeNumber;
+                
+                int intN = num.intValue();
+                double diff = floatN - intN;
+                boolean exact;  // We will try to ignore precision glitches (like 0.3 - 0.2 - 0.1 = -2.7E-17)
+                if (diff == 0) {
+                    exact = true;
+                // We already reach ULP 7.6293945E-6 with bytes, so we don't continue with shorts.
+                } else if (intN >= Byte.MIN_VALUE && intN <= Byte.MAX_VALUE) {
+                    if (diff > 0) {
+                        if (diff < 0.00001) {
+                            exact = false;
+                        } else if (diff > 0.99999) {
+                            exact = false;
+                            intN++;
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    } else {  // => diff < 0
+                        if (diff > -0.00001) {
+                            exact = false;
+                        } else if (diff < -0.99999) {
+                            exact = false;
+                            intN--;
+                        } else {
+                            break checkIfWholeNumber;
+                        }
+                    }
+                } else {
+                    break checkIfWholeNumber;
+                }
+                
+                // If we reach this, it can be treated as a whole number.
+                
+                if ((targetNumTypes & FLAG_BYTE) != 0 && intN <= Byte.MAX_VALUE && intN >= Byte.MIN_VALUE) {
+                    return new FloatOrByte((Float) num, (byte) intN);
+                } else if ((targetNumTypes & FLAG_SHORT) != 0 && intN <= Short.MAX_VALUE && intN >= Short.MIN_VALUE) {
+                    return new FloatOrShort((Float) num, (short) intN);
+                } else if ((targetNumTypes & FLAG_INTEGER) != 0) {
+                    return new FloatOrInteger((Float) num, intN);
+                } else if ((targetNumTypes & FLAG_LONG) != 0) {
+                    // We can't even go outside the range of integers, so we don't need Long variation:
+                    return exact
+                            ? (Number) new FloatOrInteger((Float) num, intN)
+                            : (Number) new FloatOrByte((Float) num, (byte) intN);  // as !exact implies (-128..127)
+                }
+                // This point is reached if the float value was out of the range of target integer type(s). 
+                // Falls through!
+            } while (false);
+            // If we reach this that means that it can't be treated as a whole number. So it's simply a Float:
+            return num;
+        } else if (numClass == Byte.class) {
+            return num;
+        } else if (numClass == Short.class) {
+            short pn = num.shortValue(); 
+            if ((targetNumTypes & FLAG_BYTE) != 0 && pn <= Byte.MAX_VALUE && pn >= Byte.MIN_VALUE) {
+                return new ShortOrByte((Short) num, (byte) pn);
+            } else {
+                return num;
+            }
+        } else if (numClass == BigInteger.class) {
+            if ((targetNumTypes
+                    & ((MASK_KNOWN_INTEGERS | MASK_KNOWN_NONINTEGERS) ^ (FLAG_BIG_INTEGER | FLAG_BIG_DECIMAL))) != 0) {
+                BigInteger biNum = (BigInteger) num;
+                final int bitLength = biNum.bitLength();  // Doesn't include sign bit, so it's one less than expected
+                if ((targetNumTypes & FLAG_BYTE) != 0 && bitLength <= 7) {
+                    return new BigIntegerOrByte(biNum);
+                } else if ((targetNumTypes & FLAG_SHORT) != 0 && bitLength <= 15) {
+                    return new BigIntegerOrShort(biNum);
+                } else if ((targetNumTypes & FLAG_INTEGER) != 0 && bitLength <= 31) {
+                    return new BigIntegerOrInteger(biNum);
+                } else if ((targetNumTypes & FLAG_LONG) != 0 && bitLength <= 63) {
+                    return new BigIntegerOrLong(biNum);
+                } else if ((targetNumTypes & FLAG_FLOAT) != 0
+                        && (bitLength <= MAX_FLOAT_OR_INT_LOG_2
+                            || bitLength == MAX_FLOAT_OR_INT_LOG_2 + 1
+                               && biNum.getLowestSetBit() >= MAX_FLOAT_OR_INT_LOG_2)) {
+                    return new BigIntegerOrFloat(biNum);
+                } else if ((targetNumTypes & FLAG_DOUBLE) != 0
+                        && (bitLength <= MAX_DOUBLE_OR_LONG_LOG_2
+                            || bitLength == MAX_DOUBLE_OR_LONG_LOG_2 + 1
+                               && biNum.getLowestSetBit() >= MAX_DOUBLE_OR_LONG_LOG_2)) {
+                    return new BigIntegerOrDouble(biNum);
+                } else {
+                    return num;
+                }
+            } else {
+                // No relevant coercion target types; return the BigInteger as is:
+                return num;
+            }
+        } else {
+            // Unknown number type:
+            return num;
+        }
+    }
+
+    static interface ByteSource { Byte byteValue(); }
+    static interface ShortSource { Short shortValue(); }
+    static interface IntegerSource { Integer integerValue(); }
+    static interface LongSource { Long longValue(); }
+    static interface FloatSource { Float floatValue(); }
+    static interface DoubleSource { Double doubleValue(); }
+    static interface BigIntegerSource { BigInteger bigIntegerValue(); }
+    static interface BigDecimalSource { BigDecimal bigDecimalValue(); }
+    
+    /**
+     * Superclass of "Or"-ed numerical types. With an example, a {@code int} 1 has the fallback type {@code byte}, as
+     * that's the smallest type that can store the value, so it can be represented as an {@link IntegerOrByte}.
+     * This is useful as overloaded method selection only examines the type of the arguments, not the value of them,
+     * but with "Or"-ed types we can encode this value-related information into the argument type, hence influencing the
+     * method selection.
+     */
+    abstract static class NumberWithFallbackType extends Number implements Comparable {
+        
+        protected abstract Number getSourceNumber();
+
+        public int intValue() {
+            return getSourceNumber().intValue();
+        }
+
+        public long longValue() {
+            return getSourceNumber().longValue();
+        }
+
+        public float floatValue() {
+            return getSourceNumber().floatValue();
+        }
+
+        public double doubleValue() {
+            return getSourceNumber().doubleValue();
+        }
+
+        public byte byteValue() {
+            return getSourceNumber().byteValue();
+        }
+
+        public short shortValue() {
+            return getSourceNumber().shortValue();
+        }
+
+        public int hashCode() {
+            return getSourceNumber().hashCode();
+        }
+
+        public boolean equals(Object obj) {
+            if (obj != null && this.getClass() == obj.getClass()) {
+                return getSourceNumber().equals(((NumberWithFallbackType) obj).getSourceNumber());
+            } else {
+                return false;
+            }
+        }
+
+        public String toString() {
+            return getSourceNumber().toString();
+        }
+
+        // We have to implement this, so that if a potential matching method expects a Comparable, which is implemented
+        // by all the supported numerical types, the "Or" type will be a match. 
+        public int compareTo(Object o) {
+            Number n = getSourceNumber();
+            if (n instanceof Comparable) {
+                return ((Comparable) n).compareTo(o); 
+            } else {
+                throw new ClassCastException(n.getClass().getName() + " is not Comparable.");
+            }
+        }
+        
+    }
+
+    /**
+     * Holds a {@link BigDecimal} that stores a whole number. When selecting a overloaded method, FreeMarker tries to
+     * associate {@link BigDecimal} values to parameters of types that can hold non-whole numbers, unless the
+     * {@link BigDecimal} is wrapped into this class, in which case it does the opposite. This mechanism is, however,
+     * too rough to prevent roll overs or magnitude losses. Those are not yet handled for backward compatibility (they
+     * were suppressed earlier too).
+     */
+    static final class IntegerBigDecimal extends NumberWithFallbackType {
+
+        private final BigDecimal n;
+        
+        IntegerBigDecimal(BigDecimal n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        public BigInteger bigIntegerValue() {
+            return n.toBigInteger();
+        }
+        
+    }
+
+    static abstract class LongOrSmallerInteger extends NumberWithFallbackType {
+        
+        private final Long n;
+        
+        protected LongOrSmallerInteger(Long n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        public long longValue() {
+            return n.longValue();
+        }
+        
+    }
+    
+    static class LongOrByte extends LongOrSmallerInteger {
+        
+        private final byte w; 
+
+        LongOrByte(Long n, byte w) {
+            super(n);
+            this.w = w;
+        }
+
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static class LongOrShort extends LongOrSmallerInteger {
+        
+        private final short w; 
+
+        LongOrShort(Long n, short w) {
+            super(n);
+            this.w = w;
+        }
+
+        public short shortValue() {
+            return w;
+        }
+        
+    }
+    
+    static class LongOrInteger extends LongOrSmallerInteger {
+        
+        private final int w; 
+
+        LongOrInteger(Long n, int w) {
+            super(n);
+            this.w = w;
+        }
+
+        public int intValue() {
+            return w;
+        }
+        
+    }
+    
+    static abstract class IntegerOrSmallerInteger extends NumberWithFallbackType {
+        
+        private final Integer n;
+        
+        protected IntegerOrSmallerInteger(Integer n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        public int intValue() {
+            return n.intValue();
+        }
+        
+    }
+    
+    static class IntegerOrByte extends IntegerOrSmallerInteger {
+        
+        private final byte w; 
+
+        IntegerOrByte(Integer n, byte w) {
+            super(n);
+            this.w = w;
+        }
+
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static class IntegerOrShort extends IntegerOrSmallerInteger {
+        
+        private final short w; 
+
+        IntegerOrShort(Integer n, short w) {
+            super(n);
+            this.w = w;
+        }
+
+        public short shortValue() {
+            return w;
+        }
+        
+    }
+    
+    static class ShortOrByte extends NumberWithFallbackType {
+        
+        private final Short n;
+        private final byte w;
+        
+        protected ShortOrByte(Short n, byte w) {
+            this.n = n;
+            this.w = w;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+
+        public short shortValue() {
+            return n.shortValue();
+        }
+
+        public byte byteValue() {
+            return w;
+        }
+        
+    }
+    
+    static abstract class DoubleOrWholeNumber extends NumberWithFallbackType {
+        
+        private final Double n; 
+
+        protected DoubleOrWholeNumber(Double n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        public double doubleValue() {
+            return n.doubleValue();
+        }
+        
+    }
+    
+    static final class DoubleOrByte extends DoubleOrWholeNumber {
+        
+        private final byte w;
+
+        DoubleOrByte(Double n, byte w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public byte byteValue() {
+            return w;
+        }
+        
+        public short shortValue() {
+            return w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrShort extends DoubleOrWholeNumber {
+        
+        private final short w;
+
+        DoubleOrShort(Double n, short w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public short shortValue() {
+            return w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrIntegerOrFloat extends DoubleOrWholeNumber {
+
+        private final int w;
+
+        DoubleOrIntegerOrFloat(Double n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrInteger extends DoubleOrWholeNumber {
+
+        private final int w;
+
+        DoubleOrInteger(Double n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrLong extends DoubleOrWholeNumber {
+
+        private final long w;
+
+        DoubleOrLong(Double n, long w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class DoubleOrFloat extends NumberWithFallbackType {
+        
+        private final Double n;
+
+        DoubleOrFloat(Double n) {
+            this.n = n;
+        }
+        
+        public float floatValue() {
+            return n.floatValue();
+        }
+        
+        public double doubleValue() {
+            return n.doubleValue();
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+    }
+
+    static abstract class FloatOrWholeNumber extends NumberWithFallbackType {
+        
+        private final Float n; 
+
+        FloatOrWholeNumber(Float n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+        public float floatValue() {
+            return n.floatValue();
+        }
+        
+    }
+    
+    static final class FloatOrByte extends FloatOrWholeNumber {
+        
+        private final byte w;
+
+        FloatOrByte(Float n, byte w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public byte byteValue() {
+            return w;
+        }
+        
+        public short shortValue() {
+            return w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+    
+    static final class FloatOrShort extends FloatOrWholeNumber {
+        
+        private final short w;
+
+        FloatOrShort(Float n, short w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public short shortValue() {
+            return w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+
+    static final class FloatOrInteger extends FloatOrWholeNumber {
+        
+        private final int w;
+
+        FloatOrInteger(Float n, int w) {
+            super(n);
+            this.w = w;
+        }
+        
+        public int intValue() {
+            return w;
+        }
+        
+        public long longValue() {
+            return w;
+        }
+        
+    }
+
+    abstract static class BigIntegerOrPrimitive extends NumberWithFallbackType {
+
+        protected final BigInteger n;
+        
+        BigIntegerOrPrimitive(BigInteger n) {
+            this.n = n;
+        }
+
+        protected Number getSourceNumber() {
+            return n;
+        }
+        
+    }
+    
+    final static class BigIntegerOrByte extends BigIntegerOrPrimitive {
+
+        BigIntegerOrByte(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrShort extends BigIntegerOrPrimitive {
+
+        BigIntegerOrShort(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrInteger extends BigIntegerOrPrimitive {
+
+        BigIntegerOrInteger(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrLong extends BigIntegerOrPrimitive {
+
+        BigIntegerOrLong(BigInteger n) {
+            super(n);
+        }
+
+    }
+
+    abstract static class BigIntegerOrFPPrimitive extends BigIntegerOrPrimitive {
+
+        BigIntegerOrFPPrimitive(BigInteger n) {
+            super(n);
+        }
+
+        /** Faster version of {@link BigDecimal#floatValue()}, utilizes that the number known to fit into a long. */
+        public float floatValue() {
+            return n.longValue(); 
+        }
+        
+        /** Faster version of {@link BigDecimal#doubleValue()}, utilizes that the number known to fit into a long. */
+        public double doubleValue() {
+            return n.longValue(); 
+        }
+
+    }
+    
+    final static class BigIntegerOrFloat extends BigIntegerOrFPPrimitive {
+
+        BigIntegerOrFloat(BigInteger n) {
+            super(n);
+        }
+
+    }
+    
+    final static class BigIntegerOrDouble extends BigIntegerOrFPPrimitive {
+
+        BigIntegerOrDouble(BigInteger n) {
+            super(n);
+        }
+        
+    }
+    
+    /**
+     * Returns a non-negative number that indicates how much we want to avoid a given numerical type conversion. Since
+     * we only consider the types here, not the actual value, we always consider the worst case scenario. Like it will
+     * say that converting int to short is not allowed, although int 1 can be converted to byte without loss. To account
+     * for such situations, "Or"-ed types, like {@link IntegerOrByte} has to be used. 
+     * 
+     * @param fromC the non-primitive type of the argument (with other words, the actual type).
+     *        Must be {@link Number} or its subclass. This is possibly an {@link NumberWithFallbackType} subclass.
+     * @param toC the <em>non-primitive</em> type of the target parameter (with other words, the format type).
+     *        Must be a {@link Number} subclass, not {@link Number} itself.
+     *        Must <em>not</em> be {@link NumberWithFallbackType} or its subclass.
+     * 
+     * @return
+     *     <p>The possible values are:
+     *     <ul>
+     *       <li>0: No conversion is needed
+     *       <li>[0, 30000): Lossless conversion
+     *       <li>[30000, 40000): Smaller precision loss in mantissa is possible.
+     *       <li>[40000, 50000): Bigger precision loss in mantissa is possible.
+     *       <li>{@link Integer#MAX_VALUE}: Conversion not allowed due to the possibility of magnitude loss or overflow</li>
+     *     </ul>
+     * 
+     *     <p>At some places, we only care if the conversion is possible, i.e., whether the return value is
+     *     {@link Integer#MAX_VALUE} or not. But when multiple overloaded methods have an argument type to which we
+     *     could convert to, this number will influence which of those will be chosen.
+     */
+    static int getArgumentConversionPrice(Class fromC, Class toC) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        if (toC == fromC) {
+            return 0;
+        } else if (toC == Integer.class) {
+            if (fromC == IntegerBigDecimal.class) return 31003;
+            else if (fromC == BigDecimal.class) return 41003;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10003;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 21003;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 22003;
+            else if (fromC == DoubleOrInteger.class) return 22003;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 0;
+            else if (fromC == DoubleOrByte.class) return 22003;
+            else if (fromC == LongOrByte.class) return 21003;
+            else if (fromC == Short.class) return 10003;
+            else if (fromC == LongOrShort.class) return 21003;
+            else if (fromC == ShortOrByte.class) return 10003;
+            else if (fromC == FloatOrInteger.class) return 21003;
+            else if (fromC == FloatOrByte.class) return 21003;
+            else if (fromC == FloatOrShort.class) return 21003;
+            else if (fromC == BigIntegerOrInteger.class) return 16003;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 16003;
+            else if (fromC == IntegerOrShort.class) return 0;
+            else if (fromC == DoubleOrShort.class) return 22003;
+            else if (fromC == BigIntegerOrShort.class) return 16003;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Long.class) {
+            if (fromC == Integer.class) return 10004;
+            else if (fromC == IntegerBigDecimal.class) return 31004;
+            else if (fromC == BigDecimal.class) return 41004;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10004;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 0;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 21004;
+            else if (fromC == DoubleOrInteger.class) return 21004;
+            else if (fromC == DoubleOrLong.class) return 21004;
+            else if (fromC == IntegerOrByte.class) return 10004;
+            else if (fromC == DoubleOrByte.class) return 21004;
+            else if (fromC == LongOrByte.class) return 0;
+            else if (fromC == Short.class) return 10004;
+            else if (fromC == LongOrShort.class) return 0;
+            else if (fromC == ShortOrByte.class) return 10004;
+            else if (fromC == FloatOrInteger.class) return 21004;
+            else if (fromC == FloatOrByte.class) return 21004;
+            else if (fromC == FloatOrShort.class) return 21004;
+            else if (fromC == BigIntegerOrInteger.class) return 15004;
+            else if (fromC == BigIntegerOrLong.class) return 15004;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 15004;
+            else if (fromC == IntegerOrShort.class) return 10004;
+            else if (fromC == DoubleOrShort.class) return 21004;
+            else if (fromC == BigIntegerOrShort.class) return 15004;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Double.class) {
+            if (fromC == Integer.class) return 20007;
+            else if (fromC == IntegerBigDecimal.class) return 32007;
+            else if (fromC == BigDecimal.class) return 32007;
+            else if (fromC == Long.class) return 30007;
+            else if (fromC == Float.class) return 10007;
+            else if (fromC == Byte.class) return 20007;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 21007;
+            else if (fromC == DoubleOrFloat.class) return 0;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 0;
+            else if (fromC == DoubleOrInteger.class) return 0;
+            else if (fromC == DoubleOrLong.class) return 0;
+            else if (fromC == IntegerOrByte.class) return 20007;
+            else if (fromC == DoubleOrByte.class) return 0;
+            else if (fromC == LongOrByte.class) return 21007;
+            else if (fromC == Short.class) return 20007;
+            else if (fromC == LongOrShort.class) return 21007;
+            else if (fromC == ShortOrByte.class) return 20007;
+            else if (fromC == FloatOrInteger.class) return 10007;
+            else if (fromC == FloatOrByte.class) return 10007;
+            else if (fromC == FloatOrShort.class) return 10007;
+            else if (fromC == BigIntegerOrInteger.class) return 20007;
+            else if (fromC == BigIntegerOrLong.class) return 30007;
+            else if (fromC == BigIntegerOrDouble.class) return 20007;
+            else if (fromC == BigIntegerOrFloat.class) return 20007;
+            else if (fromC == BigIntegerOrByte.class) return 20007;
+            else if (fromC == IntegerOrShort.class) return 20007;
+            else if (fromC == DoubleOrShort.class) return 0;
+            else if (fromC == BigIntegerOrShort.class) return 20007;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Float.class) {
+            if (fromC == Integer.class) return 30006;
+            else if (fromC == IntegerBigDecimal.class) return 33006;
+            else if (fromC == BigDecimal.class) return 33006;
+            else if (fromC == Long.class) return 40006;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 20006;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return 30006;
+            else if (fromC == DoubleOrFloat.class) return 30006;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 23006;
+            else if (fromC == DoubleOrInteger.class) return 30006;
+            else if (fromC == DoubleOrLong.class) return 40006;
+            else if (fromC == IntegerOrByte.class) return 24006;
+            else if (fromC == DoubleOrByte.class) return 23006;
+            else if (fromC == LongOrByte.class) return 24006;
+            else if (fromC == Short.class) return 20006;
+            else if (fromC == LongOrShort.class) return 24006;
+            else if (fromC == ShortOrByte.class) return 20006;
+            else if (fromC == FloatOrInteger.class) return 0;
+            else if (fromC == FloatOrByte.class) return 0;
+            else if (fromC == FloatOrShort.class) return 0;
+            else if (fromC == BigIntegerOrInteger.class) return 30006;
+            else if (fromC == BigIntegerOrLong.class) return 40006;
+            else if (fromC == BigIntegerOrDouble.class) return 40006;
+            else if (fromC == BigIntegerOrFloat.class) return 24006;
+            else if (fromC == BigIntegerOrByte.class) return 24006;
+            else if (fromC == IntegerOrShort.class) return 24006;
+            else if (fromC == DoubleOrShort.class) return 23006;
+            else if (fromC == BigIntegerOrShort.class) return 24006;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Byte.class) {
+            if (fromC == Integer.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerBigDecimal.class) return 35001;
+            else if (fromC == BigDecimal.class) return 45001;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 22001;
+            else if (fromC == DoubleOrByte.class) return 25001;
+            else if (fromC == LongOrByte.class) return 23001;
+            else if (fromC == Short.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == ShortOrByte.class) return 21001;
+            else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == FloatOrByte.class) return 23001;
+            else if (fromC == FloatOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 18001;
+            else if (fromC == IntegerOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrShort.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrShort.class) return Integer.MAX_VALUE;
+            else return Integer.MAX_VALUE;
+        } else if (toC == Short.class) {
+            if (fromC == Integer.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerBigDecimal.class) return 34002;
+            else if (fromC == BigDecimal.class) return 44002;
+            else if (fromC == Long.class) return Integer.MAX_VALUE;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10002;
+            else if (fromC == BigInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == LongOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == IntegerOrByte.class) return 21002;
+            else if (fromC == DoubleOrByte.class) return 24002;
+            else if (fromC == LongOrByte.class) return 22002;
+            else if (fromC == LongOrShort.class) return 22002;
+            else if (fromC == ShortOrByte.class) return 0;
+            else if (fromC == FloatOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == FloatOrByte.class) return 22002;
+            else if (fromC == FloatOrShort.class) return 22002;
+            else if (fromC == BigIntegerOrInteger.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrLong.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrDouble.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == BigIntegerOrByte.class) return 17002;
+            else if (fromC == IntegerOrShort.class) return 21002;
+            else if (fromC == DoubleOrShort.class) return 24002;
+            else if (fromC == BigIntegerOrShort.class) return 17002;
+            else return Integer.MAX_VALUE;
+        } else if (toC == BigDecimal.class) {
+            if (fromC == Integer.class) return 20008;
+            else if (fromC == IntegerBigDecimal.class) return 0;
+            else if (fromC == Long.class) return 20008;
+            else if (fromC == Double.class) return 20008;
+            else if (fromC == Float.class) return 20008;
+            else if (fromC == Byte.class) return 20008;
+            else if (fromC == BigInteger.class) return 10008;
+            else if (fromC == LongOrInteger.class) return 20008;
+            else if (fromC == DoubleOrFloat.class) return 20008;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 20008;
+            else if (fromC == DoubleOrInteger.class) return 20008;
+            else if (fromC == DoubleOrLong.class) return 20008;
+            else if (fromC == IntegerOrByte.class) return 20008;
+            else if (fromC == DoubleOrByte.class) return 20008;
+            else if (fromC == LongOrByte.class) return 20008;
+            else if (fromC == Short.class) return 20008;
+            else if (fromC == LongOrShort.class) return 20008;
+            else if (fromC == ShortOrByte.class) return 20008;
+            else if (fromC == FloatOrInteger.class) return 20008;
+            else if (fromC == FloatOrByte.class) return 20008;
+            else if (fromC == FloatOrShort.class) return 20008;
+            else if (fromC == BigIntegerOrInteger.class) return 10008;
+            else if (fromC == BigIntegerOrLong.class) return 10008;
+            else if (fromC == BigIntegerOrDouble.class) return 10008;
+            else if (fromC == BigIntegerOrFloat.class) return 10008;
+            else if (fromC == BigIntegerOrByte.class) return 10008;
+            else if (fromC == IntegerOrShort.class) return 20008;
+            else if (fromC == DoubleOrShort.class) return 20008;
+            else if (fromC == BigIntegerOrShort.class) return 10008;
+            else return Integer.MAX_VALUE;
+        } else if (toC == BigInteger.class) {
+            if (fromC == Integer.class) return 10005;
+            else if (fromC == IntegerBigDecimal.class) return 10005;
+            else if (fromC == BigDecimal.class) return 40005;
+            else if (fromC == Long.class) return 10005;
+            else if (fromC == Double.class) return Integer.MAX_VALUE;
+            else if (fromC == Float.class) return Integer.MAX_VALUE;
+            else if (fromC == Byte.class) return 10005;
+            else if (fromC == LongOrInteger.class) return 10005;
+            else if (fromC == DoubleOrFloat.class) return Integer.MAX_VALUE;
+            else if (fromC == DoubleOrIntegerOrFloat.class) return 21005;
+            else if (fromC == DoubleOrInteger.class) return 21005;
+            else if (fromC == DoubleOrLong.class) return 21005;
+            else if (fromC == IntegerOrByte.class) return 10005;
+            else if (fromC == DoubleOrByte.class) return 21005;
+            else if (fromC == LongOrByte.class) return 10005;
+            else if (fromC == Short.class) return 10005;
+            else if (fromC == LongOrShort.class) return 10005;
+            else if (fromC == ShortOrByte.class) return 10005;
+            else if (fromC == FloatOrInteger.class) return 25005;
+            else if (fromC == FloatOrByte.class) return 25005;
+            else if (fromC == FloatOrShort.class) return 25005;
+            else if (fromC == BigIntegerOrInteger.class) return 0;
+            else if (fromC == BigIntegerOrLong.class) return 0;
+            else if (fromC == BigIntegerOrDouble.class) return 0;
+            else if (fromC == BigIntegerOrFloat.class) return 0;
+            else if (fromC == BigIntegerOrByte.class) return 0;
+            else if (fromC == IntegerOrShort.class) return 10005;
+            else if (fromC == DoubleOrShort.class) return 21005;
+            else if (fromC == BigIntegerOrShort.class) return 0;
+            else return Integer.MAX_VALUE;
+        } else {
+            // Unknown toC; we don't know how to convert to it:
+            return Integer.MAX_VALUE;
+        }        
+    }
+
+    static int compareNumberTypeSpecificity(Class c1, Class c2) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        c1 = ClassUtil.primitiveClassToBoxingClass(c1);
+        c2 = ClassUtil.primitiveClassToBoxingClass(c2);
+        
+        if (c1 == c2) return 0;
+        
+        if (c1 == Integer.class) {
+            if (c2 == Long.class) return 4 - 3;
+            if (c2 == Double.class) return 7 - 3;
+            if (c2 == Float.class) return 6 - 3;
+            if (c2 == Byte.class) return 1 - 3;
+            if (c2 == Short.class) return 2 - 3;
+            if (c2 == BigDecimal.class) return 8 - 3;
+            if (c2 == BigInteger.class) return 5 - 3;
+            return 0;
+        }
+        if (c1 == Long.class) {
+            if (c2 == Integer.class) return 3 - 4;
+            if (c2 == Double.class) return 7 - 4;
+            if (c2 == Float.class) return 6 - 4;
+            if (c2 == Byte.class) return 1 - 4;
+            if (c2 == Short.class) return 2 - 4;
+            if (c2 == BigDecimal.class) return 8 - 4;
+            if (c2 == BigInteger.class) return 5 - 4;
+            return 0;
+        }
+        if (c1 == Double.class) {
+            if (c2 == Integer.class) return 3 - 7;
+            if (c2 == Long.class) return 4 - 7;
+            if (c2 == Float.class) return 6 - 7;
+            if (c2 == Byte.class) return 1 - 7;
+            if (c2 == Short.class) return 2 - 7;
+            if (c2 == BigDecimal.class) return 8 - 7;
+            if (c2 == BigInteger.class) return 5 - 7;
+            return 0;
+        }
+        if (c1 == Float.class) {
+            if (c2 == Integer.class) return 3 - 6;
+            if (c2 == Long.class) return 4 - 6;
+            if (c2 == Double.class) return 7 - 6;
+            if (c2 == Byte.class) return 1 - 6;
+            if (c2 == Short.class) return 2 - 6;
+            if (c2 == BigDecimal.class) return 8 - 6;
+            if (c2 == BigInteger.class) return 5 - 6;
+            return 0;
+        }
+        if (c1 == Byte.class) {
+            if (c2 == Integer.class) return 3 - 1;
+            if (c2 == Long.class) return 4 - 1;
+            if (c2 == Double.class) return 7 - 1;
+            if (c2 == Float.class) return 6 - 1;
+            if (c2 == Short.class) return 2 - 1;
+            if (c2 == BigDecimal.class) return 8 - 1;
+            if (c2 == BigInteger.class) return 5 - 1;
+            return 0;
+        }
+        if (c1 == Short.class) {
+            if (c2 == Integer.class) return 3 - 2;
+            if (c2 == Long.class) return 4 - 2;
+            if (c2 == Double.class) return 7 - 2;
+            if (c2 == Float.class) return 6 - 2;
+            if (c2 == Byte.class) return 1 - 2;
+            if (c2 == BigDecimal.class) return 8 - 2;
+            if (c2 == BigInteger.class) return 5 - 2;
+            return 0;
+        }
+        if (c1 == BigDecimal.class) {
+            if (c2 == Integer.class) return 3 - 8;
+            if (c2 == Long.class) return 4 - 8;
+            if (c2 == Double.class) return 7 - 8;
+            if (c2 == Float.class) return 6 - 8;
+            if (c2 == Byte.class) return 1 - 8;
+            if (c2 == Short.class) return 2 - 8;
+            if (c2 == BigInteger.class) return 5 - 8;
+            return 0;
+        }
+        if (c1 == BigInteger.class) {
+            if (c2 == Integer.class) return 3 - 5;
+            if (c2 == Long.class) return 4 - 5;
+            if (c2 == Double.class) return 7 - 5;
+            if (c2 == Float.class) return 6 - 5;
+            if (c2 == Byte.class) return 1 - 5;
+            if (c2 == Short.class) return 2 - 5;
+            if (c2 == BigDecimal.class) return 8 - 5;
+            return 0;
+        }
+        return 0;
+    }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java b/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
index 3b49a5d..ce566bb 100644
--- a/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
+++ b/src/main/java/freemarker/ext/beans/OverloadedVarArgsMethods.java
@@ -93,7 +93,7 @@
                 System.arraycopy(args, 0, packedArgs, 0, fixArgCount);
                 Object varargs = Array.newInstance(varArgsCompType, totalArgCount - fixArgCount);
                 for(int i = fixArgCount; i < totalArgCount; ++i) {
-                    Object val = w.unwrapInternal((TemplateModel)modelArgs.get(i), varArgsCompType);
+                    Object val = w.tryUnwrap((TemplateModel)modelArgs.get(i), varArgsCompType);
                     if(val == BeansWrapper.CAN_NOT_UNWRAP) {
                         return null;
                     }
@@ -103,7 +103,7 @@
                 return packedArgs;
             }
             else {
-                Object val = w.unwrapInternal((TemplateModel)modelArgs.get(fixArgCount), varArgsCompType);
+                Object val = w.tryUnwrap((TemplateModel)modelArgs.get(fixArgCount), varArgsCompType);
                 if(val == BeansWrapper.CAN_NOT_UNWRAP) {
                     return null;
                 }
@@ -142,73 +142,79 @@
             else {
                 argPacker = canonical;
             }
-        }
         argPackers.put(memberDesc.member, argPacker);
+        }
         
         return preprocessedParamTypes;
     }
 
-    void afterWideningUnwrappingHints(Class[] paramTypes) {
-        final int paramCount = paramTypes.length;
-        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
-        final Class[] hints = unwrappingHintsByParamCount[paramCount];
-
+    void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
         // Overview
         // --------
         //
         // So far, m(t1, t2...) was treated by the hint widening like m(t1, t2). So now we have to continue hint
         // widening like if we had further methods:
-        // - m(t1), because a vararg array can be 0 long
-        // - m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ...
+        // - m(t1, t2, t2), m(t1, t2, t2, t2), ...
+        // - m(t1), because a varargs array can be 0 long
         //
         // But we can't do that for real, because we had to add infinite number of methods. Also, for efficiency we
         // don't want to create unwrappingHintsByParamCount entries at the indices which are still unused.
         // So we only update the already existing hints. Remember that we already have m(t1, t2) there.
         
-        // The case of m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ..., where m is an *earlier* added method.
+        final int paramCount = paramTypes.length;
+        final Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
+        
+        // The case of e(t1, t2), e(t1, t2, t2), e(t1, t2, t2, t2), ..., where e is an *earlier* added method.
         // When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
         // so we do that now:
         // FIXME: Only needed if m(t1, t2) was filled an empty slot, otherwise whatever was there was already
         // widened by the preceding hints, so this will be a no-op.
         for(int i = paramCount - 1; i >= 0; i--) {
-            Class[] previousHints = unwrappingHintsByParamCount[i];
+            final Class[] previousHints = unwrappingHintsByParamCount[i];
             if(previousHints != null) {
-                widenToCommonSupertypesForUnwrappingHint(hints, previousHints);
-                break;
+                widenHintsToCommonSupertypes(
+                        paramCount,
+                        previousHints, getPossibleNumericalTypes(i));
+                break;  // we only do this for the first hit, as the methods before that has already widened it.
             }
         }
-        // The case of m(t1), where m is an *earlier* added method.
+        // The case of e(t1), where e is an *earlier* added method.
         // When that was added, this method wasn't added yet, so it had no chance updating the hints of this method,
         // so we do that now:
         // FIXME: Same as above; it's often unnecessary.
         if(paramCount + 1 < unwrappingHintsByParamCount.length) {
             Class[] oneLongerHints = unwrappingHintsByParamCount[paramCount + 1];
             if(oneLongerHints != null) {
-                widenToCommonSupertypesForUnwrappingHint(hints, oneLongerHints);
+                widenHintsToCommonSupertypes(
+                        paramCount,
+                        oneLongerHints, getPossibleNumericalTypes(paramCount + 1));
             }
         }
         
-        // The case of m(t1, t2), m(t1, t2, t2), , m(t1, t2, t2, t2), ..., where m is the currently added method.
+        // The case of m(t1, t2, t2), m(t1, t2, t2, t2), ..., where m is the currently added method.
         // Update the longer hints-arrays:  
-        // FIXME Shouldn't these use the original hint array instead of the widened one?
-        for(int i = paramCount + 1; i < unwrappingHintsByParamCount.length; ++i) {
-            Class[] longerHints = unwrappingHintsByParamCount[i];
-            if(longerHints != null) {
-                widenToCommonSupertypesForUnwrappingHint(longerHints, paramTypes);
-            }
+        for(int i = paramCount + 1; i < unwrappingHintsByParamCount.length; i++) {
+            widenHintsToCommonSupertypes(
+                    i,
+                    paramTypes, paramNumericalTypes);
         }
         // The case of m(t1) where m is the currently added method.
         // update the one-shorter hints-array:  
         if(paramCount > 0) {  // (should be always true, or else it wasn't a varags method)
-            Class[] oneShorterUnwrappingHints = unwrappingHintsByParamCount[paramCount - 1];
-            if(oneShorterUnwrappingHints != null) {
-                widenToCommonSupertypesForUnwrappingHint(oneShorterUnwrappingHints, paramTypes);
-            }
+            widenHintsToCommonSupertypes(
+                    paramCount - 1,
+                    paramTypes, paramNumericalTypes);
         }
         
     }
     
-    private void widenToCommonSupertypesForUnwrappingHint(Class[] typesToWiden, Class[] wideningTypes) {
+    private void widenHintsToCommonSupertypes(
+            int paramCountOfWidened, Class[] wideningTypes, int[] wideningNumTypes) {
+        final Class[] typesToWiden = getUnwrappingHintsByParamCount()[paramCountOfWidened];
+        if (typesToWiden == null) { 
+            return;  // no such overload exists; nothing to widen
+        }
+        
         final int typesToWidenLen = typesToWiden.length;
         final int wideningTypesLen = wideningTypes.length;
         int min = Math.min(wideningTypesLen, typesToWidenLen);
@@ -221,6 +227,10 @@
                 typesToWiden[i] = getCommonSupertypeForUnwrappingHint(typesToWiden[i], varargsComponentType);
             }
         }
+        
+        if (bugfixed) {
+            mergeInNumericalTypes(paramCountOfWidened, wideningNumTypes);
+        }
     }
     
     MaybeEmptyMemberAndArguments getMemberAndArguments(List tmArgs, BeansWrapper w) 
@@ -229,30 +239,39 @@
             // null is treated as empty args
             tmArgs = Collections.EMPTY_LIST;
         }
-        int l = tmArgs.size();
+        int argsLen = tmArgs.size();
         Class[][] unwrappingHintsByParamCount = getUnwrappingHintsByParamCount();
-        Object[] pojoArgs = new Object[l];
+        Object[] pojoArgs = new Object[argsLen];
+        int[] possibleNumericalTypes = null;
         // Starting from args.length + 1 as we must try to match against a case
         // where all specified args are fixargs, and the vararg portion 
         // contains zero args
-        outer:  for(int j = Math.min(l + 1, unwrappingHintsByParamCount.length - 1); j >= 0; --j) {
-            Class[] unwarappingArgTypes = unwrappingHintsByParamCount[j];
-            if(unwarappingArgTypes == null) {
-                if(j == 0) {
+        outer: for(int paramCount = Math.min(argsLen + 1, unwrappingHintsByParamCount.length - 1); paramCount >= 0; --paramCount) {
+            Class[] unwarappingHints = unwrappingHintsByParamCount[paramCount];
+            if(unwarappingHints == null) {
+                if (paramCount == 0) {
                     return EmptyMemberAndArguments.NO_SUCH_METHOD;
                 }
                 continue;
             }
-            // Try to marshal the arguments
+            
+            possibleNumericalTypes = getPossibleNumericalTypes(paramCount);
+            if (possibleNumericalTypes == ALL_ZEROS_ARRAY) {
+                possibleNumericalTypes = null;
+            }
+            
+            // Try to unwrap the arguments
             Iterator it = tmArgs.iterator();
-            for(int i = 0; i < l; ++i) {
-                Object pojo = w.unwrapInternal((TemplateModel)it.next(), i < j ? unwarappingArgTypes[i] : unwarappingArgTypes[j - 1]);
+            for(int i = 0; i < argsLen; ++i) {
+                int paramIdx = i < paramCount ? i : paramCount - 1;
+                Object pojo = w.tryUnwrap(
+                        (TemplateModel)it.next(),
+                        unwarappingHints[paramIdx],
+                        possibleNumericalTypes != null ? possibleNumericalTypes[paramIdx] : 0);
                 if(pojo == BeansWrapper.CAN_NOT_UNWRAP) {
                     continue outer;
                 }
-                if(pojo != pojoArgs[i]) {
-                    pojoArgs[i] = pojo;
-                }
+                pojoArgs[i] = pojo;
             }
             break;
         }
@@ -260,11 +279,20 @@
         MaybeEmptyCallableMemberDescriptor maybeEmtpyMemberDesc = getMemberDescriptorForArgs(pojoArgs, true);
         if(maybeEmtpyMemberDesc instanceof CallableMemberDescriptor) {
             CallableMemberDescriptor memberDesc = (CallableMemberDescriptor) maybeEmtpyMemberDesc;
-            pojoArgs = ((ArgumentPacker)argPackers.get(memberDesc.member)).packArgs(pojoArgs, tmArgs, w);
+            pojoArgs = ((ArgumentPacker) argPackers.get(memberDesc.member)).packArgs(pojoArgs, tmArgs, w);
             if(pojoArgs == null) {
                 return EmptyMemberAndArguments.NO_SUCH_METHOD;
             }
-            BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+            if (bugfixed) {
+                if (possibleNumericalTypes != null) {
+                    // Note that overloaded method selection has already accounted for overflow errors when the method
+                    // was selected. So this forced conversion shouldn't cause such corruption. Except, conversion from
+                    // BigDecimal is allowed to overflow for backward-compatibility.
+                    forceNumberArgumentsToParameterTypes(pojoArgs, memberDesc.paramTypes, possibleNumericalTypes);
+                }
+            } else {
+                BeansWrapper.coerceBigDecimals(memberDesc.paramTypes, pojoArgs);
+            }
             return new MemberAndArguments(memberDesc.member, pojoArgs);
         } else {
             return EmptyMemberAndArguments.from((EmptyCallableMemberDescriptor) maybeEmtpyMemberDesc); // either NOT_FOUND or AMBIGUOUS
diff --git a/src/main/java/freemarker/ext/beans/SimpleMemberModel.java b/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
index b5696ee..5903847 100644
--- a/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
+++ b/src/main/java/freemarker/ext/beans/SimpleMemberModel.java
@@ -119,7 +119,7 @@
         while (argIdx < normalArgCnt) {
             Class argType = argTypes[argIdx];
             TemplateModel argVal = (TemplateModel) it.next();
-            Object unwrappedArgVal = w.unwrapInternal(argVal, argType);
+            Object unwrappedArgVal = w.tryUnwrap(argVal, argType);
             if(unwrappedArgVal == BeansWrapper.CAN_NOT_UNWRAP) {
                 throw createArgumentTypeMismarchException(argIdx, argVal, argType);
             }
@@ -143,7 +143,7 @@
                 // We first try to treat the last argument as a vararg *array*.
                 // This is consistent to what OverloadedVarArgMethod does.
                 if (argsLen - argIdx == 1
-                        && (unwrappedArgVal = w.unwrapInternal(argVal, varargType))
+                        && (unwrappedArgVal = w.tryUnwrap(argVal, varargType))
                             != BeansWrapper.CAN_NOT_UNWRAP) {
                     // It was a vararg array.
                     unwrappedArgs[argIdx++] = unwrappedArgVal;
@@ -154,7 +154,7 @@
                     Object varargArray = Array.newInstance(varargItemType, varargArrayLen);
                     for (int varargIdx = 0; varargIdx < varargArrayLen; varargIdx++) {
                         TemplateModel varargVal = (TemplateModel) (varargIdx == 0 ? argVal : it.next());
-                        Object unwrappedVarargVal = w.unwrapInternal(varargVal, varargItemType);
+                        Object unwrappedVarargVal = w.tryUnwrap(varargVal, varargItemType);
                         if(unwrappedVarargVal == BeansWrapper.CAN_NOT_UNWRAP) {
                             throw createArgumentTypeMismarchException(
                                     argIdx + varargIdx, varargVal, varargItemType);
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index b7d9cb6..cd5deee 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -391,5 +391,17 @@
         if (boxingClass == Void.class) return void.class;  // not really a primitive, but we normalize to it
         return boxingClass;
     }
+
+    /**
+     * Tells if a type is numerical; works both for primitive types and classes.
+     * 
+     * @param type can't be {@code null}
+     * 
+     * @since. 2.3.21
+     */
+    public static boolean isNumerical(Class type) {
+        return Number.class.isAssignableFrom(type)
+                || type.isPrimitive() && type != Boolean.TYPE && type != Character.TYPE && type != Void.TYPE;
+    }
     
 }
diff --git a/src/main/java/freemarker/template/utility/NumberUtil.java b/src/main/java/freemarker/template/utility/NumberUtil.java
index 0d80cc0..0f9bd64 100644
--- a/src/main/java/freemarker/template/utility/NumberUtil.java
+++ b/src/main/java/freemarker/template/utility/NumberUtil.java
@@ -76,6 +76,18 @@
         }
     }
     
+    /**
+     * Tells if a {@link BigDecimal} stores a whole number. For example, it returns {@code true} for {@code 1.0000},
+     * but {@code false} for {@code 1.0001}.
+     * @since 2.3.21
+     */
+    static public boolean isBigDecimalInteger(BigDecimal bd) {
+            return bd.scale() <= 0  // A fast check that whole numbers usually (not always) match
+                   || bd.setScale(0, BigDecimal.ROUND_DOWN).compareTo(bd) == 0;  // This is rather slow
+            // Note that `bd.signum() == 0 || bd.stripTrailingZeros().scale() <= 0` was also tried for the last
+            // condition, but stripTrailingZeros was slower than setScale + compareTo.
+    }
+    
     private static boolean isNonFPNumberOfSupportedClass(Number num) {
         return num instanceof Integer || num instanceof BigDecimal || num instanceof Long
                 || num instanceof Short || num instanceof Byte || num instanceof BigInteger;
diff --git a/src/main/misc/overloadedNumberRules/README.txt b/src/main/misc/overloadedNumberRules/README.txt
new file mode 100644
index 0000000..20a40e7
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/README.txt
@@ -0,0 +1,12 @@
+This FMPP project is used for generating the source code of some
+`freemarker.ext.beans.OverloadedNumberUtil` methods based on the
+content of `prices.ods` (LibreOffice spreadsheet).
+
+Usage:
+1. Edit `prices.ods`
+3. If you have introduced new types in it, also update `toCsFreqSorted` and
+   `toCsCostBoosts` and `toCsContCosts` in `config.fmpp`.
+4. Save it into `prices.csv` (use comma as field separator)
+5. Run FMPP from this directory. It will generate
+   `<freemarkerProjectDir>/build/getArgumentConversionPrice.java`.
+6. Copy-pase its content into `OverloadedNumberUtil.java`.
\ No newline at end of file
diff --git a/src/main/misc/overloadedNumberRules/config.fmpp b/src/main/misc/overloadedNumberRules/config.fmpp
new file mode 100644
index 0000000..1017c30
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/config.fmpp
@@ -0,0 +1,56 @@
+sources: generator.ftl
+outputFile: ../../../../build/overloadedNumberRules.java
+data: {
+  t: csv(prices.csv, { separator: ',' })
+  
+  # Conversion target types sorted by decreasing probablity of occurence
+  toCsFreqSorted: [ Integer, Long, Double, Float, Byte, Short, BigDecimal, BigInteger ]
+  
+  # Conversion target types associated to conversion price boost. The prices from the spreadsheet
+  # will be multipied by 10000 and the boost will be added to it. Thus, if the price of two possible
+  # targets are the same according the spreadsheet (and only then!), the choice will depend on
+  # this boost.
+  # The more specific the (the smaller) type is, the lower the boost should be. This is improtant,
+  # because this number is also used for comparing the specificity of numerical types where
+  # there's no argument type available.
+  # Note where the price from the spreadsheet is 0 or "-" or "N/A", the boost is not used.
+  toCsCostBoosts: {
+    'Byte': 1, 'Short': 2, 'Integer': 3, 'Long': 4, 'BigInteger': 5,
+    'Float': 6, 'Double': 7,
+    'BigDecimal': 8
+  }
+  
+  # Conversion source types sorted by decreasing probablity of occurence
+  fromCsFreqSorted: [
+    Integer, 
+    IntegerBigDecimal, 
+    BigDecimal, 
+    Long, 
+    Double, 
+    Float, 
+    Byte, 
+    BigInteger,
+    LongOrInteger
+    DoubleOrFloat, 
+    DoubleOrIntegerOrFloat, 
+    DoubleOrInteger, 
+    DoubleOrLong, 
+    IntegerOrByte,
+    DoubleOrByte, 
+    LongOrByte
+    Short, 
+    LongOrShort
+    ShortOrByte
+    FloatOrInteger, 
+    FloatOrByte, 
+    FloatOrShort, 
+    BigIntegerOrInteger, 
+    BigIntegerOrLong, 
+    BigIntegerOrDouble,   
+    BigIntegerOrFloat, 
+    BigIntegerOrByte, 
+    IntegerOrShort,
+    DoubleOrShort, 
+    BigIntegerOrShort, 
+  ]
+}
\ No newline at end of file
diff --git a/src/main/misc/overloadedNumberRules/generator.ftl b/src/main/misc/overloadedNumberRules/generator.ftl
new file mode 100644
index 0000000..bbd7463
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/generator.ftl
@@ -0,0 +1,62 @@
+    static int getArgumentConversionPrice(Class fromC, Class toC) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        if (toC == fromC) {
+            return 0;
+        <#list toCsFreqSorted as toC><#t>
+        } else if (toC == ${toC}.class) {
+                <#assign firstFromC = true>
+                <#list fromCsFreqSorted as fromC>
+                    <#if toC != fromC>
+            <#assign row = []>
+            <#list t as i>
+                <#if i[0] == fromC>
+                    <#assign row = i>
+                    <#break>
+                </#if>
+            </#list>
+            <#if !row?has_content><#stop "Not found: " + fromC></#if>
+            <#if !firstFromC>else </#if>if (fromC == ${fromC}.class) return ${toPrice(row[toC], toCsCostBoosts[toC])};
+            <#assign firstFromC = false>
+                    </#if>
+                </#list>
+            else return Integer.MAX_VALUE;
+        </#list>
+        } else {
+            // Unknown toC; we don't know how to convert to it:
+            return Integer.MAX_VALUE;
+        }        
+    }
+
+    static int compareNumberTypeSpecificity(Class c1, Class c2) {
+        // DO NOT EDIT, generated code!
+        // See: src\main\misc\overloadedNumberRules\README.txt
+        c1 = ClassUtil.primitiveClassToBoxingClass(c1);
+        c2 = ClassUtil.primitiveClassToBoxingClass(c2);
+        
+        if (c1 == c2) return 0;
+        
+        <#list toCsFreqSorted as c1><#t>
+        if (c1 == ${c1}.class) {
+          <#list toCsFreqSorted as c2><#if c1 != c2><#t>
+            if (c2 == ${c2}.class) return ${toCsCostBoosts[c2]} - ${toCsCostBoosts[c1]};
+          </#if></#list>
+            return 0;
+        }
+        </#list>
+        return 0;
+    }
+
+<#function toPrice cellValue, boost>
+    <#if cellValue?starts_with("BC ")>
+        <#local cellValue = cellValue[3..]>
+    <#elseif cellValue == '-' || cellValue == 'N/A'>
+        <#return 'Integer.MAX_VALUE'>
+    </#if>
+    <#local cellValue = cellValue?number>
+    <#if cellValue != 0>
+        <#return cellValue * 10000 + boost>
+    <#else>
+        <#return 0>
+    </#if>
+</#function>
diff --git a/src/main/misc/overloadedNumberRules/prices.csv b/src/main/misc/overloadedNumberRules/prices.csv
new file mode 100644
index 0000000..c12d7ba
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/prices.csv
@@ -0,0 +1,48 @@
+,Byte,Short,Integer,Long,BigInteger,Float,Double,BigDecimal,,Note
+Byte,0,1,1,1,1,2,2,2,,
+ShortOrByte,2.1,0,1,1,1,2,2,2,,
+Short,-,0,1,1,1,2,2,2,,
+IntegerOrByte,2.2,2.1,0,1,1,2.4,2,2,,
+IntegerOrShort,-,2.1,0,1,1,2.4,2,2,,
+Integer,-,-,0,1,1,3,2,2,,
+LongOrByte,2.3,2.2,2.1,0,1,2.4,2.1,2,,
+LongOrShort,-,2.2,2.1,0,1,2.4,2.1,2,,
+LongOrInteger,-,-,2.1,0,1,3,2.1,2,,
+Long,-,-,-,0,1,4,3,2,,
+BigIntegerOrByte,1.8,1.7,1.6,1.5,0,2.4,2,1,,
+BigIntegerOrShort,-,1.7,1.6,1.5,0,2.4,2,1,,
+BigIntegerOrInteger,-,-,1.6,1.5,0,3,2,1,,
+BigIntegerOrLong,-,-,-,1.5,0,4,3,1,,
+BigIntegerOrFloat,N/A,N/A,N/A,N/A,0,2.4,2,1,,"Condition: No whole-number target types exist, and abs(value) <= 2**24"
+BigIntegerOrDouble,N/A,N/A,N/A,N/A,0,4,2,1,,"Condition: No whole-number target types exist, and abs(value) <= 2**53"
+BigInteger,-,-,-,-,0,-,-,1,,
+FloatOrByte,2.3,2.2,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+FloatOrShort,-,2.2,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+FloatOrInteger,-,-,2.1,2.1,2.5,0,1,2,,"Condition: fraction == 0, in [-16777216 .. 16777216]"
+Float,-,-,-,-,-,0,1,2,,
+DoubleOrByte,2.5,2.4,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrShort,-,2.4,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrIntegerOrFloat,-,-,2.2,2.1,2.1,2.3,0,2,,"Condition: fraction == 0, in [-16777216 .. 16777216]"
+DoubleOrInteger,-,-,2.2,2.1,2.1,3,0,2,,"Condition: fraction == 0, in [T.MIN_VALUE .. T.MAX_VALUE]"
+DoubleOrLong,-,-,-,2.1,2.1,4,0,2,,"Condition: fraction == 0, in [-9007199254740992..9007199254740992]"
+DoubleOrFloat,-,-,-,-,-,3,0,2,,Condition: no magnitude loss
+Double,-,-,-,-,-,-,0,2,,
+IntegerBigDecimal,3.5,3.4,3.1,3.1,1,3.3,3.2,0,,Condition: fraction == 0; for BC we convert rather “bravely” here.
+BigDecimal,4.5,4.4,4.1,4.1,4,3.3,3.2,0,,Condition: fraction != 0; for BC we convert rather “bravely” here.
+,,,,,,,,,,
+,Legend:,,,,,,,,,
+,,"The numbers are the „price” of the conversion, and they tell how much we try to avoid it (when selecting among overloaded methods):",,,,,,,,
+,,,,,,,,,,
+,,0,No conversion,,,,,,,
+,,1,Lossless conversion,,,,,,,
+,,2,"Up to but excluding 3, lossless but certainly unintended conversion.",,,,,,,
+,,3,Smaller precision loss in mantissa.,,,,,,,
+,,4,Bigger precision loss in mantissa.,,,,,,,
+,,-,Magnitude loss or overflow => not allowed,,,,,,,
+,,N/A,Doesn't mater (Not Applicable),,,,,,,
+,,,,,,,,,,
+,Notes:,,,,,,,,,
+,-,"FooOrBar types mean that the argument's type was Foo, but the value also fits into the more specific Bar type.",,,,,,,,
+,,"This possibility should only be utilized as a last resort, to keep the method selection stable (always selects the same method) for Foo-s regardless of the actual value.",,,,,,,,
+,,"This is why the column of Bar-s tends to use higher costs than the less fitting columns that are, however, also selectable for higher values of type Foo.",,,,,,,,
+,-,"Between same-price conversions the one in the leftmost column (i.e., the smallest type) wins. This is realized in the Java code generator.",,,,,,,,
diff --git a/src/main/misc/overloadedNumberRules/prices.ods b/src/main/misc/overloadedNumberRules/prices.ods
new file mode 100644
index 0000000..4beabcf
--- /dev/null
+++ b/src/main/misc/overloadedNumberRules/prices.ods
Binary files differ
diff --git a/src/manual/book.xml b/src/manual/book.xml
index f79c552..a04c8b7 100644
--- a/src/manual/book.xml
+++ b/src/manual/book.xml
@@ -20372,89 +20372,163 @@
               <literal>ObjectWrapper</literal> instead. (As
               <literal>ObjectWrapper</literal> objects are often shared among
               multiple <literal>Configuration</literal>-s, they can't use that
-              setting of the <literal>Configuration</literal>.)</para>
+              setting of the <literal>Configuration</literal>.) In new or
+              actively developed projects it's recommended to use
+              <literal>Version(2, 3, 21)</literal> or higher.</para>
             </listitem>
 
             <listitem>
-              <para>Fixes in the overloaded method selection rules when
-              <literal>BeansWrapper</literal> or
+              <para>Fixes and reworkings in the overloaded method selection
+              rules when <literal>BeansWrapper</literal> or
               <literal>DefaultObjectWrapper</literal> is used (almost all
-              applications use these), but only if you create
+              applications use these), but only if you create the
               <literal>BeansWrapper</literal>/<literal>DefaultObjectWrapper</literal>
               with constructor parameter <literal>Version(2, 3, 21)</literal>
-              (or higher). There's a very little chance that because of these
-              fixes, a different overloaded method will chosen than before
-              (which in theory could change the behavior of the application),
-              or ambiguity error will be reported where earlier it has only
-              seen one choice, hence the fixes aren't automatically active (at
-              least not unit FreeMarker 2.4). But the fix mostly only effect
-              calls that were failing earlier, so it's recommended to active
-              it for project that are still actively developed. The fixes
-              are:</para>
+              or higher. There's a little chance that because of these
+              changes, a different overloaded method will chosen than before,
+              or even that ambiguity error will arise where earlier they
+              didn't (although the opposite is much more probable), hence the
+              fixes aren't automatically active, at least not unit FreeMarker
+              2.4. But the fix mostly only effect calls that were failing or
+              has chosen then wrong method, so it's recommended to activate it
+              for projects that are still actively developed. This fix
+              includes numerous changes:</para>
 
               <itemizedlist>
                 <listitem>
-                  <para><literal>null</literal> argument values has only
-                  matched overloaded methods where the corresponding parameter
-                  had <literal>Object</literal> type, not a subclass of it.
-                  This is the most risky fix regarding backward-compatibility,
-                  as when the argument is <literal>null</literal>, earlier it
-                  has chosen the overload that has <literal>Object</literal>
-                  at the given position, while after the fix it also considers
-                  the other overloads with a non-primitive type at the given
-                  position. In that case the overload with the
-                  <literal>Object</literal> parameter will fall out (so far so
-                  good), but if there are two other candidates, whose
-                  parameter types at that position aren't in
-                  <literal>instanceof</literal> relationship at some
-                  direction, their relative specificity is undecidable and so
-                  there will be ambiguity exception.</para>
+                  <para>Earlier, <literal>null</literal> argument values has
+                  only matched overloaded methods where the corresponding
+                  parameter had <literal>Object</literal> type, not a subclass
+                  of it. Now it considers all overloads where the parameter
+                  type is non-primitive, and just like the Java language, it
+                  choses the one with the most specific type among them. This
+                  is the most important fix, and also the most risky one
+                  regarding backward-compatibility. Like if you have
+                  <literal>m(Object o)</literal> and <literal>m(String
+                  s)</literal> in a Java class, earlier for a
+                  <literal>m(null)</literal> call in the template it has
+                  chosen <literal>m(Object o)</literal>, but now it will
+                  choose <literal>m(String s)</literal> instead (because
+                  <literal>String</literal> is also
+                  <literal>null</literal>-able and is more specific then
+                  <literal>Object</literal>). Furthermore, if you also had
+                  <literal>m(File f)</literal> in the same class, now it will
+                  cause an ambiguity exception, since the specificity of
+                  <literal>File</literal> and <literal>String</literal> can't
+                  be compared (same rule as under Java language), while
+                  earlier that wasn't a problem as only <literal>m(Object
+                  o)</literal> was seen as applicable.</para>
                 </listitem>
 
                 <listitem>
-                  <para>TODO: number conversions</para>
-                </listitem>
-
-                <listitem>
-                  <para>TODO: method order dependence</para>
-                </listitem>
-
-                <listitem>
-                  <para>Ambiguity is now resolved in some cases where earlier
-                  it was an error. Ambiguities was and are resolved by
-                  selecting the compatible methods then choosing the one with
-                  the most specific parameter types among them. How the
-                  specificity of the parameter types is calculated was
-                  extended:</para>
+                  <para>The behavior of numbers with overloaded method
+                  selection was heavily reworked:</para>
 
                   <itemizedlist>
                     <listitem>
-                      <para>When comparing the specificity of two parameters
-                      types at the same parameter position: The algorithm now
-                      considers a primitive type as more specific that its
-                      corresponding boxing class. Also, the now it considers
-                      widening conversion from primitive numerical types to
-                      numerical classes, and from numerical class types to
-                      other numerical class types, and if the target is wider
-                      than the source, then the source is considered to be
-                      more specific. (Earlier this has only worked among two
-                      primitive types). Furthermore, it considers any
-                      primitive numerical type as more specific than
-                      <literal>Number</literal>.</para>
+                      <para>If possible, it now always choses the overload
+                      where overflow and truncation to integer (like 1.5 to 1)
+                      is avoided. Among the methods where no such critical
+                      loss occurs, it choses the overload with the least risk
+                      of precision loss (unless other conditions with higher
+                      priority suggest otherwise). Earlier, the method
+                      selection was prone to do choices that led to overflow
+                      or precision loss, especially when the parameter was a
+                      literal with decimals.</para>
                     </listitem>
 
                     <listitem>
+                      <para>Overloaded method call can now convert to
+                      non-primitive numerical types, like a
+                      <literal>Byte</literal> or <literal>byte</literal> value
+                      is automatically converted to <literal>Integer</literal>
+                      if the parameter type is <literal>Integer</literal>.
+                      (This has always worked for non-overloaded methods.)
+                      Earlier where such conversion was needed, the method
+                      wasn't seen seen applicable.</para>
+                    </listitem>
+
+                    <listitem>
+                      <para>Method selection is now not only based on the type
+                      of the wrapped number, but also on its value. For
+                      example, a <literal>Long</literal> with value
+                      <literal>1</literal> is now seen to be compatible with a
+                      method with parameter type <literal>int</literal> or
+                      <literal>short</literal> or <literal>byte</literal>, as
+                      <literal>1</literal> can be stored in those without
+                      loss. This is important as unlike in Java language, in
+                      FTL you doesn't have strict control over the numerical
+                      types (the type of the wrapped number, actually), as FTL
+                      has no type declarations. (If multiple compatible
+                      methods are available, it will still try to chose the
+                      one with the same or bigger type first.)</para>
+                    </listitem>
+
+                    <listitem>
+                      <para>Conversion from/to <literal>BigInteger</literal>
+                      is now supported.</para>
+                    </listitem>
+                  </itemizedlist>
+                </listitem>
+
+                <listitem>
+                  <para>Method choice ambiguity errors now occur much less
+                  often. Ambiguities was and are resolved by selecting the
+                  compatible methods then choosing the one with the most
+                  specific parameter types among them. The changes are:</para>
+
+                  <itemizedlist>
+                    <listitem>
                       <para>When comparing the overall specificity of two
                       parameter lists: Earlier the more specific parameter
                       list was the one that had some parameters that won in
                       specificity, and if both had such parameters then it was
-                      an ambiguity. This rule changes in that now parameter
-                      specificity wins has a score, and the parameter list
-                      that contains the parameter with the highest score wins.
-                      The scores from lowest to highest: primitive wins over
-                      boxing type, narrower numerical type wins over wider
-                      numerical type, and subclass wins over
-                      super-class.</para>
+                      an ambiguity. Now it's enough if a method has more such
+                      parameters where it's a better match than the other has,
+                      or if the two methods are still equal, if it has the
+                      first better matching parameter. While this can lead to
+                      choices that seem arbitrary (but are still
+                      deterministic), as there is no automated way of
+                      discovering method selection ambiguities in templates
+                      (unlike in Java source code, where they will be detected
+                      during compilation), especially as overloaded selection
+                      has to rely on the <emphasis>runtime</emphasis> type of
+                      the values which makes proper testing hard, this was
+                      considered to be a better compromise than throwing an
+                      exception whenever the choice of the method is not
+                      obvious. Also note that in fact it's more complicated
+                      then counting the <quote>winner</quote> parameter
+                      positions for each methods, as certain kind of wins are
+                      stronger than any number of the others: wins there the
+                      other possibility is risking the loss of substantial
+                      mantissa precision are the strongest (like dropping
+                      decimals versus not to), wins where the primitive type
+                      wins over the boxed class is the weakest (like
+                      <literal>int</literal> versus
+                      <literal>Integer</literal>), subclassing wins (like
+                      <literal>String</literal> versus
+                      <literal>Object</literal>) are between these two.</para>
+                    </listitem>
+
+                    <listitem>
+                      <para>When comparing the specificity of two parameters
+                      types at the same parameter position: The algorithm now
+                      considers a primitive type as more specific that its
+                      corresponding boxing class (like <literal>int</literal>
+                      is considered to be more specific than
+                      <literal>Integer</literal>).</para>
+                    </listitem>
+
+                    <listitem>
+                      <para>The was a bug with overloaded varargs methods of
+                      different parameter counts, where sometimes the last
+                      parameters of the compared methods was ignored, which is
+                      taking away a potential deciding factor and thus can
+                      lead to ambiguity error. Whether this happened depends
+                      on the order in which the Java reflection API has
+                      returned the methods, which is undocumented and known to
+                      change at least after some Java updates, breaking the
+                      application.</para>
                     </listitem>
                   </itemizedlist>
                 </listitem>
@@ -20466,21 +20540,23 @@
                   unwrapping hint has influence when there's an ambiguity
                   regarding how to create a Java object form an FTL value. In
                   the vast majority of the cases, a too generic hint has no
-                  effect.) (This is a highly technical topic. The limitation
-                  of the applied algorithm is that, a single common unwrapping
-                  hint class is chosen for a given argument position shared by
-                  the overloads that has the same number of parameters. The
-                  issue with the too generic hints had several instances: (a)
-                  When the most specific common class/interface of two
-                  same-position parameter types were searched, if there was
-                  multiple common class/interfaces that had no relationship
-                  (this is always at most one class and one or more unrelated
-                  interfaces), due to the ambiguity it has felt back to using
-                  <literal>Object</literal> as the unwrapping hint. Now if
-                  there's a non-<literal>Object</literal> class among them in
-                  such case, it will be chosen as the hint. Otherwise if only
-                  a single interface remains by removing
-                  <literal>Cloneable</literal>,
+                  effect.) (This is a highly technical topic. The way it works
+                  is that a single common unwrapping hint class is chosen for
+                  a given argument position shared by the overloads that has
+                  the same number of parameters, and that hint class has to be
+                  as specific as possible while it must fit all those
+                  parameter types. The issue with the too generic hints had
+                  several instances: (a) When the most specific common
+                  class/interface of two same-position parameter types were
+                  searched, if there was multiple common class/interfaces that
+                  had no relationship (this is always at most one class and
+                  one or more unrelated interfaces), due to the ambiguity it
+                  has felt back to using <literal>Object</literal> as the
+                  unwrapping hint. Now if there's a
+                  non-<literal>Object</literal> class among them in such case,
+                  it will be chosen as the hint (i.e., we ignore the common
+                  interfaces). Otherwise if only a single interface remains by
+                  removing <literal>Cloneable</literal>,
                   <literal>Serializable</literal>, and
                   <literal>Comparable</literal> (in that order), that will be
                   chosen. Only then it falls back to
@@ -20488,7 +20564,8 @@
                   class of a primitive type and the corresponding boxing class
                   was sometimes <literal>Object</literal> instead of the
                   boxing class. This depended on Java's the internal ordering
-                  of the methods, and so were quite unpredictable. (c) The
+                  of the methods, and so were quite unpredictable, like could
+                  change after upgrading Java under the application. (c) The
                   common superclass of a numerical primitive value and a
                   numerical non-primitive value was always
                   <literal>Object</literal>, now if they are primitive-boxing
@@ -20497,12 +20574,38 @@
                   position was not the same in all the overloaded varargs
                   methods, sometimes some varargs arguments where unwrapped
                   with too generic hints. When this happened was unpredictable
-                  as it depended on the internal method ordering.)</para>
+                  as it depended on the internal method ordering
+                  again.)</para>
                 </listitem>
               </itemizedlist>
             </listitem>
 
             <listitem>
+              <para>Bug fixed, fix is active only if you create the
+              <literal>BeansWrapper</literal>/<literal>DefaultObjectWrapper</literal>
+              with constructor parameter <literal>Version(2, 3, 21)</literal>
+              or higher: When <literal>BeansWrapper</literal> (and so also
+              <literal>DefaultObjectWrapper</literal>) has unwrapped method
+              call arguments before calling a Java method, when the argument
+              was a <literal>AdapterTemplateModel</literal> and the target
+              parameter type was primitive,
+              <literal>AdapterTemplateModel.getAdaptedObject(Class
+              hint)</literal> has received the primitive type of the target
+              parameter (like <literal>int</literal> instead of
+              <literal>Integer</literal>) as the hint. This did not make sense
+              since <literal>getAdaptedObject</literal> can only return
+              <literal>Object</literal>-s, not primitive values. Furthermore,
+              <literal>BeansWrapper</literal> has expected the returned value
+              to be of the primitive type, otherwise it has discarded it.
+              Exactly the same problem occurs with
+              <literal>WrapperTemplateModel</literal>. Thus, ultimately, if
+              the target parameter type was primitive and some of these
+              interfaces were implemented, their return value was always
+              discarded, instead FreeMarker has felt back to other means of
+              unwrapping.</para>
+            </listitem>
+
+            <listitem>
               <para><literal>ParseException</literal>-s now also store the
               end-location of the error, not just its start-location. This is
               useful if you want to underline the error in the source code,
diff --git a/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java b/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
index de167eb..0aee299 100644
--- a/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
+++ b/src/test/java/freemarker/ext/beans/BeansWrapperWithShortedMethods.java
@@ -27,13 +27,12 @@
     @Override
     MethodDescriptor[] shortMethodDescriptors(MethodDescriptor[] methodDescriptors) {
         ArrayList<MethodDescriptor> ls = new ArrayList<MethodDescriptor>(Arrays.asList(methodDescriptors));
-                Collections.sort(ls, new Comparator<MethodDescriptor>() {
-        
-                    public int compare(MethodDescriptor o1, MethodDescriptor o2) {
-                        int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
-                        return desc ? -res : res;
-                    }
-                });
+        Collections.sort(ls, new Comparator<MethodDescriptor>() {
+            public int compare(MethodDescriptor o1, MethodDescriptor o2) {
+                int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
+                return desc ? -res : res;
+            }
+        });
         return ls.toArray(new MethodDescriptor[ls.size()]);
     }
 
diff --git a/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java b/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
index d5b8775..97718da 100644
--- a/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
+++ b/src/test/java/freemarker/ext/beans/CommonSupertypeForUnwrappingHintTest.java
@@ -118,7 +118,7 @@
         }
 
         @Override
-        void afterWideningUnwrappingHints(Class[] paramTypes) {
+        void afterWideningUnwrappingHints(Class[] paramTypes, int[] paramNumericalTypes) {
             // Do nothing
         }
 
diff --git a/src/test/java/freemarker/ext/beans/IsApplicableTest.java b/src/test/java/freemarker/ext/beans/IsApplicableTest.java
new file mode 100644
index 0000000..24b7469
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/IsApplicableTest.java
@@ -0,0 +1,152 @@
+package freemarker.ext.beans;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class IsApplicableTest extends TestCase {
+
+    public IsApplicableTest(String name) {
+        super(name);
+    }
+    
+    public void testSingle() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object() }, true);
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        
+        ats = new ArgumentTypes(new Object[] { "" }, true);
+        assertApplicable(ats, Object.class);
+        assertApplicable(ats, CharSequence.class);
+        assertApplicable(ats, String.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+
+        ats = new ArgumentTypes(new Object[] { 1 }, true);
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, Short.class);
+        assertNotApplicable(ats, Short.TYPE);
+        assertApplicable(ats, Integer.class);
+        assertApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertApplicable(ats, BigInteger.class);
+
+        ats = new ArgumentTypes(new Object[] { new OverloadedNumberUtil.IntegerOrByte(1, (byte) 1) }, true);
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertApplicable(ats, Short.class);
+        assertApplicable(ats, Short.TYPE);
+        assertApplicable(ats, Integer.class);
+        assertApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertApplicable(ats, BigInteger.class);
+        
+        ats = new ArgumentTypes(new Object[] { 1.0f }, true);
+        assertApplicable(ats, Object.class);
+        assertNotApplicable(ats, CharSequence.class);
+        assertNotApplicable(ats, String.class);
+        assertNotApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        assertApplicable(ats, Float.class);
+        assertApplicable(ats, Float.TYPE);
+        assertApplicable(ats, Double.class);
+        assertApplicable(ats, Double.TYPE);
+        assertApplicable(ats, BigDecimal.class);
+        assertNotApplicable(ats, BigInteger.class);
+        
+        ats = new ArgumentTypes(new Object[] { null }, true);
+        assertApplicable(ats, Object.class);
+        assertApplicable(ats, String.class);
+        assertApplicable(ats, Integer.class);
+        assertNotApplicable(ats, Integer.TYPE);
+        assertNotApplicable(ats, Boolean.TYPE);
+        assertNotApplicable(ats, Object.class, Object.class);
+        assertNotApplicable(ats);
+    }
+    
+    public void testMulti() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { new Object(), "", 1, true }, true);
+        assertApplicable(ats, Object.class, Object.class, Object.class, Object.class);
+        assertApplicable(ats, Object.class, String.class, Number.class, Boolean.class);
+        assertApplicable(ats, Object.class, CharSequence.class, Integer.class, Serializable.class);
+        assertApplicable(ats, Object.class, Comparable.class, Integer.TYPE, Serializable.class);
+        assertNotApplicable(ats, Object.class, String.class, Number.class, Number.class);
+        assertNotApplicable(ats, Object.class, StringBuffer.class, Number.class, Boolean.class);
+        assertNotApplicable(ats, int.class, Object.class, Object.class, Object.class);
+        assertNotApplicable(ats, Object.class, Object.class, Object.class);
+        assertNotApplicable(ats, Object.class, Object.class, Object.class, Object.class, Object.class);
+    }    
+
+    public void testNoParam() {
+        ArgumentTypes ats = new ArgumentTypes(new Object[] { }, true);
+        assertApplicable(ats);
+        assertNotApplicable(ats, Object.class);
+    }
+
+    public void testVarags() {
+        Object[][] argLists = new Object[][] {
+            new Object[] { "", 1, 2, 3 },
+            new Object[] { "", 1, (byte) 2, 3 },
+            new Object[] { "", 1},
+            new Object[] { "" },
+        };
+        for (Object[] args : argLists) {
+            ArgumentTypes ats = new ArgumentTypes(args, true);
+            assertApplicable(ats, true, String.class, int[].class);
+            assertApplicable(ats, true, String.class, Integer[].class);
+            assertApplicable(ats, true, Object.class, Comparable[].class);
+            assertApplicable(ats, true, Object.class, Object[].class);
+            assertNotApplicable(ats, true, StringBuilder.class, int[].class);
+            if (args.length > 1) {
+                assertNotApplicable(ats, true, String.class, String[].class);
+            } else {
+                assertApplicable(ats, true, String.class, String[].class);
+            }
+        }
+    }
+    
+    private void assertNotApplicable(ArgumentTypes ats, Class... paramTypes) {
+        assertNotApplicable(ats, false, paramTypes);
+    }
+    
+    private void assertNotApplicable(ArgumentTypes ats, boolean varargs, Class... paramTypes) {
+        List tested = new ArrayList();
+        tested.add(new CallableMemberDescriptor((Method) null, paramTypes));
+        if (ats.getApplicables(tested, varargs).size() != 0) {
+            fail("Parameter types were applicable");
+        }
+    }
+
+    private void assertApplicable(ArgumentTypes ats, Class<?>... paramTypes) {
+        assertApplicable(ats, false, paramTypes);
+    }
+    
+    private void assertApplicable(ArgumentTypes ats, boolean varargs, Class<?>... paramTypes) {
+        List tested = new ArrayList();
+        tested.add(new CallableMemberDescriptor((Method) null, paramTypes));
+        if (ats.getApplicables(tested, varargs).size() != 1) {
+            fail("Parameter types weren't applicable");
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java b/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java
new file mode 100644
index 0000000..759f4ff
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MiscNumericalOperationsTest.java
@@ -0,0 +1,100 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.junit.Assert;
+
+import freemarker.template.Version;
+
+import junit.framework.TestCase;
+
+public class MiscNumericalOperationsTest extends TestCase {
+
+    public MiscNumericalOperationsTest(String name) {
+        super(name);
+    }
+    
+    public void testForceUnwrappedNumberToType() {
+        // Usual type to to all other types:
+        Double n = Double.valueOf(123.75);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Short.class, false), Short.valueOf(n.shortValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Short.TYPE, false), Short.valueOf(n.shortValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Byte.class, false), Byte.valueOf(n.byteValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Byte.TYPE, false), Byte.valueOf(n.byteValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Integer.class, false), Integer.valueOf(n.intValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Integer.TYPE, false), Integer.valueOf(n.intValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Long.class, false), Long.valueOf(n.longValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Long.TYPE, false), Long.valueOf(n.longValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Float.class, false), Float.valueOf(n.floatValue()));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, Float.TYPE, false), Float.valueOf(n.floatValue()));
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Double.class, false) == n);
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Double.TYPE, false) == n);
+        try {
+            assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigInteger.class, false), new BigInteger("123"));
+            fail();
+        } catch (NumberFormatException e) {
+            // expected
+        }
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigInteger.class, true), new BigInteger("123"));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(n, BigDecimal.class, false), new BigDecimal("123.75"));
+        
+        // Cases of conversion to BigDecimal:
+        BigDecimal bd = new BigDecimal("123");
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(new BigInteger("123"), BigDecimal.class, false), bd);
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(bd, BigDecimal.class, false) == bd);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(Long.valueOf(123), BigDecimal.class, false), bd);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(Double.valueOf(123), BigDecimal.class, false), bd);
+        
+        // Cases of conversion to BigInteger:
+        BigInteger bi = new BigInteger("123");
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(new BigDecimal("123.6"), BigInteger.class, true), bi);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(
+                new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal("123")), BigInteger.class, true), bi);
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(bi, BigInteger.class, true) == bi);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(Long.valueOf(123), BigInteger.class, true), bi);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(Double.valueOf(123.6), BigInteger.class, true), bi);
+
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(n, Number.class, true) == n);
+        assertNull(BeansWrapper.forceUnwrappedNumberToType(n, RationalNumber.class, true));
+        RationalNumber r = new RationalNumber(1, 2);
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(r, RationalNumber.class, true) == r);
+        assertTrue(BeansWrapper.forceUnwrappedNumberToType(r, Number.class, true) == r);
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, Double.class, true), Double.valueOf(0.5));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, BigDecimal.class, true), new BigDecimal("0.5"));
+        assertEquals(BeansWrapper.forceUnwrappedNumberToType(r, BigInteger.class, true), BigInteger.ZERO);
+    }
+    
+    @SuppressWarnings("boxing")
+    public void testForceNumberArgumentsToParameterTypes() {
+        OverloadedMethodsSubset oms = new OverloadedFixArgsMethods(new BeansWrapper(new Version(2, 3, 21)));
+        Class[] paramTypes = new Class[] { Short.TYPE, Short.class, Double.TYPE, BigDecimal.class, BigInteger.class };
+        Object[] args;
+        
+        args = newArgs();
+        oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF });
+        Assert.assertArrayEquals(
+                args,
+                new Object[] { (short) 123, (short) 123, 123.75, new BigDecimal("123.75"), BigInteger.valueOf(123) });
+        
+        args = newArgs();
+        oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0, 0, 0, 0, 0 });
+        Assert.assertArrayEquals(args, newArgs());
+        
+        args = newArgs();
+        oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 8, 8, 8, 8, 8 });
+        Assert.assertArrayEquals(args, newArgs());
+        
+        args = newArgs();
+        oms.forceNumberArgumentsToParameterTypes(args, paramTypes, new int[] { 0xFFFF, 0, 0xFFFF, 0, 0xFFFF });
+        Assert.assertArrayEquals(
+                args,
+                new Object[] { (short) 123, 123.75, 123.75, 123.75, BigInteger.valueOf(123) });
+    }
+
+    @SuppressWarnings("boxing")
+    private Object[] newArgs() {
+        return new Object[] { 123.75, 123.75, 123.75, 123.75, 123.75 };
+    }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java b/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java
new file mode 100644
index 0000000..58c5892
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/OverloadedMethodsNumericalFlagsTest.java
@@ -0,0 +1,338 @@
+package freemarker.ext.beans;
+
+import java.beans.MethodDescriptor;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+import freemarker.template.Version;
+
+public class OverloadedMethodsNumericalFlagsTest extends TestCase {
+
+    public OverloadedMethodsNumericalFlagsTest(String name) {
+        super(name);
+    }
+    
+    private final BeansWrapper bw = new BeansWrapper(new Version(2, 3, 21)); 
+
+    public void testSingleNumType() {
+        checkPossibleParamTypes(SingleTypeC.class, "mInt",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT);
+        checkPossibleParamTypes(SingleTypeC.class, "mLong",
+                OverloadedNumberUtil.FLAG_LONG);
+        checkPossibleParamTypes(SingleTypeC.class, "mShort",
+                OverloadedNumberUtil.FLAG_SHORT);
+        checkPossibleParamTypes(SingleTypeC.class, "mByte",
+                OverloadedNumberUtil.FLAG_BYTE,
+                0);
+        checkPossibleParamTypes(SingleTypeC.class, "mDouble",
+                OverloadedNumberUtil.FLAG_DOUBLE);
+        checkPossibleParamTypes(SingleTypeC.class, "mFloat",
+                OverloadedNumberUtil.FLAG_FLOAT);
+        checkPossibleParamTypes(SingleTypeC.class, "mUnknown",
+                OverloadedNumberUtil.FLAG_UNKNOWN_TYPE);
+        
+        checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+                OverloadedNumberUtil.FLAG_BIG_DECIMAL);
+        checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+                OverloadedNumberUtil.FLAG_BIG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE);
+        checkPossibleParamTypes(SingleTypeC.class, "mVarParamCnt",
+                OverloadedNumberUtil.FLAG_DOUBLE,
+                OverloadedNumberUtil.FLAG_FLOAT,
+                OverloadedNumberUtil.FLAG_INTEGER);
+    }
+    
+    static public class SingleTypeC {
+        public void mInt(int a1, String a2) { }
+        public void mInt(int a1, int a2) { }
+        public void mLong(long a1) { }
+        public void mLong(Long a1) { }
+        public void mShort(short a1) { }
+        public void mByte(byte a1, boolean a2) { }
+        public void mByte(byte a1, String a2) { }
+        public void mByte(byte a1, Object a2) { }
+        public void mDouble(double a1) { }
+        public void mFloat(float a1) { }
+        public void mUnknown(RationalNumber a1) { };
+        public void mVarParamCnt(BigDecimal a1) { }
+        public void mVarParamCnt(BigInteger a1, Double a2) { }
+        public void mVarParamCnt(Double a1, Float a2, Integer a3) { }
+        public void mVarParamCnt(Object a1, char a2, boolean a3, File a4, Map a5, Boolean a6) { }
+        public void mVarParamCnt(Long a1, int a2, short a3, byte a4, double a5, float a6) { }
+    }
+
+    public void testMultipleNumType() {
+        checkPossibleParamTypes(MultiTypeC.class, "m1",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT 
+                | OverloadedNumberUtil.FLAG_BYTE | OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+                );
+        
+        checkPossibleParamTypes(MultiTypeC.class, "m2",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT 
+                | OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_FLOAT
+                );
+
+        checkPossibleParamTypes(MultiTypeC.class, "m3",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT 
+                | OverloadedNumberUtil.FLAG_BIG_DECIMAL| OverloadedNumberUtil.FLAG_BIG_INTEGER,
+                OverloadedNumberUtil.FLAG_BIG_INTEGER,
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                | OverloadedNumberUtil.FLAG_BIG_DECIMAL | OverloadedNumberUtil.FLAG_UNKNOWN_TYPE
+                );
+        
+        checkPossibleParamTypes(MultiTypeC.class, "m4",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_FLOAT
+                );
+        
+        checkPossibleParamTypes(MultiTypeC.class, "m5",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                | OverloadedNumberUtil.FLAG_FLOAT | OverloadedNumberUtil.FLAG_DOUBLE
+                );
+        
+        checkPossibleParamTypes(MultiTypeC.class, "m6",
+                OverloadedNumberUtil.FLAG_INTEGER
+                );
+        assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", false, 2), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", true, 2), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", false, 3), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        assertEquals(getPossibleParamTypes(MultiTypeC.class, "m6", true, 3), OverloadedMethodsSubset.ALL_ZEROS_ARRAY);
+        checkPossibleParamTypes(MultiTypeC.class, "m6",
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_DOUBLE,
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT | OverloadedNumberUtil.FLAG_DOUBLE,
+                0
+                );
+    }
+    
+    static public class MultiTypeC {
+        public void m1(byte a1) { };
+        public void m1(int a1) { };
+        public void m1(double a2) { };
+        
+        public void m2(short a1) { };
+        public void m2(long a1) { };
+        public void m2(float a1) { };
+        public void m2(char a1) { };
+        
+        public void m3(BigInteger a1, BigInteger a2, BigDecimal a3) { };
+        public void m3(BigDecimal a1, BigInteger a2, RationalNumber a3) { };
+        
+        public void m4(float a1) { };
+        public void m4(char a1) { };
+        
+        public void m5(Float a1) { };
+        public void m5(Double a1) { };
+        public void m5(Enum a1) { };
+        
+        public void m6(int a1) { };
+        public void m6(String a1, char a2) { };
+        public void m6(String a1, char a2, boolean a3) { };
+        public void m6(String a1, char a2, char a3) { };
+        public void m6(double a1, int a2, String a3, String a4) { };
+        public void m6(String a1, int a2, double a3, String a4) { };
+    }
+
+    public void testVarArgs() {
+        checkPossibleParamTypes(VarArgsC.class, "m1",
+                OverloadedNumberUtil.FLAG_INTEGER
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m2",
+                OverloadedNumberUtil.FLAG_DOUBLE,
+                OverloadedNumberUtil.FLAG_INTEGER
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m3",
+                OverloadedNumberUtil.FLAG_INTEGER
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m3",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m3",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER
+                | OverloadedNumberUtil.FLAG_BIG_DECIMAL | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m4",
+                OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m5",
+                OverloadedNumberUtil.FLAG_LONG
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m6",
+                OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m7",
+                OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_BYTE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+
+        checkPossibleParamTypes(VarArgsC.class, "m8",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m9",
+                OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_BYTE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_DOUBLE
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m10",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m10",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE,
+                OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m11",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m11",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m12",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE
+                );
+        checkPossibleParamTypes(VarArgsC.class, "m12",
+                OverloadedNumberUtil.FLAG_INTEGER,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_SHORT
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_BYTE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+        
+        checkPossibleParamTypes(VarArgsC.class, "m13",
+                0,
+                OverloadedNumberUtil.FLAG_DOUBLE);
+        checkPossibleParamTypes(VarArgsC.class, "m13",
+                0,
+                OverloadedNumberUtil.FLAG_DOUBLE,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_UNKNOWN_TYPE
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT,
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_LONG
+                | OverloadedNumberUtil.FLAG_WIDENED_UNWRAPPING_HINT
+                );
+    }
+    
+    static public class VarArgsC {
+        public void m1(int... va) { }
+        
+        public void m2(double a1, int... va) { }
+        
+        public void m3(int... va) { }
+        public void m3(int a1, double... va) { }
+        public void m3(int a1, double a2, BigDecimal... va) { }
+        
+        public void m4(int... va) { }
+        public void m4(long... va) { }
+
+        public void m5(Long... va) { }
+        public void m5(long... va) { }
+        
+        public void m6(long... va) { }
+        public void m6(String... va) { }
+        
+        public void m7(int a1, double... va) { }
+        public void m7(byte a1, float... va) { }
+
+        public void m8(int a1, double... va) { }
+        public void m8(int a1, float... va) { }
+        
+        public void m9(int a1, double... va) { }
+        public void m9(byte a1, double... va) { }
+        
+        public void m10(int a1, double a2, long... va) { }
+        public void m10(int a1, double... va) { }
+        
+        public void m11(int a1, short a2, long... va) { }
+        public void m11(int a1, double... va) { }
+        
+        public void m12(int a1, short a2, byte a3, long... va) { }
+        public void m12(int a1, double... va) { }
+        
+        public void m13(char a1, double a2, RationalNumber a3, Long... va) { }
+        public void m13(char a1, Double... va) { }
+    }
+
+    private OverloadedMethodsSubset newOverloadedMethodsSubset(Class cl, String methodName, final boolean desc) {
+        final Method[] ms = cl.getMethods();
+        
+        final List<Method> filteredMethods = new ArrayList();
+        for (Method m : ms) {
+            if (m.getName().equals(methodName)) {
+                filteredMethods.add(m);
+            }
+        }
+        // As the order in which getMethods() returns the methods is undefined, we sort them for test predictability: 
+        Collections.sort(filteredMethods, new Comparator<Method>() {
+            public int compare(Method o1, Method o2) {
+                int res = o1.toString().compareTo(o2.toString());
+                return desc ? -res : res;
+            }
+        });
+        
+        final OverloadedMethodsSubset oms = cl.getName().indexOf("VarArgs") == -1
+                ? new OverloadedFixArgsMethods(bw) : new OverloadedVarArgsMethods(bw);
+        for (Method m : filteredMethods) {
+            oms.addCallableMemberDescriptor(new CallableMemberDescriptor(m, m.getParameterTypes()));
+        }
+        return oms;
+    }
+    
+    private void checkPossibleParamTypes(Class cl, String methodName, int... expectedParamTypes) {
+        checkPossibleParamTypes(cl, methodName, false, expectedParamTypes);
+        checkPossibleParamTypes(cl, methodName, true, expectedParamTypes);
+    }
+    
+    private void checkPossibleParamTypes(Class cl, String methodName, boolean revMetOrder, int... expectedParamTypes) {
+        int[] actualParamTypes = getPossibleParamTypes(cl, methodName, revMetOrder, expectedParamTypes.length);
+        assertNotNull(actualParamTypes);
+        assertEquals(expectedParamTypes.length, actualParamTypes.length);
+        for (int i = 0; i < expectedParamTypes.length; i++) {
+            assertEquals(expectedParamTypes[i], actualParamTypes[i]);
+        }
+    }
+
+    private int[] getPossibleParamTypes(Class cl, String methodName, boolean revMetOrder, int paramCnt) {
+        OverloadedMethodsSubset oms = newOverloadedMethodsSubset(cl, methodName, revMetOrder);
+        int[] actualParamTypes = oms.getPossibleNumericalTypes(paramCnt);
+        return actualParamTypes;
+    }
+    
+}
diff --git a/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java b/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java
new file mode 100644
index 0000000..a51828a
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/OverloadedNumberUtilTest.java
@@ -0,0 +1,566 @@
+package freemarker.ext.beans;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class OverloadedNumberUtilTest extends TestCase {
+
+    public OverloadedNumberUtilTest(String name) {
+        super(name);
+    }
+    
+    public void testIntegerCoercions() {
+        cipEqu(new Byte(Byte.MAX_VALUE));
+        cipEqu(new Byte((byte) 0));
+        cipEqu(new Byte(Byte.MIN_VALUE));
+        
+        cipEqu(new Short(Byte.MAX_VALUE),
+                new OverloadedNumberUtil.ShortOrByte((short) Byte.MAX_VALUE, Byte.MAX_VALUE));
+        cipEqu(new Short((short) 0),
+                new OverloadedNumberUtil.ShortOrByte((short) 0, (byte) 0));
+        cipEqu(new Short(Byte.MIN_VALUE),
+                new OverloadedNumberUtil.ShortOrByte((short) Byte.MIN_VALUE, Byte.MIN_VALUE));
+        
+        cipEqu(new Short((short) (Byte.MAX_VALUE + 1)));
+        cipEqu(new Short((short) (Byte.MIN_VALUE - 1)));
+        cipEqu(new Short(Short.MAX_VALUE));
+        cipEqu(new Short(Short.MIN_VALUE));
+        
+        cipEqu(new Integer(Byte.MAX_VALUE),
+                new OverloadedNumberUtil.IntegerOrByte((int) Byte.MAX_VALUE, Byte.MAX_VALUE));
+        cipEqu(new Integer(0),
+                new OverloadedNumberUtil.IntegerOrByte(0, (byte) 0));
+        cipEqu(new Integer(Byte.MIN_VALUE),
+                new OverloadedNumberUtil.IntegerOrByte((int) Byte.MIN_VALUE, Byte.MIN_VALUE));
+        
+        cipEqu(new Integer(Byte.MAX_VALUE + 1),
+                new OverloadedNumberUtil.IntegerOrShort(Byte.MAX_VALUE + 1, (short) (Byte.MAX_VALUE + 1)));
+        cipEqu(new Integer(Byte.MIN_VALUE - 1),
+                new OverloadedNumberUtil.IntegerOrShort(Byte.MIN_VALUE - 1, (short) (Byte.MIN_VALUE - 1)));
+        cipEqu(new Integer(Short.MAX_VALUE),
+                new OverloadedNumberUtil.IntegerOrShort((int) Short.MAX_VALUE, Short.MAX_VALUE));
+        cipEqu(new Integer(Short.MIN_VALUE),
+                new OverloadedNumberUtil.IntegerOrShort((int) Short.MIN_VALUE, Short.MIN_VALUE));
+        
+        cipEqu(new Integer(Short.MAX_VALUE + 1));
+        cipEqu(new Integer(Short.MIN_VALUE - 1));
+        cipEqu(new Integer(Integer.MAX_VALUE));
+        cipEqu(new Integer(Integer.MIN_VALUE));
+        
+        cipEqu(new Long(Byte.MAX_VALUE),
+                new OverloadedNumberUtil.LongOrByte((long) Byte.MAX_VALUE, Byte.MAX_VALUE));
+        cipEqu(new Long(0),
+                new OverloadedNumberUtil.LongOrByte((long) 0, (byte) 0));
+        cipEqu(new Long(Byte.MIN_VALUE),
+                new OverloadedNumberUtil.LongOrByte((long) Byte.MIN_VALUE, Byte.MIN_VALUE));
+        
+        cipEqu(new Long(Byte.MAX_VALUE + 1),
+                new OverloadedNumberUtil.LongOrShort((long) (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+        cipEqu(new Long(Byte.MIN_VALUE - 1),
+                new OverloadedNumberUtil.LongOrShort((long) (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+        cipEqu(new Long(Short.MAX_VALUE),
+                new OverloadedNumberUtil.LongOrShort((long) Short.MAX_VALUE, Short.MAX_VALUE));
+        cipEqu(new Long(Short.MIN_VALUE),
+                new OverloadedNumberUtil.LongOrShort((long) Short.MIN_VALUE, Short.MIN_VALUE));
+
+        cipEqu(new Long(Short.MAX_VALUE + 1),
+                new OverloadedNumberUtil.LongOrInteger((long) Short.MAX_VALUE + 1, Short.MAX_VALUE + 1));
+        cipEqu(new Long(Short.MIN_VALUE - 1),
+                new OverloadedNumberUtil.LongOrInteger((long) Short.MIN_VALUE - 1, Short.MIN_VALUE - 1));
+        cipEqu(new Long(Integer.MAX_VALUE),
+                new OverloadedNumberUtil.LongOrInteger((long) Integer.MAX_VALUE, Integer.MAX_VALUE));
+        cipEqu(new Long(Integer.MIN_VALUE),
+                new OverloadedNumberUtil.LongOrInteger((long) Integer.MIN_VALUE, Integer.MIN_VALUE));
+        
+        cipEqu(new Long(Integer.MAX_VALUE + 1L));
+        cipEqu(new Long(Integer.MIN_VALUE - 1L));
+        cipEqu(new Long(Long.MAX_VALUE));
+        cipEqu(new Long(Long.MIN_VALUE));
+    }
+    
+    public void testIntegerNoCoercions() {
+        cipEqu(new Integer(Byte.MAX_VALUE), new Integer(Byte.MAX_VALUE), 0);
+        cipEqu(new Integer(0), new Integer(0), 0);
+        cipEqu(new Integer(Byte.MIN_VALUE), new Integer(Byte.MIN_VALUE), 0);
+    }
+    
+    public void testIntegerLimitedCoercions() {
+        cipEqu(new Integer(Byte.MAX_VALUE), new Integer(Byte.MAX_VALUE), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(new Integer(0), new Integer(0), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(new Integer(Byte.MIN_VALUE), new Integer(Byte.MIN_VALUE), OverloadedNumberUtil.FLAG_INTEGER);
+        
+        cipEqu(new Long(Integer.MAX_VALUE + 1L), new Long(Integer.MAX_VALUE + 1L), OverloadedNumberUtil.FLAG_INTEGER);
+        
+        for (int n = -1; n < 2; n++) {
+            final Long longN = new Long(n);
+            cipEqu(longN, new OverloadedNumberUtil.LongOrInteger(longN, n), OverloadedNumberUtil.FLAG_INTEGER);
+            cipEqu(longN, new OverloadedNumberUtil.LongOrShort(longN, (short) n), OverloadedNumberUtil.FLAG_SHORT);
+            cipEqu(longN, new OverloadedNumberUtil.LongOrByte(longN, (byte) n), OverloadedNumberUtil.FLAG_BYTE);
+            cipEqu(longN, new OverloadedNumberUtil.LongOrShort(longN, (short) n),
+                    OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_INTEGER);
+        }
+    }
+
+    public void testBigDecimalCoercions() {
+        cipEqu(new BigDecimal(123), new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal(123)));
+        cipEqu(new BigDecimal(123), new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal(123)),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG);
+        cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_DOUBLE);
+        cipEqu(new BigDecimal(123), OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        
+        cipEqu(new BigDecimal(123.5));
+        // Not wasting time with check if it's a whole number if we only have integer-only or non-integer-only targets:  
+        cipEqu(new BigDecimal(123.5), OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_LONG);
+        cipEqu(new BigDecimal(123.5), OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        
+        cipEqu(new BigDecimal(0.01));
+        cipEqu(new BigDecimal(-0.01));
+        cipEqu(BigDecimal.ZERO, new OverloadedNumberUtil.IntegerBigDecimal(BigDecimal.ZERO));
+    }
+
+    public void testUnknownNumberCoercion() {
+        cipEqu(new RationalNumber(2, 3));
+    }
+
+    @SuppressWarnings("boxing")
+    public void testDoubleCoercion() {
+        cipEqu(Double.valueOf(1.5), new OverloadedNumberUtil.DoubleOrFloat(1.5));
+        cipEqu(Double.valueOf(-0.125), new OverloadedNumberUtil.DoubleOrFloat(-0.125));
+        cipEqu(Double.valueOf(Float.MAX_VALUE), new OverloadedNumberUtil.DoubleOrFloat((double) Float.MAX_VALUE));
+        cipEqu(Double.valueOf(-Float.MAX_VALUE), new OverloadedNumberUtil.DoubleOrFloat((double) -Float.MAX_VALUE));
+        cipEqu(Double.valueOf(Float.MAX_VALUE * 10.0));
+        cipEqu(Double.valueOf(-Float.MAX_VALUE * 10.0));
+        
+        cipEqu(Double.valueOf(0), new OverloadedNumberUtil.DoubleOrByte(0.0, (byte) 0));
+        cipEqu(Double.valueOf(Byte.MAX_VALUE), new OverloadedNumberUtil.DoubleOrByte((double) Byte.MAX_VALUE, Byte.MAX_VALUE));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE), new OverloadedNumberUtil.DoubleOrByte((double) Byte.MIN_VALUE, Byte.MIN_VALUE));
+        
+        cipEqu(Double.valueOf(Byte.MAX_VALUE + 1), new OverloadedNumberUtil.DoubleOrShort((double)
+                (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE - 1), new OverloadedNumberUtil.DoubleOrShort((double)
+                (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+        
+        cipEqu(Double.valueOf(Short.MAX_VALUE + 1),
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat((double) Short.MAX_VALUE + 1, Short.MAX_VALUE + 1));
+        cipEqu(Double.valueOf(Short.MIN_VALUE - 1),
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat((double) Short.MIN_VALUE - 1, Short.MIN_VALUE-  1));
+        cipEqu(Double.valueOf(16777216), new OverloadedNumberUtil.DoubleOrIntegerOrFloat(16777216.0, 16777216));
+        cipEqu(Double.valueOf(-16777216), new OverloadedNumberUtil.DoubleOrIntegerOrFloat(-16777216.0, -16777216));
+        
+        cipEqu(Double.valueOf(Integer.MAX_VALUE),
+                new OverloadedNumberUtil.DoubleOrInteger((double) Integer.MAX_VALUE, Integer.MAX_VALUE));
+        cipEqu(Double.valueOf(Integer.MIN_VALUE),
+                new OverloadedNumberUtil.DoubleOrInteger((double) Integer.MIN_VALUE, Integer.MIN_VALUE));
+        
+        cipEqu(Double.valueOf(Integer.MAX_VALUE + 1L),
+                new OverloadedNumberUtil.DoubleOrLong(Double.valueOf(Integer.MAX_VALUE + 1L), Integer.MAX_VALUE + 1L));
+        cipEqu(Double.valueOf(Integer.MIN_VALUE - 1L),
+                new OverloadedNumberUtil.DoubleOrLong(Double.valueOf(Integer.MIN_VALUE - 1L), Integer.MIN_VALUE - 1L));
+        cipEqu(Double.valueOf(Long.MAX_VALUE),
+                new OverloadedNumberUtil.DoubleOrFloat((double) Long.MAX_VALUE));
+        cipEqu(Double.valueOf(Long.MIN_VALUE),
+                new OverloadedNumberUtil.DoubleOrFloat((double) Long.MIN_VALUE));
+
+        // When only certain target types are present:
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrByte(5.0, (byte) 5), OverloadedNumberUtil.FLAG_BYTE);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrByte(5.0, (byte) 5), OverloadedNumberUtil.FLAG_BYTE | OverloadedNumberUtil.FLAG_SHORT);
+        cipEqu(Double.valueOf(-129), OverloadedNumberUtil.FLAG_BYTE);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrShort(5.0, (short) 5), OverloadedNumberUtil.FLAG_SHORT);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrInteger(5.0, 5), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrLong(5.0, 5), OverloadedNumberUtil.FLAG_LONG);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrFloat(5.0), OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(5), Double.valueOf(5), OverloadedNumberUtil.FLAG_DOUBLE);
+        cipEqu(Double.valueOf(5), new OverloadedNumberUtil.DoubleOrFloat(5.0),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(5.9), new OverloadedNumberUtil.DoubleOrFloat(5.9),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(5.9),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(5.9), new OverloadedNumberUtil.DoubleOrFloat(5.9),
+                OverloadedNumberUtil.FLAG_FLOAT | OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(5.9), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(Long.MAX_VALUE),
+                new OverloadedNumberUtil.DoubleOrFloat((double) Long.MAX_VALUE),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(Long.MAX_VALUE),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_LONG);
+        cipEqu(Double.valueOf(Long.MIN_VALUE),
+                new OverloadedNumberUtil.DoubleOrFloat((double) Long.MIN_VALUE),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(Float.MAX_VALUE * 10),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(Double.valueOf(-Float.MAX_VALUE * 10),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        
+        // Rounded values:
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrByte(0.0000009, (byte) 0));
+        cipEqu(Double.valueOf(-0.0000009),
+                new OverloadedNumberUtil.DoubleOrByte(-0.0000009, (byte) 0));
+        cipEqu(Double.valueOf(0.9999991),
+                new OverloadedNumberUtil.DoubleOrByte(0.9999991, (byte) 1));
+        cipEqu(Double.valueOf(-0.9999991),
+                new OverloadedNumberUtil.DoubleOrByte(-0.9999991, (byte) -1));
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrShort(0.0000009, (short) 0),
+                OverloadedNumberUtil.FLAG_SHORT | OverloadedNumberUtil.FLAG_DOUBLE);
+        cipEqu(Double.valueOf(0.0000009), new OverloadedNumberUtil.DoubleOrInteger(0.0000009, 0),
+                OverloadedNumberUtil.FLAG_INTEGER | OverloadedNumberUtil.FLAG_DOUBLE);
+        cipEqu(Double.valueOf(0.0000009), new OverloadedNumberUtil.DoubleOrLong(0.0000009, 0),
+                OverloadedNumberUtil.FLAG_LONG | OverloadedNumberUtil.FLAG_DOUBLE);
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrByte(0.0000009, (byte) 0), OverloadedNumberUtil.FLAG_BYTE);
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrShort(0.0000009, (short) 0), OverloadedNumberUtil.FLAG_SHORT);
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrInteger(0.0000009, 0), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(0.0000009),
+                new OverloadedNumberUtil.DoubleOrLong(0.0000009, 0L), OverloadedNumberUtil.FLAG_LONG);
+        cipEqu(Double.valueOf(0.9999999),
+                new OverloadedNumberUtil.DoubleOrInteger(0.9999999, 1), OverloadedNumberUtil.FLAG_INTEGER);
+        cipEqu(Double.valueOf(Byte.MAX_VALUE + 0.9e-6),
+                new OverloadedNumberUtil.DoubleOrByte(Byte.MAX_VALUE + 0.9e-6, Byte.MAX_VALUE));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE - 0.9e-6),
+                new OverloadedNumberUtil.DoubleOrByte(Byte.MIN_VALUE - 0.9e-6, Byte.MIN_VALUE));
+        cipEqu(Double.valueOf(Byte.MAX_VALUE + 1.1e-6),
+                new OverloadedNumberUtil.DoubleOrFloat(Byte.MAX_VALUE + 1.1e-6));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE - 1.1e-6),
+                new OverloadedNumberUtil.DoubleOrFloat(Byte.MIN_VALUE - 1.1e-6));
+        cipEqu(Double.valueOf(Byte.MAX_VALUE + 0.9999991),
+                new OverloadedNumberUtil.DoubleOrShort(
+                        Byte.MAX_VALUE + 0.9999991, (short) (Byte.MAX_VALUE + 1)));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE - 0.9999991),
+                new OverloadedNumberUtil.DoubleOrShort(
+                        Byte.MIN_VALUE - 0.9999991, (short) (Byte.MIN_VALUE - 1)));
+        
+        cipEqu(Double.valueOf(Byte.MAX_VALUE + 1), new OverloadedNumberUtil.DoubleOrShort((double)
+                (Byte.MAX_VALUE + 1), (short) (Byte.MAX_VALUE + 1)));
+        cipEqu(Double.valueOf(Byte.MIN_VALUE - 1), new OverloadedNumberUtil.DoubleOrShort((double)
+                (Byte.MIN_VALUE - 1), (short) (Byte.MIN_VALUE - 1)));
+        
+        cipEqu(Short.MAX_VALUE + 0.9999991,
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat(Short.MAX_VALUE + 0.9999991, Short.MAX_VALUE + 1));
+        cipEqu(Short.MIN_VALUE - 0.9999991,
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat(Short.MIN_VALUE - 0.9999991, Short.MIN_VALUE - 1));
+        cipEqu(16777216 + 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat(16777216 + 0.9e-6, 16777216));
+        cipEqu(-16777216 - 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrIntegerOrFloat(-16777216 - 0.9e-6, -16777216));
+        
+        cipEqu(Integer.MAX_VALUE + 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrInteger(Integer.MAX_VALUE + 0.9e-6, Integer.MAX_VALUE));
+        cipEqu(Integer.MIN_VALUE - 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrInteger(Integer.MIN_VALUE - 0.9e-6, Integer.MIN_VALUE));
+        
+        cipEqu(Integer.MAX_VALUE + 1L + 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrFloat(Integer.MAX_VALUE + 1L + 0.9e-6));
+        cipEqu(Integer.MIN_VALUE - 1L - 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrFloat(Integer.MIN_VALUE - 1L - 0.9e-6));
+        cipEqu(Long.MAX_VALUE + 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrFloat(Long.MAX_VALUE + 0.9e-6));
+        cipEqu(Long.MIN_VALUE - 0.9e-6,
+                new OverloadedNumberUtil.DoubleOrFloat(Long.MIN_VALUE - 0.9e-6));
+    }
+
+    @SuppressWarnings("boxing")
+    public void testFloatCoercion() {
+        cipEqu(1.00002f);
+        cipEqu(-1.00002f);
+        cipEqu(1.999989f);
+        cipEqu(-1.999989f);
+        cipEqu(16777218f);
+        cipEqu(-16777218f);
+        
+        cipEqu(1f, new OverloadedNumberUtil.FloatOrByte(1f, (byte) 1));
+        cipEqu(-1f, new OverloadedNumberUtil.FloatOrByte(-1f, (byte) -1));
+        cipEqu(1.000009f, new OverloadedNumberUtil.FloatOrByte(1.000009f, (byte) 1));
+        cipEqu(-1.000009f, new OverloadedNumberUtil.FloatOrByte(-1.000009f, (byte) -1));
+        cipEqu(1.999991f, new OverloadedNumberUtil.FloatOrByte(1.999991f, (byte) 2));
+        cipEqu(-1.999991f, new OverloadedNumberUtil.FloatOrByte(-1.999991f, (byte) -2));
+        
+        cipEqu(1000f, new OverloadedNumberUtil.FloatOrShort(1000f, (short) 1000));
+        cipEqu(-1000f, new OverloadedNumberUtil.FloatOrShort(-1000f, (short) -1000));
+        cipEqu(1000.00006f);
+
+        cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000));
+        cipEqu(-60000f, new OverloadedNumberUtil.FloatOrInteger(-60000f, -60000));
+        cipEqu(60000.004f);
+
+        cipEqu(100f, new OverloadedNumberUtil.FloatOrByte(100f, (byte) 100), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+        cipEqu(1000f, new OverloadedNumberUtil.FloatOrShort(1000f, (short) 1000), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+        cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000), OverloadedNumberUtil.MASK_KNOWN_INTEGERS);
+        cipEqu(60000f, new OverloadedNumberUtil.FloatOrInteger(60000f, 60000), OverloadedNumberUtil.FLAG_LONG);
+        cipEqu((float) Integer.MAX_VALUE, (float) Integer.MAX_VALUE, OverloadedNumberUtil.FLAG_LONG);
+        cipEqu((float) -Integer.MAX_VALUE, (float) -Integer.MAX_VALUE);
+        
+        cipEqu(0.5f, 0.5f, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(0.5f, 0.5f, OverloadedNumberUtil.FLAG_DOUBLE);
+    }
+    
+    public void testBigIntegerCoercion() {
+        BigInteger bi;
+        
+        cipEqu(BigInteger.ZERO, new OverloadedNumberUtil.BigIntegerOrByte(BigInteger.ZERO));
+        bi = new BigInteger(String.valueOf(Byte.MAX_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrByte(bi));
+        bi = new BigInteger(String.valueOf(Byte.MIN_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrByte(bi));
+        
+        bi = new BigInteger(String.valueOf(Byte.MAX_VALUE + 1));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+        bi = new BigInteger(String.valueOf(Byte.MIN_VALUE - 1));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+        bi = new BigInteger(String.valueOf(Short.MAX_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+        bi = new BigInteger(String.valueOf(Short.MIN_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrShort(bi));
+        
+        bi = new BigInteger(String.valueOf(Short.MAX_VALUE + 1));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+        bi = new BigInteger(String.valueOf(Short.MIN_VALUE - 1));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+        bi = new BigInteger(String.valueOf(Integer.MAX_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+        bi = new BigInteger(String.valueOf(Integer.MIN_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrInteger(bi));
+        
+        bi = new BigInteger(String.valueOf(Integer.MAX_VALUE + 1L));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+        bi = new BigInteger(String.valueOf(Integer.MIN_VALUE - 1L));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+        bi = new BigInteger(String.valueOf(Long.MAX_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+        bi = new BigInteger(String.valueOf(Long.MIN_VALUE));
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrLong(bi));
+        
+        cipEqu(new BigInteger(String.valueOf(Long.MAX_VALUE)).add(BigInteger.ONE));
+        cipEqu(new BigInteger(String.valueOf(Long.MIN_VALUE)).subtract(BigInteger.ONE));
+        
+        bi = new BigInteger("0");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+                OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+                OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi),
+                OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi),
+                OverloadedNumberUtil.FLAG_DOUBLE);
+
+        bi = new BigInteger("16777215");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        bi = new BigInteger("-16777215");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        
+        bi = new BigInteger("16777216");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        bi = new BigInteger("-16777216");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrFloat(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        
+        bi = new BigInteger("16777217");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+        bi = new BigInteger("-16777217");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+        
+        bi = new BigInteger("9007199254740991");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+        bi = new BigInteger("-9007199254740991");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+        
+        bi = new BigInteger("9007199254740992");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+        bi = new BigInteger("-9007199254740992");
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, new OverloadedNumberUtil.BigIntegerOrDouble(bi), OverloadedNumberUtil.FLAG_DOUBLE);
+        
+        bi = new BigInteger("9007199254740993");
+        cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE);
+        bi = new BigInteger("-9007199254740993");
+        cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE | OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_FLOAT);
+        cipEqu(bi, OverloadedNumberUtil.FLAG_DOUBLE);
+        
+        bi = new BigInteger("9007199254740994");
+        cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+        bi = new BigInteger("-9007199254740994");
+        cipEqu(bi, OverloadedNumberUtil.MASK_KNOWN_NONINTEGERS);
+    }
+    
+    private void cipEqu(Number actualAndExpected) {
+        cipEqu(actualAndExpected, actualAndExpected, -1);
+    }
+    
+    private void cipEqu(Number actual, Number expected) {
+        cipEqu(actual, expected, -1);
+    }
+
+    private void cipEqu(Number actualAndExpected, int flags) {
+        cipEqu(actualAndExpected, actualAndExpected, flags);
+    }
+    
+    @SuppressWarnings("boxing")
+    private void cipEqu(Number actual, Number expected, int flags) {
+        Number res = OverloadedNumberUtil.addFallbackType(actual, flags);
+        assertEquals(expected.getClass(), res.getClass());
+        assertEquals(expected, res);
+        
+        // Some number types wrap the number with multiple types and equals() only compares one of them. So we try to
+        // catch any inconsistency:
+        assertEquals(expected.byteValue(), res.byteValue());
+        assertEquals(expected.shortValue(), res.shortValue());
+        assertEquals(expected.intValue(), res.intValue());
+        assertEquals(expected.longValue(), res.longValue());
+        assertEquals(expected.floatValue(), res.floatValue());
+        assertEquals(expected.doubleValue(), res.doubleValue());
+    }
+    
+    public void testGetArgumentConversionPrice() {
+        // Unknown number types:
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                RationalNumber.class, Integer.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, RationalNumber.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                RationalNumber.class, Float.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                Float.class, RationalNumber.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                RationalNumber.class, RationalNumber.class));
+        
+        // Fully check some rows (not all of them; the code is generated anyways):
+
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Byte.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Short.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Integer.class));
+        assertEquals(10004, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Long.class));
+        assertEquals(10005, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, BigInteger.class));
+        assertEquals(30006, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Float.class));
+        assertEquals(20007, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, Double.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, BigDecimal.class));
+        
+        assertEquals(45001, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Byte.class));
+        assertEquals(44002, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Short.class));
+        assertEquals(41003, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Integer.class));
+        assertEquals(41004, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Long.class));
+        assertEquals(40005, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, BigInteger.class));
+        assertEquals(33006, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Float.class));
+        assertEquals(32007, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, Double.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, BigDecimal.class));
+        
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Byte.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Short.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Integer.class));
+        assertEquals(21004, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Long.class));
+        assertEquals(21005, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, BigInteger.class));
+        assertEquals(40006, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Float.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, Double.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, BigDecimal.class));
+        
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Byte.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Short.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Integer.class));
+        assertEquals(Integer.MAX_VALUE, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Long.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, BigInteger.class));
+        assertEquals(40006, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Float.class));
+        assertEquals(20007, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, Double.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, BigDecimal.class));
+        
+        // Check if all fromC is present:
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Byte.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Short.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Integer.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Long.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigInteger.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Float.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                Double.class, BigDecimal.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                BigDecimal.class, BigDecimal.class));
+        assertEquals(0, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.IntegerBigDecimal.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrFloat.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.FloatOrByte.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrShort.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.FloatOrByte.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.FloatOrShort.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.FloatOrInteger.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrByte.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrIntegerOrFloat.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrInteger.class, BigDecimal.class));
+        assertEquals(20008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.DoubleOrLong.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrByte.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrShort.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrInteger.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrLong.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrFloat.class, BigDecimal.class));
+        assertEquals(10008, OverloadedNumberUtil.getArgumentConversionPrice(
+                OverloadedNumberUtil.BigIntegerOrDouble.class, BigDecimal.class));
+    }
+    
+}
diff --git a/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
new file mode 100644
index 0000000..6908554
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/ParameterListPreferabilityTest.java
@@ -0,0 +1,408 @@
+package freemarker.ext.beans;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import freemarker.template.utility.NumberUtil;
+
+import junit.framework.TestCase;
+
+@SuppressWarnings("boxing")
+public class ParameterListPreferabilityTest extends TestCase {
+
+    public ParameterListPreferabilityTest(String name) {
+        super(name);
+    }
+    
+    public void testNumberical() {
+        // Note: the signature lists consists of the same elements, only their order changes depending on the type
+        // of the argument value.
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { byte.class },
+                    new Class[] { Byte.class },
+                    new Class[] { short.class },
+                    new Class[] { Short.class },
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { BigDecimal.class },
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class }
+                },
+                new Object[] { (byte) 1 });
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { BigDecimal.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    new Class[] { short.class },
+                    new Class[] { Short.class },
+                    new Class[] { byte.class },
+                    new Class[] { Byte.class },
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class },
+                },
+                new Object[] { new OverloadedNumberUtil.IntegerBigDecimal(new BigDecimal("1")) });
+
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { BigDecimal.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { short.class },
+                    new Class[] { Short.class },
+                    new Class[] { byte.class },
+                    new Class[] { Byte.class },
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class },
+                },
+                new Object[] { new BigDecimal("1") /* possibly non-integer */ });
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { BigDecimal.class },
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { short.class },
+                    new Class[] { Short.class },
+                    new Class[] { byte.class },
+                    new Class[] { Byte.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class },
+                },
+                new Object[] { new OverloadedNumberUtil.FloatOrByte(1f, (byte) 1) });
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { BigDecimal.class },
+                    new Class[] { short.class },
+                    new Class[] { Short.class },
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    
+                    // Two incompatibles removed, would be removed in reality:
+                    new Class[] { byte.class },
+                    new Class[] { Byte.class },
+                    
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class }
+                },
+                new Object[] { new OverloadedNumberUtil.IntegerOrShort(1, (short) 1) });
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { long.class },
+                    new Class[] { Long.class },
+                    new Class[] { BigInteger.class },
+                    new Class[] { BigDecimal.class },
+                    new Class[] { double.class },
+                    new Class[] { Double.class },
+                    new Class[] { float.class },
+                    new Class[] { Float.class },
+                    // skip byte and short  as the would be equal with int (all invalid target types)
+                    new Class[] { int.class },  // In reality, this and Integer are removed as not-applicable overloads
+                    new Class[] { Integer.class },
+                    new Class[] { Number.class },
+                    new Class[] { Serializable.class },
+                    new Class[] { Object.class }
+                },
+                new Object[] { 1L });
+        
+        // Undecidable comparisons:
+        
+        testAllCmpPermutationsEqu(
+                new Class[][] {
+                        new Class[] { Byte.class },
+                        new Class[] { Short.class },
+                        new Class[] { Integer.class },
+                        new Class[] { Long.class },
+                        new Class[] { BigInteger.class },
+                        new Class[] { Float.class },
+                    },
+                    new Object[] { 1.0 });
+        
+        testAllCmpPermutationsEqu(
+                new Class[][] {
+                        new Class[] { byte.class },
+                        new Class[] { short.class },
+                        new Class[] { int.class },
+                        new Class[] { long.class },
+                        new Class[] { float.class },
+                    },
+                    new Object[] { 1.0 });
+    }
+    
+    public void testPrimitiveIsMoreSpecific() {
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { boolean.class },
+                    new Class[] { Boolean.class }
+                },
+                new Object[] { true });
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { char.class },
+                    new Class[] { Character.class }
+                },
+                new Object[] { 'x' });
+    }
+    
+    public void testClassHierarchy() {
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { LinkedHashMap.class },
+                    new Class[] { HashMap.class },
+                    new Class[] { Map.class },
+                    new Class[] { Object.class }
+                },
+                new Object[] { new LinkedHashMap() });
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { LinkedHashMap.class },
+                    new Class[] { Cloneable.class },
+                    new Class[] { Object.class }
+                },
+                new Object[] { new LinkedHashMap() });
+    }
+
+    public void testNumericalWithNonNumerical() {
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { Comparable.class },
+                    new Class[] { Object.class },
+                },
+                new Object[] { 1 });
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { int.class },
+                    new Class[] { Integer.class },
+                    new Class[] { char.class },
+                    new Class[] { Character.class },
+                },
+                new Object[] { 1 });
+    }
+    
+    public void testUnrelated() {
+        testAllCmpPermutationsEqu(
+                new Class[][] {
+                    new Class[] { Serializable.class },
+                    new Class[] { CharSequence.class },
+                    new Class[] { Comparable.class }
+                },
+                new Object[] { "s" });
+        
+        testAllCmpPermutationsEqu(
+                new Class[][] {
+                    new Class[] { HashMap.class },
+                    new Class[] { TreeMap.class }
+                },
+                new Object[] { null });
+    }
+
+    public void testMultiParameter() {
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, String.class, String.class },
+                        
+                    new Class[] { String.class, String.class, Object.class },
+                    new Class[] { String.class, Object.class, String.class },
+                    new Class[] { Object.class, String.class, String.class },
+                    
+                    new Class[] { String.class, Object.class, Object.class },
+                    new Class[] { Object.class, String.class, Object.class },
+                    new Class[] { Object.class, Object.class, String.class },
+                    
+                    new Class[] { Object.class, Object.class, Object.class },
+                },
+                new Object[] { "a", "b", "c" });
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, String.class },
+                    new Class[] { String.class, Object.class },
+                    new Class[] { CharSequence.class, CharSequence.class },
+                    new Class[] { CharSequence.class, Object.class },
+                    new Class[] { Object.class, String.class },
+                    new Class[] { Object.class, CharSequence.class },
+                    new Class[] { Object.class, Object.class },
+                },
+                new Object[] { "a", "b" });
+        
+        /** Subclassing is more important than primitive-VS-boxed: */
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { boolean.class, boolean.class, boolean.class, String.class },
+                    new Class[] { Boolean.class, boolean.class, boolean.class, String.class },
+                    new Class[] { boolean.class, Boolean.class, Boolean.class, String.class },
+                    new Class[] { Boolean.class, boolean.class, Boolean.class, String.class },
+                    new Class[] { Boolean.class, Boolean.class, boolean.class, String.class },
+                    new Class[] { Boolean.class, Boolean.class, Boolean.class, String.class },
+                    new Class[] { Boolean.class, Boolean.class, Boolean.class, CharSequence.class },
+                    new Class[] { boolean.class, boolean.class, boolean.class, Object.class },
+                    new Class[] { Boolean.class, boolean.class, boolean.class, Object.class },
+                    new Class[] { boolean.class, Boolean.class, Boolean.class, Object.class },
+                    new Class[] { Boolean.class, boolean.class, Boolean.class, Object.class },
+                    new Class[] { Boolean.class, Boolean.class, boolean.class, Object.class },
+                    new Class[] { Boolean.class, Boolean.class, Boolean.class, Object.class },
+                },
+                new Object[] { true, false, true, "a" });
+        
+        /** Subclassing is more important than primitive-VS-boxed: */
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { int.class, int.class, int.class, String.class },
+                    new Class[] { Integer.class, int.class, int.class, String.class },
+                    new Class[] { int.class, Integer.class, Integer.class, String.class },
+                    new Class[] { Integer.class, int.class, Integer.class, String.class },
+                    new Class[] { Integer.class, Integer.class, int.class, String.class },
+                    new Class[] { Integer.class, Integer.class, Integer.class, String.class },
+                    new Class[] { Integer.class, Integer.class, Integer.class, CharSequence.class },
+                    new Class[] { int.class, int.class, int.class, Object.class },
+                    new Class[] { Integer.class, int.class, int.class, Object.class },
+                    new Class[] { int.class, Integer.class, Integer.class, Object.class },
+                    new Class[] { Integer.class, int.class, Integer.class, Object.class },
+                    new Class[] { Integer.class, Integer.class, int.class, Object.class },
+                    new Class[] { Integer.class, Integer.class, Integer.class, Object.class },
+                },
+                new Object[] { 1, 2, 3, "a" });
+    }
+
+    public void testVarargs() {
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, String[].class },
+                    new Class[] { String.class, CharSequence[].class },
+                    new Class[] { String.class, Object[].class },
+                },
+                new Object[] { "a", "b", "c" },
+                true);
+        
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, int[].class },
+                    new Class[] { String.class, Integer[].class },
+                    new Class[] { String.class, long[].class },
+                    new Class[] { String.class, Long[].class },
+                    new Class[] { String.class, double[].class },
+                    new Class[] { String.class, Double[].class },
+                    new Class[] { String.class, Serializable[].class },
+                    new Class[] { String.class, Object[].class },
+                },
+                new Object[] { "a", 1, 2, 3 },
+                true);
+        
+        // 0-long varargs list; in case of ambiguity, the varargs component type decides:
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, Object[].class },
+                    new Class[] { CharSequence.class, int[].class },
+                    new Class[] { CharSequence.class, Integer[].class },
+                    new Class[] { CharSequence.class, long[].class },
+                    new Class[] { CharSequence.class, Long[].class },
+                    new Class[] { CharSequence.class, double[].class },
+                    new Class[] { CharSequence.class, Double[].class },
+                    new Class[] { CharSequence.class, Serializable[].class },
+                    new Class[] { CharSequence.class, Object[].class },
+                    new Class[] { Object.class, int[].class },
+                },
+                new Object[] { "a" },
+                true);
+        
+        
+        // Different fixed prefix length; in the case of ambiguity, the one with higher fixed param count wins.
+        testAllCmpPermutationsInc(
+                new Class[][] {
+                    new Class[] { String.class, int.class, int.class, int[].class },
+                    new Class[] { String.class, int.class, int[].class },
+                    new Class[] { String.class, int[].class },
+                },
+                new Object[] { "a", 1, 2, 3 },
+                true);
+    }
+    
+    private void testAllCmpPermutationsInc(Class[][] sortedSignatures, Object[] args) {
+        testAllCmpPermutationsInc(sortedSignatures, args, false);
+    }
+
+    /**
+     * Compares all items with all other items in the provided descending sorted array of signatures, checking that
+     * for all valid indexes i and j, where j > i, it stands that sortedSignatures[i] > sortedSignatures[j].
+     * The comparisons are done with both operand orders, also each items is compared to itself too.
+     * 
+     * @param sortedSignatures method signatures sorted by decreasing specificity
+     */
+    private void testAllCmpPermutationsInc(Class[][] sortedSignatures, Object[] args, boolean varargs) {
+        final ArgumentTypes argTs = new ArgumentTypes(args, true);
+        for (int i = 0; i < sortedSignatures.length; i++) {
+            for (int j = 0; j < sortedSignatures.length; j++) {
+                assertEquals("sortedSignatures[" + i + "] <==> sortedSignatures [" + j + "]",
+                        NumberUtil.getSignum(
+                                Integer.valueOf(j).compareTo(i)),
+                        NumberUtil.getSignum(
+                                argTs.compareParameterListPreferability(
+                                        sortedSignatures[i], sortedSignatures[j], varargs)));
+            }
+        }
+    }
+
+    private void testAllCmpPermutationsEqu(Class[][] signatures, Object[] args) {
+        final ArgumentTypes argTs = new ArgumentTypes(args, true);
+        for (int i = 0; i < signatures.length; i++) {
+            for (int j = 0; j < signatures.length; j++) {
+                assertEquals("sortedSignatures[" + i + "] <==> sortedSignatures [" + j + "]",
+                        0,
+                        argTs.compareParameterListPreferability(signatures[i], signatures[j], false));
+            }
+        }
+    }
+    
+}
diff --git a/src/test/java/freemarker/ext/beans/RationalNumber.java b/src/test/java/freemarker/ext/beans/RationalNumber.java
new file mode 100644
index 0000000..b4aa63d
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/RationalNumber.java
@@ -0,0 +1,71 @@
+package freemarker.ext.beans;
+
+public final class RationalNumber extends Number {
+    
+    final int divident;
+    final int divisor;
+    
+    public RationalNumber(int divident, int divisor) {
+        this.divident = divident;
+        this.divisor = divisor;
+    }
+
+    @Override
+    public int intValue() {
+        return divident / divisor;
+    }
+
+    @Override
+    public long longValue() {
+        return divident / (long) divisor;
+    }
+
+    @Override
+    public float floatValue() {
+        return (float) (divident / (double) divisor);
+    }
+
+    @Override
+    public double doubleValue() {
+        return divident / (double) divisor;
+    }
+
+    public int getDivident() {
+        return divident;
+    }
+
+    public int getDivisor() {
+        return divisor;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + divident;
+        result = prime * result + divisor;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        RationalNumber other = (RationalNumber) obj;
+        if (divident != other.divident)
+            return false;
+        if (divisor != other.divisor)
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return divident + "/" + divisor;
+    }
+    
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/utility/NumberUtilTest.java b/src/test/java/freemarker/template/utility/NumberUtilTest.java
index 3759cd6..2bfb3ea 100644
--- a/src/test/java/freemarker/template/utility/NumberUtilTest.java
+++ b/src/test/java/freemarker/template/utility/NumberUtilTest.java
@@ -60,5 +60,37 @@
         assertEquals(0, NumberUtil.getSignum(BigInteger.valueOf(0)));
         assertEquals(-1, NumberUtil.getSignum(BigInteger.valueOf(-3)));
     }
+    
+    public void testIsBigDecimalInteger() {
+        BigDecimal n1 = new BigDecimal("1.125");
+        if (n1.precision() != 4 || n1.scale() != 3) {
+            throw new RuntimeException("Wrong: " +  n1);
+        }
+        BigDecimal n2 = new BigDecimal("1.125").subtract(new BigDecimal("0.005"));
+        if (n2.precision() != 4 || n2.scale() != 3) {
+            throw new RuntimeException("Wrong: " +  n2);
+        }
+        BigDecimal n3 = new BigDecimal("123");
+        BigDecimal n4 = new BigDecimal("6000");
+        BigDecimal n5 = new BigDecimal("1.12345").subtract(new BigDecimal("0.12345"));
+        if (n5.precision() != 6 || n5.scale() != 5) {
+            throw new RuntimeException("Wrong: " +  n5);
+        }
+        BigDecimal n6 = new BigDecimal("0"); 
+        BigDecimal n7 = new BigDecimal("0.001").subtract(new BigDecimal("0.001")); 
+        BigDecimal n8 = new BigDecimal("60000.5").subtract(new BigDecimal("0.5")); 
+        BigDecimal n9 = new BigDecimal("6").movePointRight(3).setScale(-3);
+        
+        BigDecimal[] ns = new BigDecimal[] {
+                n1, n2, n3, n4, n5, n6, n7, n8, n9,
+                n1.negate(), n2.negate(), n3.negate(), n4.negate(), n5.negate(), n6.negate(), n7.negate(), n8.negate(),
+                n9.negate(),
+        };
+        
+        for (BigDecimal n : ns) {
+            assertEquals(n.doubleValue() == n.longValue(), NumberUtil.isBigDecimalInteger(n));
+        }
+        
+    }
 
 }
diff --git a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
index d563d8d..8b1f31d 100644
--- a/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
+++ b/src/test/java/freemarker/test/templatesuite/TemplateTestCase.java
@@ -91,6 +91,7 @@
 import freemarker.template.TemplateNodeModel;
 import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
+import freemarker.template.utility.NullArgumentException;
 import freemarker.template.utility.NullWriter;
 import freemarker.template.utility.StringUtil;
 import freemarker.test.templatesuite.models.BooleanAndStringTemplateModel;
@@ -119,11 +120,17 @@
     private final boolean noOutput;
     
     private Configuration conf = new Configuration();
+
+    public TemplateTestCase(String name) {
+        this(name, null, null, false);
+    }
     
     public TemplateTestCase(String name, String templateName, String expectedFileName, boolean noOutput) {
         super(name);
-        this.templateName = templateName;
-        this.expectedFileName = expectedFileName;
+        
+        NullArgumentException.check("name", name);
+        this.templateName = templateName != null ? templateName : name + ".ftl";
+        this.expectedFileName = expectedFileName != null ? expectedFileName : name + ".txt";
         this.noOutput = noOutput;
     }
     
diff --git a/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java b/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
index 2d4d30e..849562d 100644
--- a/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
+++ b/src/test/java/freemarker/test/templatesuite/TemplateTestSuite.java
@@ -136,7 +136,7 @@
                     }
                 }
                 if (n.getNodeName().equals("testcase")) {
-                    TestCase tc = createTestCaseFromNode((Element) n, filter);
+                    TemplateTestCase tc = createTestCaseFromNode((Element) n, filter);
                     if (tc != null) addTest(tc);
                 }
             }
@@ -164,44 +164,32 @@
      * it must extend {@link TestCase} and have a constructor with the same parameters as of
      * {@link TemplateTestCase#TemplateTestCase(String, String, String, boolean)}.
      */
-    private TestCase createTestCaseFromNode(Element e, Pattern filter) throws Exception {
+    private TemplateTestCase createTestCaseFromNode(Element e, Pattern filter) throws Exception {
         String name = StringUtil.emptyToNull(e.getAttribute("name"));
         if (name == null) throw new Exception("Invalid XML: the \"name\" attribute is mandatory.");
         if (filter != null && !filter.matcher(name).matches()) return null;
         
         String templateName = StringUtil.emptyToNull(e.getAttribute("template"));
-        if (templateName == null) templateName = name + ".ftl";
 
         String expectedFileName = StringUtil.emptyToNull(e.getAttribute("expected"));
-        if (expectedFileName == null) expectedFileName = name + ".txt";
         
         String noOutputStr = StringUtil.emptyToNull(e.getAttribute("nooutput"));
         boolean noOutput = noOutputStr == null ? false : StringUtil.getYesNo(noOutputStr);
         
-        String classname = StringUtil.emptyToNull(e.getAttribute("class"));
-        
-        if (classname != null) {
-            Class cl = Class.forName(classname);
-            Constructor cons = cl.getConstructor(new Class[] {
-                    String.class, String.class, String.class, boolean.class});
-            return (TestCase) cons.newInstance(new Object [] {
-                    name, templateName, expectedFileName, Boolean.valueOf(noOutput)});
-        } else { 
-	        TemplateTestCase result = new TemplateTestCase(name, templateName, expectedFileName, noOutput);
-	        for (Iterator it=configParams.entrySet().iterator(); it.hasNext();) {
-	            Map.Entry entry = (Map.Entry) it.next();
-	            result.setConfigParam(entry.getKey().toString(), entry.getValue().toString());
-	        }
-	        NodeList configs = e.getElementsByTagName("config");
-	        for (int i=0; i<configs.getLength(); i++)  {
-	            NamedNodeMap atts = configs.item(i).getAttributes();
-	            for (int j=0; j<atts.getLength(); j++) {
-	                Attr att = (Attr) atts.item(j);
-	                result.setConfigParam(att.getName(), att.getValue());
-	            }
-	        }
-	        return result;
+        TemplateTestCase result = new TemplateTestCase(name, templateName, expectedFileName, noOutput);
+        for (Iterator it=configParams.entrySet().iterator(); it.hasNext();) {
+            Map.Entry entry = (Map.Entry) it.next();
+            result.setConfigParam(entry.getKey().toString(), entry.getValue().toString());
         }
+        NodeList configs = e.getElementsByTagName("config");
+        for (int i=0; i<configs.getLength(); i++)  {
+            NamedNodeMap atts = configs.item(i).getAttributes();
+            for (int j=0; j<atts.getLength(); j++) {
+                Attr att = (Attr) atts.item(j);
+                result.setConfigParam(att.getName(), att.getValue());
+            }
+        }
+        return result;
     }
     
     public static void main (String[] args) throws Exception {
diff --git a/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java b/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
index d7633b2..995828e 100644
--- a/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
+++ b/src/test/java/freemarker/test/templatesuite/models/OverloadedMethods2.java
@@ -1,9 +1,18 @@
 package freemarker.test.templatesuite.models;
 
 import java.io.File;
+import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 
+import freemarker.ext.beans.RationalNumber;
+import freemarker.ext.util.WrapperTemplateModel;
+import freemarker.template.AdapterTemplateModel;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.StringUtil;
+
 public class OverloadedMethods2 {
 
     public String mVarargs(String... a1) {
@@ -17,6 +26,10 @@
     public BigInteger bigInteger(BigDecimal n) {
         return n.toBigInteger();
     }
+
+    public RationalNumber rational(int a, int b) {
+        return new RationalNumber(a, b);
+    }
     
     public String mVarargs(File a1, String... a2) {
         return "mVarargs(File a1, String... a2)";
@@ -223,4 +236,345 @@
         return "mDecimalLoss(double a1 = " + a1 + ")";
     }
     
+    public String mNumConversionLoses1(byte i, Object o1, Object o2) {
+        return "byte " + i;
+    }
+    
+    public String mNumConversionLoses1(double i, Object o1, Object o2) {
+        return "double " + i;
+    }
+
+    public String mNumConversionLoses1(Number i, String o1, String o2) {
+        return "Number " + i + " " + i.getClass().getName();
+    }
+
+    public String mNumConversionLoses2(int i, Object o1, Object o2) {
+        return "int " + i;
+    }
+
+    public String mNumConversionLoses2(long i, Object o1, Object o2) {
+        return "long " + i;
+    }
+
+    public String mNumConversionLoses2(Number i, String o1, String o2) {
+        return "Number " + i + " " + i.getClass().getName();
+    }
+
+    public String mNumConversionLoses3(int i, Object o1, Object o2) {
+        return "int " + i;
+    }
+
+    public String mNumConversionLoses3(Serializable i, String o1, String o2) {
+        return "Serializable " + i + " " + i.getClass().getName();
+    }
+    
+    public String nIntAndLong(int i) {
+        return "nIntAndLong(int " + i + ")";
+    }
+    
+    public String nIntAndLong(long i) {
+        return "nIntAndLong(long " + i + ")";
+    }
+
+    public String nIntAndShort(int i) {
+        return "nIntAndShort(int " + i + ")";
+    }
+    
+    public String nIntAndShort(short i) {
+        return "nIntAndShort(short " + i + ")";
+    }
+
+    public String nLongAndShort(long i) {
+        return "nLongAndShort(long " + i + ")";
+    }
+    
+    public String nLongAndShort(short i) {
+        return "nLongAndShort(short " + i + ")";
+    }
+
+    public String varargs1(String s, int... xs) {
+        return "varargs1(String s = " + StringUtil.jQuote(s) + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs1(String s, double... xs) {
+        return "varargs1(String s = " + StringUtil.jQuote(s) + ", double... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs1(String s, Object... xs) {
+        return "varargs1(String s = " + StringUtil.jQuote(s) + ", Object... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs1(Object s, Object... xs) {
+        return "varargs1(Object s = " + s + ", Object... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs2(int... xs) {
+        return "varargs2(int... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs2(double... xs) {
+        return "varargs2(double... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs3(String... xs) {
+        return "varargs3(String... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs3(Comparable... xs) {
+        return "varargs3(Comparable... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs3(Object... xs) {
+        return "varargs3(Object... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs4(Integer... xs) {
+        return "varargs4(Integer... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs4(int... xs) {
+        return "varargs4(int... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs5(int... xs) {
+        return "varargs5(int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs5(int a1, int... xs) {
+        return "varargs5(int a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs5(int a1, int a2, int... xs) {
+        return "varargs5(int a1 = " + a1 + ", int a2 = " + a2 + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs5(int a1, int a2, int a3, int... xs) {
+        return "varargs5(int a1 = " + a1 + ", int a2 = " + a2 + ", int a3 = " + a3
+                + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+
+    public String varargs6(String a1, int... xs) {
+        return "varargs6(String a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs6(Object a1, int a2, int... xs) {
+        return "varargs6(Object a1 = " + a1 + ", int a2 = " + a2 + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs7(int... xs) {
+        return "varargs7(int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    public String varargs7(short a1, int... xs) {
+        return "varargs7(short a1 = " + a1 + ", int... xs = [" + arrayToString(xs) + "])";
+    }
+    
+    private String arrayToString(int[] xs) {
+        StringBuilder sb = new StringBuilder();
+        for (int x : xs) {
+            if (sb.length() != 0) sb.append(", ");
+            sb.append(x);
+        }
+        return sb.toString();
+    }
+
+    private String arrayToString(double[] xs) {
+        StringBuilder sb = new StringBuilder();
+        for (double x : xs) {
+            if (sb.length() != 0) sb.append(", ");
+            sb.append(x);
+        }
+        return sb.toString();
+    }
+
+    private String arrayToString(Object[] xs) {
+        StringBuilder sb = new StringBuilder();
+        for (Object x : xs) {
+            if (sb.length() != 0) sb.append(", ");
+            sb.append(x);
+        }
+        return sb.toString();
+    }
+    
+    public String mNullAmbiguous(String s) {
+        return "mNullAmbiguous(String s = " + s + ")";
+    }
+
+    public String mNullAmbiguous(int i) {
+        return "mNullAmbiguous(int i = " + i + ")";
+    }
+
+    public String mNullAmbiguous(File f) {
+        return "mNullAmbiguous(File f = " + f + ")";
+    }
+    
+    public String mNullAmbiguous2(String s) {
+        return "mNullNonAmbiguous(String s = " + s + ")";
+    }
+
+    public String mNullAmbiguous2(File f) {
+        return "mNullAmbiguous(File f = " + f + ")";
+    }
+
+    public String mNullAmbiguous2(Object o) {
+        return "mNullAmbiguous(Object o = " + o + ")";
+    }
+
+    public String mNullNonAmbiguous(String s) {
+        return "mNullNonAmbiguous(String s = " + s + ")";
+    }
+
+    public String mNullNonAmbiguous(int i) {
+        return "mNullNonAmbiguous(int i = " + i + ")";
+    }
+    
+    public String mVarargsIgnoredTail(int i, double... ds) {
+        return "mVarargsIgnoredTail(int i = " + i + ", double... ds = [" + arrayToString(ds) + "])"; 
+    }
+    
+    public String mVarargsIgnoredTail(int... is) {
+        return "mVarargsIgnoredTail(int... is = [" + arrayToString(is) + "])"; 
+    }
+    
+    public String mLowRankWins(int x, int y, Object o) {
+        return "mLowRankWins(int x = " + x + ", int y = " + y + ", Object o = " + o + ")";
+    }
+
+    public String mLowRankWins(Integer x, Integer y, String s) {
+        return "mLowRankWins(Integer x = " + x + ", Integer y = " + y + ", String s = " + s + ")";
+    }
+    
+    public String mRareWrappings(File f, double d1, Double d2, double d3, boolean b) {
+        return "mRareWrappings(File f = " + f + ", double d1 = " + d1 + ", Double d2 = " + d2
+                + ", double d3 = " + d3 + ", b = " + b + ")";
+    }
+
+    public String mRareWrappings(Object o, double d1, Double d2, Double d3, boolean b) {
+        return "mRareWrappings(Object o = " + o + ", double d1 = " + d1 + ", Double d2 = " + d2
+                + ", double d3 = " + d3 + ", b = " + b + ")";
+    }
+
+    public String mRareWrappings(String s, double d1, Double d2, Double d3, boolean b) {
+        return "mRareWrappings(String s = " + s + ", double d1 = " + d1 + ", Double d2 = " + d2
+                + ", double d3 = " + d3 + ", b = " + b + ")";
+    }
+
+    public String mRareWrappings2(String s) {
+        return "mRareWrappings2(String s = " + s + ")";
+    }
+    
+    public String mRareWrappings2(byte b) {
+        return "mRareWrappings2(byte b = " + b + ")";
+    }
+    
+    public File getFile() {
+        return new File("file");
+    }
+
+    public TemplateNumberModel getAdaptedNumber() {
+        return new MyAdapterNumberModel();
+    }
+
+    public TemplateNumberModel getWrapperNumber() {
+        return new MyWrapperNumberModel();
+    }
+
+    public TemplateBooleanModel getStringAdaptedToBoolean() {
+        return new MyStringAdaptedToBooleanModel();
+    }
+    
+    public TemplateBooleanModel getStringAdaptedToBoolean2() {
+        return new MyStringAdaptedToBooleanModel2();
+    }
+    
+    public TemplateBooleanModel getStringWrappedAsBoolean() {
+        return new MyStringWrapperAsBooleanModel();
+    }
+    
+    public TemplateBooleanModel getBooleanWrappedAsAnotherBoolean() {
+        return new MyBooleanWrapperAsAnotherBooleanModel(); 
+    }
+    
+    private static class MyAdapterNumberModel implements TemplateNumberModel, AdapterTemplateModel {
+
+        public Object getAdaptedObject(Class hint) {
+            if (hint == double.class) {
+                return Double.valueOf(123.0001);
+            } else if (hint == Double.class) {
+                return Double.valueOf(123.0002);
+            } else {
+                return Long.valueOf(124L);
+            }
+        }
+
+        public Number getAsNumber() throws TemplateModelException {
+            return Integer.valueOf(122);
+        }
+        
+    }
+    
+    private static class MyWrapperNumberModel implements TemplateNumberModel, WrapperTemplateModel {
+
+        public Number getAsNumber() throws TemplateModelException {
+            return Integer.valueOf(122);
+        }
+
+        public Object getWrappedObject() {
+            return Double.valueOf(123.0001);
+        }
+        
+    }
+    
+    private static class MyStringWrapperAsBooleanModel implements TemplateBooleanModel, WrapperTemplateModel {
+
+        public Object getWrappedObject() {
+            return "yes";
+        }
+
+        public boolean getAsBoolean() throws TemplateModelException {
+            return true;
+        }
+        
+    }
+
+    private static class MyBooleanWrapperAsAnotherBooleanModel implements TemplateBooleanModel, WrapperTemplateModel {
+
+        public Object getWrappedObject() {
+            return Boolean.TRUE;
+        }
+
+        public boolean getAsBoolean() throws TemplateModelException {
+            return false;
+        }
+        
+    }
+    
+    private static class MyStringAdaptedToBooleanModel implements TemplateBooleanModel, AdapterTemplateModel {
+
+        public Object getAdaptedObject(Class hint) {
+            if (hint != Boolean.class && hint != boolean.class) {
+                return "yes";
+            } else {
+                return Boolean.TRUE;
+            }
+        }
+
+        public boolean getAsBoolean() throws TemplateModelException {
+            return false;
+        }
+        
+    }
+
+    private static class MyStringAdaptedToBooleanModel2 implements TemplateBooleanModel, AdapterTemplateModel {
+
+        public Object getAdaptedObject(Class hint) {
+            return "yes";
+        }
+
+        public boolean getAsBoolean() throws TemplateModelException {
+            return true;
+        }
+        
+    }
+    
 }
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
index 35b6f1e..3200723 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-desc-ici-2.3.20.ftl
@@ -3,5 +3,18 @@
 <@assertFails message="no compatible overloaded">${obj.mIntPrimVSBoxed(123?long)}</@>
 <@assertEquals actual=obj.mIntPrimVSBoxed(123?short) expected="mIntPrimVSBoxed(int a1 = 123)" />
 <@assertEquals actual=obj.mIntPrimVSBoxed(123) expected="mIntPrimVSBoxed(int a1 = 123)" />
+<@assertEquals actual=obj.varargs4(1, 2, 3) expected='varargs4(int... xs = [1, 2, 3])' />
+
+<@assertFails message="multiple compatible overloaded">${obj.mVarargsIgnoredTail(1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.mVarargsIgnoredTail(1, 2, 3.5)}</@>
+
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(int x = 1, int y = 2, Object o = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+               expected='mRareWrappings(File f = file, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 124.0, b = true)' />
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)}</@>
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)}</@>
 
 <#include 'overloaded-methods-2-ici-2.3.20.ftl'>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
index 3d3c941..2a1c34e 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.20.ftl
@@ -74,4 +74,80 @@
 <@assertEquals actual=obj.mNumBoxedFallbackToNumber('x') expected="mNumBoxedFallbackToNumber(Object a1 = x)" />
 
 <@assertEquals actual=obj.mDecimalLoss(1.5) expected="mDecimalLoss(int a1 = 1)" /><#-- Yes, buggy... -->
-<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
\ No newline at end of file
+<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
+
+<#-- BigDecimal conversions chose the smallest target type before IcI 2.3.31, increasing the risk of overflows: -->
+<@assertEquals actual=obj.nIntAndLong(1) expected="nIntAndLong(int 1)" />
+<@assertEquals actual=obj.nIntAndLong(1?long) expected="nIntAndLong(long 1)" />
+<@assertEquals actual=obj.nIntAndShort(1) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?short) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?int) expected="nIntAndShort(int 1)" />
+<@assertEquals actual=obj.nLongAndShort(1) expected="nLongAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?short) expected="nLongAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?long) expected="nLongAndShort(long 1)" />
+
+<#-- Usual wrong choice on null: -->
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3.5) expected='varargs1(Object s = null, Object... xs = [1, 2, 3.5])' />
+
+<#-- Some bugs that cause loosing of decimals will occur here... -->
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1, 2, 3.5)}</@>
+<@assertEquals actual=obj.varargs1('s', 1, 2, 'c') expected='varargs1(String s = "s", Object... xs = [1, 2, c])' />
+<@assertEquals actual=obj.varargs1('s', 1, 'b', 3) expected='varargs1(String s = "s", Object... xs = [1, b, 3])' />
+<@assertEquals actual=obj.varargs1('s', 'a', 2, 3) expected='varargs1(String s = "s", Object... xs = [a, 2, 3])' />
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1.1, 2.1, 3.1)}</@>
+<@assertEquals actual=obj.varargs1('s', 'a', 'b', 'c') expected='varargs1(String s = "s", Object... xs = [a, b, c])' />
+<@assertFails message="multiple compatible overloaded"><@assertEquals actual=obj.varargs1('s', 1?double, 2?byte, 3?byte) expected='varargs1(String s = "s", int... xs = [1, 2, 3])' /></@>
+<@assertEquals actual=obj.varargs1(0, 1, 2, 3) expected='varargs1(Object s = 0, Object... xs = [1, 2, 3])' />
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s', 1?double, 2?double, 3?double)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs1('s')}</@>
+
+<@assertEquals actual=obj.varargs2(1, 2.5, 3) expected='varargs2(int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs2(1, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?int, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2?double, 3) expected='varargs2(double... xs = [1.0, 2.0, 3.0])' />
+
+<@assertEquals actual=obj.varargs3(1, 2, 3) expected='varargs3(Comparable... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 'c') expected='varargs3(String... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs3(1, 'b', 'c') expected='varargs3(Comparable... xs = [1, b, c])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 3) expected='varargs3(Comparable... xs = [a, b, 3])' />
+<@assertEquals actual=obj.varargs3('a', [], 3) expected='varargs3(Object... xs = [a, [], 3])' />
+<@assertEquals actual=obj.varargs3(null, 'b', null) expected='varargs3(Object... xs = [null, b, null])' />
+<@assertEquals actual=obj.varargs3(null, 2, null) expected='varargs3(Object... xs = [null, 2, null])' />
+<@assertEquals actual=obj.varargs3(null, [], null) expected='varargs3(Object... xs = [null, [], null])' />
+<@assertEquals actual=obj.varargs3(null, null, null) expected='varargs3(Object... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs3() expected='varargs3(String... xs = [])' />
+
+<@assertFails message="no compatible overloaded">${obj.varargs4(null, null, null)}</@>
+
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3, 4, 5)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3, 4)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2, 3)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1, 2)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.varargs5(1)}</@>
+<@assertEquals actual=obj.varargs5() expected='varargs5(int... xs = [])' />
+
+<@assertEquals actual=obj.varargs6('s', 2) expected='varargs6(String a1 = s, int... xs = [2])' />
+<@assertEquals actual=obj.varargs6('s') expected='varargs6(String a1 = s, int... xs = [])' />
+<@assertEquals actual=obj.varargs6(1, 2) expected='varargs6(Object a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertFails message="no compatible overloaded">${obj.varargs6(1)}</@>
+
+<@assertEquals actual=obj.varargs7(1?int, 2?int) expected='varargs7(int... xs = [1, 2])' />
+<@assertEquals actual=obj.varargs7(1?short, 2?int) expected='varargs7(short a1 = 1, int... xs = [2])' />
+
+<@assertEquals actual=obj.mNullAmbiguous('a') expected='mNullAmbiguous(String s = a)' />
+<@assertEquals actual=obj.mNullAmbiguous(123) expected='mNullAmbiguous(int i = 123)' />
+<@assertEquals actual=obj.mNullAmbiguous(1.9) expected='mNullAmbiguous(int i = 1)' />
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1?double)}</@>
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1.9?double)}</@>
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(null)}</@>
+
+<@assertEquals actual=obj.mNullAmbiguous2(null) expected='mNullAmbiguous(Object o = null)' />
+
+<@assertFails message="no compatible overloaded">${obj.mNullNonAmbiguous(null)}</@>
+
+<@assertEquals actual=obj.mRareWrappings(obj.stringAdaptedToBoolean2, obj.wrapperNumber, obj.wrapperNumber, obj.wrapperNumber, obj.stringAdaptedToBoolean2)
+               expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0001, double d3 = 123.0001, b = true)' />
+               
+<@assertFails message="no compatible overloaded">${obj.mRareWrappings2(obj.adaptedNumber)}</@>
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
index 6e18758..d31fa67 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-ici-2.3.21.ftl
@@ -6,8 +6,8 @@
 <@assertEquals actual=obj.mNull1(123) expected="mNull1(int a1 = 123)" />
 <@assertEquals actual=obj.mNull2(null) expected="mNull2(String a1 = null)" />
 <@assertEquals actual=obj.mVarargs('a', null) expected="mVarargs(String... a1 = anull)" />
-<@assertFails message="multiple compatible overloaded">${obj.mVarargs(null, 'a')}</@>
-<@assertFails message="multiple compatible overloaded">${obj.mSpecificity('a', 'b')}</@>
+<@assertEquals actual=obj.mVarargs(null, 'a') expected="mVarargs(File a1, String... a2)" />
+<@assertEquals actual=obj.mSpecificity('a', 'b') expected="mSpecificity(String a1, Object a2)" />
 
 <@assertEquals actual=obj.mChar('a') expected='mChar(char a1 = a)' />
 <@assertEquals actual=obj.mBoolean(true) expected="mBoolean(boolean a1 = true)" />
@@ -16,25 +16,25 @@
 <@assertEquals actual=obj.mIntNonOverloaded(123?long) expected=123 />
 <@assertEquals actual=obj.mIntNonOverloaded(123) expected=123 />
 <@assertEquals actual=obj.mIntNonOverloaded(123.5) expected=123 />
-<@assertEquals actual=obj.mIntNonOverloaded(2147483648) expected=-2147483648 /> <#-- FIXME overflow -->
-<@assertFails message="no compatible overloaded"><@assertEquals actual=obj.mNumBoxedVSBoxed(123.5) expected=123 /></@><#-- FIXME -->
-<@assertFails message="no compatible overloaded"><@assertEquals actual=obj.mNumBoxedVSBoxed(123?int) expected=123 /></@><#-- FIXME -->
+<@assertEquals actual=obj.mIntNonOverloaded(2147483648) expected=-2147483648 /> <#-- overflow -->
+<@assertEquals actual=obj.mNumBoxedVSBoxed(123.5) expected='mNumBoxedVSBoxed(Long a1 = 123)' />
+<@assertEquals actual=obj.mNumBoxedVSBoxed(123?int) expected='mNumBoxedVSBoxed(Long a1 = 123)' />
 <@assertEquals actual=obj.mNumBoxedVSBoxed(123?long) expected="mNumBoxedVSBoxed(Long a1 = 123)" />
 <@assertEquals actual=obj.mNumBoxedVSBoxed(123?short) expected="mNumBoxedVSBoxed(Short a1 = 123)" />
 <@assertEquals 
-    actual=obj.mNumUnambigous(2147483648) expected="mNumUnambigous(Integer a1 = -2147483648)" /> <#-- FIXME overflow -->
+    actual=obj.mNumUnambigous(2147483648) expected="mNumUnambigous(Integer a1 = -2147483648)" /> <#-- overflow -->
 
 <@assertEquals actual=obj.mIntPrimVSBoxed(123?int) expected="mIntPrimVSBoxed(int a1 = 123)" />
 <@assertEquals actual=obj.mIntPrimVSBoxed(123?short) expected="mIntPrimVSBoxed(int a1 = 123)" />
 <@assertEquals actual=obj.mIntPrimVSBoxed(123) expected="mIntPrimVSBoxed(int a1 = 123)" />
-<#-- This doesn't fail as the hint will be Integer, and thus unwrapping will truncate the Long to that: -->
+<#-- This doesn't fail as 123L can be converted to int without loss: -->
 <@assertEquals actual=obj.mIntPrimVSBoxed(123?long) expected="mIntPrimVSBoxed(int a1 = 123)" />
 
 <@assertEquals actual=obj.mNumPrimVSPrim(123?short) expected="mNumPrimVSPrim(short a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimVSPrim(123?int) expected="mNumPrimVSPrim(long a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimVSPrim(123?long) expected="mNumPrimVSPrim(long a1 = 123)" />
-<@assertFails message="no compatible overloaded">${obj.mNumPrimVSPrim(123?double)}</@><#-- FIXME? -->
-<@assertEquals actual=obj.mNumPrimVSPrim(123456) expected="mNumPrimVSPrim(short a1 = -7616)" /><#-- FIXME overflow, had to choose long -->
+<@assertEquals actual=obj.mNumPrimVSPrim(123?double) expected="mNumPrimVSPrim(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimVSPrim(123456) expected="mNumPrimVSPrim(long a1 = 123456)" />
 
 <@assertEquals actual=obj.mNumPrimAll(123?byte) expected="mNumPrimAll(byte a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimAll(123?short) expected="mNumPrimAll(short a1 = 123)" />
@@ -42,7 +42,7 @@
 <@assertEquals actual=obj.mNumPrimAll(123?long) expected="mNumPrimAll(long a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimAll(123?float) expected="mNumPrimAll(float a1 = 123.0)" />
 <@assertEquals actual=obj.mNumPrimAll(123?double) expected="mNumPrimAll(double a1 = 123.0)" />
-<@assertFails message="multiple compatible overloaded">${obj.mNumPrimAll(123)}</@><#-- FIXME -->
+<@assertEquals actual=obj.mNumPrimAll(123) expected="mNumPrimAll(BigDecimal a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimAll(obj.bigInteger(123)) expected="mNumPrimAll(BigInteger a1 = 123)" />
 
 <@assertEquals actual=obj.mNumBoxedAll(123?byte) expected="mNumBoxedAll(Byte a1 = 123)" />
@@ -61,36 +61,144 @@
 <@assertEquals actual=obj.mNumPrimAll2nd(123?float) expected="mNumPrimAll2nd(double a1 = 123.0)" />
 <@assertEquals actual=obj.mNumPrimAll2nd(123?double) expected="mNumPrimAll2nd(double a1 = 123.0)" />
 
-<#-- FIXME
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?byte) expected="mNumBoxedAll2nd(Short a1 = 123)" />
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?short) expected="mNumBoxedAll2nd(Short a1 = 123)" />
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?int) expected="mNumBoxedAll2nd(Long a1 = 123)" />
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?long) expected="mNumBoxedAll2nd(Long a1 = 123)" />
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?float) expected="mNumBoxedAll2nd(Double a1 = 123.0)" />
 <@assertEquals actual=obj.mNumBoxedAll2nd(123?double) expected="mNumBoxedAll2nd(Double a1 = 123.0)" />
- -->
  
 <@assertEquals actual=obj.mNumPrimFallbackToNumber(123?int) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
 <@assertEquals actual=obj.mNumPrimFallbackToNumber(123?long) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?double) expected="mNumPrimFallbackToNumber(Number a1 = 123.0)" />
-<#-- FIXME
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(123) expected="mNumPrimFallbackToNumber(Number a1 = 123)" />
--->
-<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(123)) expected="mNumPrimFallbackToNumber(Number a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123?double) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123.5?double) expected="mNumPrimFallbackToNumber(Number a1 = 123.5)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(123) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(123)) expected="mNumPrimFallbackToNumber(long a1 = 123)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.bigInteger(9223372036854775808))
+    expected="mNumPrimFallbackToNumber(Number a1 = 9223372036854775808)" />
+<@assertEquals actual=obj.mNumPrimFallbackToNumber(obj.rational(246, 2)) expected="mNumPrimFallbackToNumber(Number a1 = 246/2)" />
 <@assertEquals actual=obj.mNumPrimFallbackToNumber('x') expected="mNumPrimFallbackToNumber(Object a1 = x)" />
 
-<#-- FIXME
 <@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?int) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
--->
 <@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?long) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
-<#-- FIXME
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?double) expected="mNumBoxedFallbackToNumber(Number a1 = 123.0)" />
--->
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123) expected="mNumBoxedFallbackToNumber(Number a1 = 123)" />
-<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(123)) expected="mNumBoxedFallbackToNumber(Number a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123?double) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123.5?double) expected="mNumBoxedFallbackToNumber(Number a1 = 123.5)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(123) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(123)) expected="mNumBoxedFallbackToNumber(Long a1 = 123)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.bigInteger(9223372036854775808))
+    expected="mNumBoxedFallbackToNumber(Number a1 = 9223372036854775808)" />
+<@assertEquals actual=obj.mNumBoxedFallbackToNumber(obj.rational(246, 2)) expected="mNumBoxedFallbackToNumber(Number a1 = 246/2)" />
 <@assertEquals actual=obj.mNumBoxedFallbackToNumber('x') expected="mNumBoxedFallbackToNumber(Object a1 = x)" />
 
-<#-- FIXME
 <@assertEquals actual=obj.mDecimalLoss(1.5) expected="mDecimalLoss(double a1 = 1.5)" />
--->
-<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
\ No newline at end of file
+<@assertEquals actual=obj.mDecimalLoss(1.5?double) expected="mDecimalLoss(double a1 = 1.5)" />
+
+<@assertEquals actual=obj.mNumConversionLoses1(1?double, '', '') expected="Number 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses1(1?short, '', '') expected="Number 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses1(1?long, '', '') expected="Number 1 java.lang.Long" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?double, '', '') expected="Number 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?short, '', '') expected="Number 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses2(1?long, '', '') expected="Number 1 java.lang.Long" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?double, '', '') expected="Serializable 1.0 java.lang.Double" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?int, '', '') expected="Serializable 1 java.lang.Integer" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?short, '', '') expected="Serializable 1 java.lang.Short" />
+<@assertEquals actual=obj.mNumConversionLoses3(1?long, '', '') expected="Serializable 1 java.lang.Long" />
+
+<#-- BigDecimal-to-int is preferred over to-long for BC and user expectations: -->
+<@assertEquals actual=obj.nIntAndLong(1) expected="nIntAndLong(int 1)" />
+<@assertEquals actual=obj.nIntAndLong(1?long) expected="nIntAndLong(long 1)" />
+<#-- BigDecimal-to-short is, however unfavored due to the higher chance of overflow: -->
+<@assertEquals actual=obj.nIntAndShort(1) expected="nIntAndShort(int 1)" />
+<@assertEquals actual=obj.nIntAndShort(1?short) expected="nIntAndShort(short 1)" />
+<@assertEquals actual=obj.nLongAndShort(1) expected="nLongAndShort(long 1)" />
+<@assertEquals actual=obj.nLongAndShort(1?short) expected="nLongAndShort(short 1)" />
+
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3.5) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.5])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2.5, 3) expected='varargs1(String s = null, double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1.5, 2, 3) expected='varargs1(String s = null, double... xs = [1.5, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 'c') expected='varargs1(String s = null, Object... xs = [1, 2, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 'b', 3) expected='varargs1(String s = null, Object... xs = [1, b, 3])' />
+<@assertEquals actual=obj.varargs1(null, 'a', 2, 3) expected='varargs1(String s = null, Object... xs = [a, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s', 1, 2, 3) expected='varargs1(String s = "s", int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s', 1.1, 2.1, 3.1) expected='varargs1(String s = "s", double... xs = [1.1, 2.1, 3.1])' />
+<@assertEquals actual=obj.varargs1('s', 'a', 'b', 'c') expected='varargs1(String s = "s", Object... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(null, 1.1, 2.1, 3.1) expected='varargs1(String s = null, double... xs = [1.1, 2.1, 3.1])' />
+<@assertEquals actual=obj.varargs1(null, 'a', 'b', 'c') expected='varargs1(String s = null, Object... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2, 3?double) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2?double, 3?double) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1, 2?float, 3?float) expected='varargs1(String s = null, double... xs = [1.0, 2.0, 3.0])' />
+<@assertEquals actual=obj.varargs1(null, 1?double, 2?byte, 3?byte) expected='varargs1(String s = null, int... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1(0, 1, 2, 3) expected='varargs1(Object s = 0, Object... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs1('s') expected='varargs1(String s = "s", int... xs = [])' />
+
+<@assertEquals actual=obj.varargs2(1, 2.5, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?int, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2.5?double, 3) expected='varargs2(double... xs = [1.0, 2.5, 3.0])' />
+<@assertEquals actual=obj.varargs2(1?long, 2?double, 3) expected='varargs2(int... xs = [1, 2, 3])' />
+
+<@assertEquals actual=obj.varargs3(1, 2, 3) expected='varargs3(Comparable... xs = [1, 2, 3])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 'c') expected='varargs3(String... xs = [a, b, c])' />
+<@assertEquals actual=obj.varargs3(1, 'b', 'c') expected='varargs3(Comparable... xs = [1, b, c])' />
+<@assertEquals actual=obj.varargs3('a', 'b', 3) expected='varargs3(Comparable... xs = [a, b, 3])' />
+<@assertEquals actual=obj.varargs3('a', [], 3) expected='varargs3(Object... xs = [a, [], 3])' />
+<@assertEquals actual=obj.varargs3(null, 'b', null) expected='varargs3(String... xs = [null, b, null])' />
+<@assertEquals actual=obj.varargs3(null, 2, null) expected='varargs3(Comparable... xs = [null, 2, null])' />
+<@assertEquals actual=obj.varargs3(null, [], null) expected='varargs3(Object... xs = [null, [], null])' />
+<@assertEquals actual=obj.varargs3(null, null, null) expected='varargs3(String... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs3() expected='varargs3(String... xs = [])' />
+
+<@assertEquals actual=obj.varargs4(null) expected='varargs4(Integer... xs = [null])' />
+<@assertEquals actual=obj.varargs4(null, null, null) expected='varargs4(Integer... xs = [null, null, null])' />
+<@assertEquals actual=obj.varargs4(1, null, 2) expected='varargs4(Integer... xs = [1, null, 2])' />
+<@assertEquals actual=obj.varargs4(1) expected='varargs4(int... xs = [1])' />
+<@assertEquals actual=obj.varargs4(1, 2, 3) expected='varargs4(int... xs = [1, 2, 3])' />
+
+<@assertEquals actual=obj.varargs5(1, 2, 3, 4, 5) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [4, 5])' />
+<@assertEquals actual=obj.varargs5(1, 2, 3, 4) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [4])' />
+<@assertEquals actual=obj.varargs5(1, 2, 3) expected='varargs5(int a1 = 1, int a2 = 2, int a3 = 3, int... xs = [])' />
+<@assertEquals actual=obj.varargs5(1, 2) expected='varargs5(int a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertEquals actual=obj.varargs5(1) expected='varargs5(int a1 = 1, int... xs = [])' />
+<@assertEquals actual=obj.varargs5() expected='varargs5(int... xs = [])' />
+
+<@assertEquals actual=obj.varargs6('s', 2) expected='varargs6(String a1 = s, int... xs = [2])' />
+<@assertEquals actual=obj.varargs6('s') expected='varargs6(String a1 = s, int... xs = [])' />
+<@assertEquals actual=obj.varargs6(1, 2) expected='varargs6(Object a1 = 1, int a2 = 2, int... xs = [])' />
+<@assertFails message="no compatible overloaded">${obj.varargs6(1)}</@>
+
+<@assertEquals actual=obj.varargs7(1?int, 2?int) expected='varargs7(int... xs = [1, 2])' />
+<@assertEquals actual=obj.varargs7(1?short, 2?int) expected='varargs7(short a1 = 1, int... xs = [2])' />
+
+<#-- Tests that a pre-2.3.21 bug is fixed now: -->
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3.5) expected='mVarargsIgnoredTail(int i = 1, double... ds = [2.0, 3.5])' />
+
+<@assertEquals actual=obj.mNullAmbiguous('a') expected='mNullAmbiguous(String s = a)' />
+<@assertEquals actual=obj.mNullAmbiguous(123) expected='mNullAmbiguous(int i = 123)' />
+<@assertEquals actual=obj.mNullAmbiguous(1.9) expected='mNullAmbiguous(int i = 1)' />
+<@assertEquals actual=obj.mNullAmbiguous(1?double) expected='mNullAmbiguous(int i = 1)' />
+<@assertFails message="no compatible overloaded">${obj.mNullAmbiguous(1.9?double)}</@>
+<@assertFails message="multiple compatible overloaded">${obj.mNullAmbiguous(null)}</@>
+
+<@assertFails message="multiple compatible overloaded">${obj.mNullAmbiguous2(null)}</@>
+
+<@assertEquals actual=obj.mNullNonAmbiguous(null) expected='mNullNonAmbiguous(String s = null)' />
+
+<#-- The primitive int-s will win twice, but then String wins over Object, which is stronger: -->
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(Integer x = 1, Integer y = 2, String s = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+               expected='mRareWrappings(File f = file, double d1 = 123.0002, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)
+               expected='mRareWrappings(String s = yes, double d1 = 123.0002, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringAdaptedToBoolean2, obj.wrapperNumber, obj.wrapperNumber, obj.wrapperNumber, obj.stringAdaptedToBoolean2)
+               expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0001, double d3 = 123.0001, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)
+               expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)
+               expected='mRareWrappings(Object o = 124, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)
+               expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+               
+<@assertEquals actual=obj.mRareWrappings2(obj.adaptedNumber) expected='mRareWrappings2(byte b = 124)' />
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
index 5644eb1..f91d7d8 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/overloaded-methods-2-inc-ici-2.3.20.ftl
@@ -3,5 +3,22 @@
 <@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123?long)}</@>
 <@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123?short)}</@>
 <@assertFails message="multiple compatible overload">${obj.mIntPrimVSBoxed(123)}</@>
+<@assertFails message="multiple compatible overload">${obj.varargs4(1, 2, 3)}</@>
+
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+<@assertEquals actual=obj.mVarargsIgnoredTail(1, 2, 3.5) expected='mVarargsIgnoredTail(int... is = [1, 2, 3])' />
+
+<@assertEquals actual=obj.mLowRankWins(1, 2, 'a') expected='mLowRankWins(Integer x = 1, Integer y = 2, String s = a)' />
+
+<@assertEquals actual=obj.mRareWrappings(obj.file, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringWrappedAsBoolean)
+               expected='mRareWrappings(File f = file, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 123.0002, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.stringWrappedAsBoolean, obj.adaptedNumber, obj.adaptedNumber, obj.adaptedNumber, obj.stringAdaptedToBoolean)
+               expected='mRareWrappings(String s = yes, double d1 = 123.0001, Double d2 = 123.0002, double d3 = 123.0002, b = false)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, obj.booleanWrappedAsAnotherBoolean)
+               expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = false)' />
+<@assertEquals actual=obj.mRareWrappings(obj.adaptedNumber, 0, 0, 0, !obj.booleanWrappedAsAnotherBoolean)
+               expected='mRareWrappings(Object o = 124, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
+<@assertEquals actual=obj.mRareWrappings(obj.booleanWrappedAsAnotherBoolean, 0, 0, 0, !obj.stringAdaptedToBoolean)
+               expected='mRareWrappings(Object o = true, double d1 = 0.0, Double d2 = 0.0, double d3 = 0.0, b = true)' />
 
 <#include 'overloaded-methods-2-ici-2.3.20.ftl'>