You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by em...@apache.org on 2023/07/14 17:11:47 UTC

[groovy] branch master updated: GROOVY-5051, GROOVY-10568: add test case

This is an automated email from the ASF dual-hosted git repository.

emilles pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new e16a44ca98 GROOVY-5051, GROOVY-10568: add test case
e16a44ca98 is described below

commit e16a44ca98cdcecfcbac5aeffe55b4800fa14139
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Fri Jul 14 11:44:32 2023 -0500

    GROOVY-5051, GROOVY-10568: add test case
---
 src/main/java/groovy/lang/MetaClassImpl.java       |   2 +-
 .../codehaus/groovy/runtime/CurriedClosure.java    | 128 +++++++++++----------
 .../org/codehaus/groovy/runtime/InvokerHelper.java |   5 +-
 .../org/codehaus/groovy/runtime/MethodClosure.java |  30 +++--
 .../groovy/runtime/ScriptBytecodeAdapter.java      |   8 +-
 src/test/groovy/ClosureCurryTest.groovy            |  48 ++++++--
 6 files changed, 133 insertions(+), 88 deletions(-)

diff --git a/src/main/java/groovy/lang/MetaClassImpl.java b/src/main/java/groovy/lang/MetaClassImpl.java
index f06923693f..b6a5257bff 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -2799,7 +2799,7 @@ public class MetaClassImpl implements MetaClass, MutableMetaClass {
         checkInitalised();
 
         //----------------------------------------------------------------------
-        // Unwrap wrapped values fo now - the new MOP will handle them properly
+        // Unwrap wrapped values for now - the new MOP will handle them properly
         //----------------------------------------------------------------------
         if (newValue instanceof Wrapper) newValue = ((Wrapper) newValue).unwrap();
 
diff --git a/src/main/java/org/codehaus/groovy/runtime/CurriedClosure.java b/src/main/java/org/codehaus/groovy/runtime/CurriedClosure.java
index f770dc65fb..eea7321790 100644
--- a/src/main/java/org/codehaus/groovy/runtime/CurriedClosure.java
+++ b/src/main/java/org/codehaus/groovy/runtime/CurriedClosure.java
@@ -20,10 +20,13 @@ package org.codehaus.groovy.runtime;
 
 import groovy.lang.Closure;
 
+import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last;
+
 /**
- * A wrapper for Closure to support currying.
- * Normally used only internally through the <code>curry()</code>, <code>rcurry()</code> or
+ * A wrapper for Closure to support currying. Normally used only internally
+ * through the <code>curry()</code>, <code>rcurry()</code> or
  * <code>ncurry()</code> methods on <code>Closure</code>.
+ * <p>
  * Typical usages:
  * <pre class="groovyTestCase">
  * // normal usage
@@ -47,118 +50,127 @@ import groovy.lang.Closure;
 public final class CurriedClosure<V> extends Closure<V> {
 
     private static final long serialVersionUID = 2077643745780234126L;
-    private final Object[] curriedParams;
+    private final Object[] curriedArguments;
     private final int minParamsExpected;
     private int index;
-    private Class varargType = null;
+    /** the last parameter type, if it's an array */
+    private Class<?> varargType;
 
     /**
-     * Creates the curried closure.
-     *
      * @param index the position where the parameters should be injected (-ve for lazy)
      * @param uncurriedClosure the closure to be called after the curried parameters are injected
      * @param arguments the supplied parameters
      */
-    public CurriedClosure(int index, Closure<V> uncurriedClosure, Object... arguments) {
+    public CurriedClosure(final int index, final Closure<V> uncurriedClosure, final Object... arguments) {
         super(uncurriedClosure.clone());
-        curriedParams = arguments;
+
         this.index = index;
-        final int origMaxLen = uncurriedClosure.getMaximumNumberOfParameters();
-        maximumNumberOfParameters = origMaxLen - arguments.length;
-        Class[] classes = uncurriedClosure.getParameterTypes();
-        Class lastType = classes.length == 0 ? null : classes[classes.length-1];
-        if (lastType != null && lastType.isArray()) {
-            varargType = lastType;
+        this.curriedArguments = arguments;
+        int maxLen = uncurriedClosure.getMaximumNumberOfParameters();
+        this.maximumNumberOfParameters = (maxLen - arguments.length);
+
+        Class<?>[] parameterTypes = uncurriedClosure.getParameterTypes();
+        if (parameterTypes.length > 0 && last(parameterTypes).isArray()){
+            this.varargType = last(parameterTypes);
         }
 
-        if (!isVararg()) {
+        if (isVararg()) {
+            this.minParamsExpected = 0;
+        } else {
             // perform some early param checking for non-vararg case
             if (index < 0) {
                 // normalise
-                this.index += origMaxLen;
-                minParamsExpected = 0;
+                this.index += maxLen;
+                this.minParamsExpected = 0;
             } else {
-                minParamsExpected = index + arguments.length;
+                this.minParamsExpected = index + arguments.length;
             }
-            if (maximumNumberOfParameters < 0) {
-                throw new IllegalArgumentException("Can't curry " + arguments.length + " arguments for a closure with " + origMaxLen + " parameters.");
+
+            if (this.maximumNumberOfParameters < 0) {
+                throw new IllegalArgumentException("Can't curry " + arguments.length + " arguments for a closure with " + maxLen + " parameters.");
             }
             if (index < 0) {
-                if (index < -origMaxLen || index > -arguments.length)
-                    throw new IllegalArgumentException("To curry " + arguments.length + " argument(s) expect index range " +
-                            (-origMaxLen) + ".." + (-arguments.length) + " but found " + index);
-            } else if (index > maximumNumberOfParameters) {
-                throw new IllegalArgumentException("To curry " + arguments.length + " argument(s) expect index range 0.." +
-                        maximumNumberOfParameters + " but found " + index);
+                int lower = -maxLen;
+                int upper = -arguments.length;
+                if (index < lower || index > upper)
+                    throw new IllegalArgumentException("To curry " + arguments.length + " argument(s) expect index range " + lower + ".." + upper + " but found " + index);
+            } else if (index > this.maximumNumberOfParameters) {
+                throw new IllegalArgumentException("To curry " + arguments.length + " argument(s) expect index range 0.." + this.maximumNumberOfParameters + " but found " + index);
             }
-        } else {
-            minParamsExpected = 0;
         }
     }
 
-    public CurriedClosure(Closure<V> uncurriedClosure, Object... arguments) {
+    public CurriedClosure(final Closure<V> uncurriedClosure, final Object... arguments) {
         this(0, uncurriedClosure, arguments);
     }
 
-    public Object[] getUncurriedArguments(Object... arguments) {
+    //--------------------------------------------------------------------------
+
+    public Object[] getUncurriedArguments(final Object... arguments) {
         if (isVararg()) {
-            int normalizedIndex = index < 0 ? index + arguments.length + curriedParams.length : index;
+            int normalizedIndex = index < 0 ? index + arguments.length + curriedArguments.length : index;
             if (normalizedIndex < 0 || normalizedIndex > arguments.length) {
                 throw new IllegalArgumentException("When currying expected index range between " +
-                        (-arguments.length - curriedParams.length) + ".." + (arguments.length + curriedParams.length) + " but found " + index);
+                        (-arguments.length - curriedArguments.length) + ".." + (arguments.length + curriedArguments.length) + " but found " + index);
             }
-            return createNewCurriedParams(normalizedIndex, arguments);
+            return getArguments(normalizedIndex, arguments);
         }
-        if (curriedParams.length + arguments.length < minParamsExpected) {
+        if (curriedArguments.length + arguments.length < minParamsExpected) {
             throw new IllegalArgumentException("When currying expected at least " + index + " argument(s) to be supplied before known curried arguments but found " + arguments.length);
         }
-        int newIndex = Math.min(index, curriedParams.length + arguments.length - 1);
+        int newIndex = Math.min(index, curriedArguments.length + arguments.length - 1);
         // rcurried arguments are done lazily to allow normal method selection between overloaded alternatives
         newIndex = Math.min(newIndex, arguments.length);
-        return createNewCurriedParams(newIndex, arguments);
+        return getArguments(newIndex, arguments);
     }
 
-    private Object[] createNewCurriedParams(int normalizedIndex, Object[] arguments) {
-        Object[] newCurriedParams = new Object[curriedParams.length + arguments.length];
-        System.arraycopy(arguments, 0, newCurriedParams, 0, normalizedIndex);
-        System.arraycopy(curriedParams, 0, newCurriedParams, normalizedIndex, curriedParams.length);
-        if (arguments.length - normalizedIndex > 0)
-            System.arraycopy(arguments, normalizedIndex, newCurriedParams, curriedParams.length + normalizedIndex, arguments.length - normalizedIndex);
-        return newCurriedParams;
+    private Object[] getArguments(final int index, final Object[] arguments) {
+        Object[] newArguments = new Object[curriedArguments.length + arguments.length];
+        System.arraycopy(arguments, 0, newArguments, 0, index);
+        System.arraycopy(curriedArguments, 0, newArguments, index, curriedArguments.length);
+        if (arguments.length - index > 0)
+            System.arraycopy(arguments, index, newArguments, curriedArguments.length + index, arguments.length - index);
+        return newArguments;
     }
 
     @Override
-    public void setDelegate(Object delegate) {
-        ((Closure) getOwner()).setDelegate(delegate);
+    public void setDelegate(final Object delegate) {
+        getOwner().setDelegate(delegate);
     }
 
     @Override
     public Object getDelegate() {
-        return ((Closure) getOwner()).getDelegate();
+        return getOwner().getDelegate();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Closure<V> getOwner() {
+        return (Closure<V>) super.getOwner();
     }
 
     @Override
-    public void setResolveStrategy(int resolveStrategy) {
-        ((Closure) getOwner()).setResolveStrategy(resolveStrategy);
+    public void setResolveStrategy(final int resolveStrategy) {
+        getOwner().setResolveStrategy(resolveStrategy);
     }
 
     @Override
     public int getResolveStrategy() {
-        return ((Closure) getOwner()).getResolveStrategy();
+        return getOwner().getResolveStrategy();
     }
 
     @Override
-    @SuppressWarnings("unchecked")
     public Object clone() {
-        Closure<V> uncurriedClosure = (Closure<V>) ((Closure) getOwner()).clone();
-        return new CurriedClosure<V>(index, uncurriedClosure, curriedParams);
+        @SuppressWarnings("unchecked")
+        Closure<V> uncurriedClosure = (Closure<V>) getOwner().clone();
+        return new CurriedClosure<V>(index, uncurriedClosure, curriedArguments);
     }
 
     @Override
     public Class[] getParameterTypes() {
-        Class[] oldParams = ((Closure) getOwner()).getParameterTypes();
+        Class[] oldParams = getOwner().getParameterTypes();
         int extraParams = 0;
-        int gobbledParams = curriedParams.length;
+        int gobbledParams = curriedArguments.length;
         if (isVararg()) {
             int numNonVarargs = oldParams.length - 1;
             if (index < 0) {
@@ -167,7 +179,7 @@ public final class CurriedClosure<V> extends Closure<V> {
                 // so work out minimal type params and vararg on end will allow for other possibilities
                 if (absIndex > numNonVarargs) gobbledParams = numNonVarargs;
                 int newNumNonVarargs = numNonVarargs - gobbledParams;
-                if (absIndex - curriedParams.length > newNumNonVarargs) extraParams = absIndex - curriedParams.length - newNumNonVarargs;
+                if (absIndex - curriedArguments.length > newNumNonVarargs) extraParams = absIndex - curriedArguments.length - newNumNonVarargs;
                 int keptParams = Math.max(numNonVarargs - absIndex, 0);
                 Class[] newParams = new Class[keptParams + newNumNonVarargs + extraParams + 1];
                 System.arraycopy(oldParams, 0, newParams, 0, keptParams);
@@ -177,11 +189,11 @@ public final class CurriedClosure<V> extends Closure<V> {
                 return newParams;
             }
             int leadingKept = Math.min(index, numNonVarargs);
-            int trailingKept = Math.max(numNonVarargs - leadingKept - curriedParams.length, 0);
+            int trailingKept = Math.max(numNonVarargs - leadingKept - curriedArguments.length, 0);
             if (index > leadingKept) extraParams = index - leadingKept;
             Class[] newParams = new Class[leadingKept + trailingKept + extraParams + 1];
             System.arraycopy(oldParams, 0, newParams, 0, leadingKept);
-            if (trailingKept > 0) System.arraycopy(oldParams, leadingKept + curriedParams.length, newParams, leadingKept, trailingKept);
+            if (trailingKept > 0) System.arraycopy(oldParams, leadingKept + curriedArguments.length, newParams, leadingKept, trailingKept);
             for (int i = 0; i < extraParams; i++) newParams[leadingKept + trailingKept + i] = varargType.getComponentType();
             newParams[newParams.length - 1] = varargType;
             return newParams;
@@ -189,7 +201,7 @@ public final class CurriedClosure<V> extends Closure<V> {
         Class[] newParams = new Class[oldParams.length - gobbledParams + extraParams];
         System.arraycopy(oldParams, 0, newParams, 0, index);
         if (newParams.length - index > 0)
-            System.arraycopy(oldParams, curriedParams.length + index, newParams, index, newParams.length - index);
+            System.arraycopy(oldParams, curriedArguments.length + index, newParams, index, newParams.length - index);
         return newParams;
     }
 
diff --git a/src/main/java/org/codehaus/groovy/runtime/InvokerHelper.java b/src/main/java/org/codehaus/groovy/runtime/InvokerHelper.java
index 7e4f547e53..87d3f990a5 100644
--- a/src/main/java/org/codehaus/groovy/runtime/InvokerHelper.java
+++ b/src/main/java/org/codehaus/groovy/runtime/InvokerHelper.java
@@ -226,9 +226,10 @@ public class InvokerHelper {
     }
 
     /**
-     * Returns the method pointer for the given object name
+     * Returns a method closure for the given object and name.
      */
-    public static Closure getMethodPointer(Object object, String methodName) {
+    @SuppressWarnings("rawtypes")
+    public static Closure getMethodPointer(final Object object, final String methodName) {
         if (object == null) {
             throw new NullPointerException("Cannot access method pointer for '" + methodName + "' on null object");
         }
diff --git a/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java b/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java
index eaad0bdeb0..897c22f852 100644
--- a/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java
+++ b/src/main/java/org/codehaus/groovy/runtime/MethodClosure.java
@@ -20,7 +20,6 @@ package org.codehaus.groovy.runtime;
 
 import groovy.lang.Closure;
 import groovy.lang.MetaMethod;
-import org.codehaus.groovy.reflection.CachedConstructor;
 import org.codehaus.groovy.reflection.ReflectionCache;
 
 import java.util.Arrays;
@@ -54,20 +53,21 @@ public class MethodClosure extends Closure {
         this.maximumNumberOfParameters = 0;
         this.parameterTypes = MetaClassHelper.EMPTY_TYPE_ARRAY;
 
-        Class<?> clazz = owner.getClass() == Class.class ? (Class<?>) owner : owner.getClass();
+        Class<?> theClass = owner.getClass();
+        if (theClass == Class.class) theClass = (Class<?>) owner;
 
-        if (NEW.equals(method)) {
-            if (clazz.isArray()) {
-                Class<?>[] sizeTypes = new Class[ArrayTypeUtils.dimension(clazz)];
+        if (method.equals(NEW)) {
+            if (theClass.isArray()) {
+                Class<?>[] sizeTypes = new Class[ArrayTypeUtils.dimension(theClass)];
                 Arrays.fill(sizeTypes, int.class);
                 setParameterTypesAndNumber(sizeTypes);
             } else {
-                for (CachedConstructor c : ReflectionCache.getCachedClass(clazz).getConstructors()) {
+                for (var c : ReflectionCache.getCachedClass(theClass).getConstructors()) {
                     setParameterTypesAndNumber(c.getNativeParameterTypes());
                 }
             }
         } else {
-            for (MetaMethod m : InvokerHelper.getMetaClass(clazz).respondsTo(owner, method)) {
+            for (var m : InvokerHelper.getMetaClass(theClass).respondsTo(owner, method)) {
                 setParameterTypesAndNumber(makeParameterTypes(owner, m));
                 if (!m.isStatic()) {
                     this.anyInstanceMethodExists = true;
@@ -76,19 +76,17 @@ public class MethodClosure extends Closure {
         }
     }
 
-    private void setParameterTypesAndNumber(final Class[] newParameterTypes) {
-        if (!(newParameterTypes.length > this.maximumNumberOfParameters)) {
-            return;
+    private void setParameterTypesAndNumber(final Class[] parameterTypes) {
+        if (parameterTypes.length > this.maximumNumberOfParameters) {
+            this.maximumNumberOfParameters = parameterTypes.length;
+            this.parameterTypes = parameterTypes;
         }
-        this.maximumNumberOfParameters = newParameterTypes.length;
-        this.parameterTypes = newParameterTypes;
     }
 
     /*
-     * Create a new array of parameter type.
-     *
-     * If the owner is a class instance(e.g. String) and the method is instance method,
-     * we expand the original array of parameter type by inserting the owner at the first place of the expanded array
+     * Creates an array of parameter types. If the owner is a class instance (ex:
+     * String) and the method is instance method, we expand the original array of
+     * parameter type by inserting the owner at the first position of the array.
      */
     private Class[] makeParameterTypes(final Object owner, final MetaMethod m) {
         Class[] newParameterTypes;
diff --git a/src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java b/src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java
index 692904a8ab..1e14400e80 100644
--- a/src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java
+++ b/src/main/java/org/codehaus/groovy/runtime/ScriptBytecodeAdapter.java
@@ -578,13 +578,13 @@ public class ScriptBytecodeAdapter {
     //  --------------------------------------------------------
 
     /**
-     * Returns the method pointer for the given object name
+     * Returns a method closure for the given object and name.
      *
-     * @param object the object containing the method
-     * @param methodName the name of the method of interest
+     * @param object the object or class providing the method
+     * @param methodName the method(s) of interest
      * @return the resulting Closure
      */
-    public static Closure getMethodPointer(Object object, String methodName) {
+    public static Closure getMethodPointer(final Object object, final String methodName) {
         return InvokerHelper.getMethodPointer(object, methodName);
     }
 
diff --git a/src/test/groovy/ClosureCurryTest.groovy b/src/test/groovy/ClosureCurryTest.groovy
index 799b91fa18..20b5abbd76 100644
--- a/src/test/groovy/ClosureCurryTest.groovy
+++ b/src/test/groovy/ClosureCurryTest.groovy
@@ -18,14 +18,14 @@
  */
 package groovy
 
-import groovy.test.GroovyTestCase
-import org.codehaus.groovy.runtime.DefaultGroovyMethods
+import org.junit.Test
 
-/**
- * Tests for curried closures
- */
-class ClosureCurryTest extends GroovyTestCase {
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+final class ClosureCurryTest {
 
+    @Test
     void testCurry() {
         def clos1 = {s1, s2 -> s1 + s2}
         def clos2 = clos1.curry("hi")
@@ -103,6 +103,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert value == "afg"
     }
 
+    @Test
     void testParameterTypes() {
         def cl1 = {String s1, int i -> return s1 + i }
         assert "foo5" == cl1("foo", 5)
@@ -114,6 +115,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert [int] == cl2.getParameterTypes().toList()
     }
 
+    @Test
     void testVarargParameterTypes() {
         def a = { Object one, Object[] others -> }
         def b = a.ncurry(2, 'x', 'y')
@@ -127,6 +129,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert h.parameterTypes.name == ['java.lang.String', '[Ljava.lang.Object;']
     }
 
+    @Test
     void testVarargCurry() {
         def c = { arg, Object[] extras -> arg + ', ' + extras.join(', ') }
         def d = c.curry( 1 ) //curry first param only
@@ -137,6 +140,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert f( 13, 15 ) == '1, 3, 5, 7, 9, 11, 13, 15'
     }
 
+    @Test
     void testDelegate() {
         def res = null
         def c = {a -> res = z}
@@ -147,6 +151,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert res == cc.delegate.z
     }
 
+    @Test
     void testExpandoWithCurry() {
         def sz = 'java.util.Date'.size()
         def c = {arg -> arg + delegate.getClass().name.size() }
@@ -157,18 +162,21 @@ class ClosureCurryTest extends GroovyTestCase {
         assert d.bar() == 'baz' + sz
     }
 
+    @Test
     void testCurryMultiply() {
         def multiply = { a, b -> a * b }
         def doubler = multiply.curry(2)
         assert doubler(4) == 8
     }
 
+    @Test
     void testRCurryDivide() {
         def divide = { a, b -> a / b }
         def halver = divide.rcurry(2)
         assert halver(8) == 4
     }
 
+    @Test
     void testNCurryBinarySearch() {
         def caseInsensitive = { a, b -> a.toLowerCase() <=> b.toLowerCase() } as Comparator
         def caseSensitive = { a, b -> a <=> b } as Comparator
@@ -185,6 +193,7 @@ class ClosureCurryTest extends GroovyTestCase {
         } == [-3, 2, -3, -4]
     }
 
+    @Test
     void testNestedNcurryRcurry() {
         def operation = { int x, Closure f, int y -> f(x, y) }
         def divider = operation.ncurry(1) { a, b -> a / b }
@@ -192,12 +201,14 @@ class ClosureCurryTest extends GroovyTestCase {
         assert 50 == halver(100)
     }
 
+    @Test
     void testEquivalentsNormal() {
         def a = {one, two, three, four, five -> "$one,$two,$three,$four,$five"}
         def expected = '1,2,3,4,5'
         checkEquivalents(a, expected)
     }
 
+    @Test
     void testEquivalentsVararg() {
         def a = {one, two, Object[] others -> "one=$one, two=$two, others=${others.join(',')}"}
         def expected = 'one=1, two=2, others=3,4,5'
@@ -229,6 +240,7 @@ class ClosureCurryTest extends GroovyTestCase {
         }
     }
 
+    @Test
     void testNullVariants() {
         assert { x, y -> x ?: y }.curry(null)(null) == null
         assert { x, y -> x ?: y }.curry(null)(2) == 2
@@ -253,7 +265,7 @@ class ClosureCurryTest extends GroovyTestCase {
     private a(b,c){ "2Obj: $b $c" }
     private a(b){ "1Obj: $b" }
 
-
+    @Test
     void testCurryWithMethodClosure() {
         def c = (this.&a).curry(0)
         assert c(1,2) == '3Int: 0 1 2'
@@ -268,6 +280,27 @@ class ClosureCurryTest extends GroovyTestCase {
         assert b() == '1Obj: 0'
     }
 
+    @Test // GROOVY-5051, GROOVY-10568
+    void testCurryWithMethodClosure2() {
+        assertScript '''
+            class C {
+                private void m(p) {
+                    println p
+                }
+                void test() {
+                    def proc = C.&m.curry(this, 'x') // was "this.&m.curry('x')"
+                    proc()
+                }
+            }
+            new C().test()
+
+            class D extends C {
+            }
+            new D().test() // IllegalArgumentException: Can't curry 1 arguments for a closure with 0 parameters
+        '''
+    }
+
+    @Test
     void testInsufficientSuppliedArgsWhenUsingNCurry() {
         def cl = { a, b, c = 'x' -> a + b + c }
         assert cl.ncurry(1, 'b')('a') == 'abx'
@@ -280,6 +313,7 @@ class ClosureCurryTest extends GroovyTestCase {
         assert cl.rcurry('b', 'c')() == 'bcx'
     }
 
+    @Test
     void testCurryInComboWithDefaultArgs() {
         def cl = { a, b, c='c', d='d' -> a + b + c + d }
         assert 'abcd' == cl.ncurry(0, 'a')('b')