[ 
https://issues.apache.org/jira/browse/GROOVY-12439?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Stanley Brubaker updated GROOVY-12439:
--------------------------------------
    Description: 
A varargs call that selected a {{double}} overload up to 4.0.3 now silently 
selects a narrowing {{float}} one, keeping about 7 significant digits instead 
of 16 -- or, when exactly one integral argument accompanies the {{BigDecimal}}, 
fails with {{GroovyRuntimeException}} instead. Which of the two you get depends 
only on how many integral arguments are present. Code that worked on 4.0.3 
therefore either loses precision without warning or dies at runtime.

The root cause is that in 4.0.4 a {{BigDecimal}} argument became applicable to 
a {{float}} *varargs* parameter; scalar {{float}} parameters already accepted 
it. From 5.0.0 a tie between scalar overloads also resolves to {{float}}. So 
where both {{float}} and {{double}} overloads exist, the outcome now depends 
only on argument count and call shape: {{double}}, an ambiguity error, or a 
silently narrowing {{float}}.

h2. Reproducer

{code:java}
public class Sel {
    public static String m(float... x)          { return "float...";  }
    public static String m(double... x)         { return "double..."; }
    public static String m2(float a, float b)   { return "float(2)";  }
    public static String m2(double a, double b) { return "double(2)"; }
}
{code}

{code:groovy}
import static Sel.*
z = 1
d = 5.0          // untyped decimal literal, therefore BigDecimal
println m(d)
println m(z, d)
println m(5, z, d)
println m2(z, d)
{code}

{code}
javac --release 17 Sel.java     // --release 17 so pre-4.0.33 ASM can read the 
class
java -cp groovy-<version>.jar:. groovy.ui.GroovyMain repro.groovy
{code}

||call||3.0.25||4.0.3||4.0.4||5.0.0 - 6.0.0||
|{{m(d)}}|double...|double...|double...|double...|
|{{m(z, d)}}|double...|double...|*AMBIGUOUS*|*AMBIGUOUS*|
|{{m(5, z, d)}}|double...|double...|*float...*|*float...*|
|{{m2(z, d)}}|AMBIGUOUS|AMBIGUOUS|AMBIGUOUS|*float(2)*|

Expected {{double}} in all three failing cases. They regress differently:

||call||behaviour||
|{{m(d)}}|{{double...}} on every version 3.0.25 - 6.0.0. Unchanged, included as 
a control.|
|{{m(z, d)}}|{{double...}} up to 4.0.3, ambiguity error from 4.0.4. One 
integral argument gives a 10/10 tie.|
|{{m(5, z, d)}}|{{double...}} up to 4.0.3, silently narrowing {{float...}} from 
4.0.4. Two integral arguments outvote the {{BigDecimal}}: 15 against 17.|
|{{m2(z, d)}}|ambiguity error 3.0.25 - 4.0.33, then silently resolves to the 
narrowing {{float(2)}} from 5.0.0.|

h2. Cause

Two independent changes, each bisected to a single release.

*4.0.4 -- BigDecimal became applicable to float varargs.* 
{{ParameterTypes.isValidVargsMethod}} swapped its applicability predicate in 
ef7026317f ("GROOVY-8045: support coercion for variadic parameter"):

{code:java}
// 4.0.3
if (MetaClassHelper.isAssignableFrom(clazz, arguments[i])) continue;
// 4.0.4
if (!componentType.isAssignableFrom(argumentTypes[i])) { return false; }
{code}

{{MetaClassHelper.isAssignableFrom}} and {{CachedClass.isAssignableFrom}} 
disagree about {{BigDecimal}} -> {{float}}. With a class exposing *only* 
{{float...}}, so no competing candidate and no distance comparison involved, 
4.0.3 raises {{MissingMethodException}} for {{f(BigDecimal)}} while 4.0.4 
accepts it. So {{float...}} was previously not a candidate at all and 
{{double...}} won by being the only applicable overload.

Note this predicate is on the varargs path only, so {{BigDecimal}} -> {{float}} 
was already applicable for *scalar* parameters before 4.0.4. That is why 
{{m2(z, d)}} was ambiguous as far back as 3.0.25 and why only the varargs row 
changed in 4.0.4.

Once both are applicable, {{PRIMITIVE_DISTANCE_TABLE}} decides. 
{{getPrimitiveDistance(from, to)}} returns {{TABLE\[toIndex\]\[fromIndex\]}} 
and is called as {{getPrimitiveDistance(parameterClass, argument)}}, so the 
lookup is {{TABLE\[argument\]\[parameter\]}}:

||argument||-> float||-> double||
|{{int}} / {{Integer}}|5|7|
|{{BigDecimal}}|5|3|

For {{(int, BigDecimal)}} the totals are 5+5=10 for {{float}} and 7+3=10 for 
{{double}}: the integral argument's two-point lean toward {{float}} exactly 
cancels the {{BigDecimal}}'s two-point lean toward {{double}}, producing the 
tie reported as ambiguous.

*5.0.0 -- the scalar tie started resolving to float.* This coincides with the 
GROOVY-11258 redundant-match pruning added to 
{{MetaClassImpl.doChooseMostSpecificParams}} (absent in 4.0.33, present in 
5.0.0). It removes a tied match whenever another match's parameter types can be 
passed to it ({{parametersAreCompatible(innerTypes, outerTypes)}}); since 
{{float}} widens to {{double}}, {{double(2)}} is discarded and the narrowing 
{{float(2)}} is kept. The same rule cannot apply to the varargs pair, because 
{{float[]}} and {{double[]}} are not assignable to each other -- which is 
consistent with {{m(z, d)}} still being ambiguous on 5.x.


So there are two distinct problems:

* in 4.0.4 a working {{double}} selection became either a silent narrowing to 
{{float}} or an error, depending on how many integral arguments accompany the 
{{BigDecimal}} (varargs), and
* in 5.0.0 a long-standing *detected* ambiguity became a silent narrowing 
conversion (scalar).

The second is arguably the more serious, since it replaces a loud failure with 
silent precision loss.

h2. Scope

Deliberately not reported here: an integral argument also prefers {{float}} 
over {{double}}, but that is long-standing (identical in 3.0.25) and javac 
selects {{float}} too, since JLS 4.10.1 makes {{float}} more specific than 
{{double}}. Only the {{BigDecimal}} behaviour is new, and Java has no 
equivalent because the conversion does not exist implicitly there.

h2. Suggested direction

Restoring the pre-4.0.4 varargs applicability -- scoping GROOVY-8045's coercion 
change to non-numeric component types -- would fix the varargs rows but not 
{{m2(z, d)}}, which narrows through the 5.0.0 tie-break on parameters that were 
always applicable. Addressing both probably means weighting lossy conversions 
in the distance calculation, so that a narrowing such as {{BigDecimal}} -> 
{{float}} can neither tie with nor be selected over a widening such as 
{{BigDecimal}} -> {{double}}.

h2. Impact

Found in Deephaven Community Core (Apache-2.0), whose generated 
{{io.deephaven.function.Numeric}} offers {{max}}/{{min}} over the full 
primitive ladder. In a Groovy console {{max(5, 2, 3, 1, 5.0)}} changed from 
{{Double}} to {{Float}} and {{max(1, 5.0)}} began throwing. Libraries that do 
not expose both {{float}} and {{double}} overloads are unaffected.

For us this specifically blocks JDK 27 support. We run fine on 3.0.25 under JDK 
17, 21 and 25, but the two requirements for 27 are mutually exclusive: reading 
JDK 27 class files needs the ASM 9.10.1 bump present only in 4.0.33+ / 5.0.7+, 
while preserving the previous selection needs 4.0.3 or earlier. No Groovy 
release does both, and the shaded {{groovyjarjarasm}} cannot be upgraded 
independently. So we stay on 3.0.25 and cannot add JDK 27 to our test matrix 
until this is resolved or we accept the precision change for our users.

Workaround: a typed literal ({{5.0d}}) or a declared {{double}}, which selects 
{{double}} on every version from 3.0.25 through 6.0.0.

Discussed on [email protected]: 
https://lists.apache.org/thread/79xony935rzccs93kmznzrvd4ros50x1

h2. Related

* GROOVY-1937 (fixed 1.1-beta-2) "Number conversion from BigDecimal to double 
does not work for Varargs" -- established {{BigDecimal}} -> {{double}} for 
varargs.
* GROOVY-5490 (open, 2012) "Double Dispatch Distance Algorithm Incorrectly 
Implemented" -- the same minimum-distance algorithm, for inheritance distance 
among reference types.

  was:
A call that selected a {{double}} overload up to 4.0.3 now silently selects a 
narrowing {{float}} one, losing about nine significant digits -- and the same 
call in varargs form throws {{GroovyRuntimeException}} instead. Code that 
worked on 4.0.3 either loses precision without warning or fails at runtime, 
depending only on the shape of the call.

The cause is that a {{BigDecimal}} argument became applicable to a {{float}} 
parameter in 4.0.4. Where both {{float}} and {{double}} overloads exist, the 
same conceptual call now gives three different answers: {{double}} for a lone 
{{BigDecimal}}, an ambiguity error for varargs, and a silently narrowing 
{{float}} for two scalars.

h2. Reproducer

{code:java}
public class Sel {
    public static String m(float... x)          { return "float...";  }
    public static String m(double... x)         { return "double..."; }
    public static String m2(float a, float b)   { return "float(2)";  }
    public static String m2(double a, double b) { return "double(2)"; }
}
{code}

{code:groovy}
import static Sel.*
z = 1
d = 5.0          // untyped decimal literal, therefore BigDecimal
println m(d)
println m(z, d)
println m2(z, d)
{code}

{code}
javac --release 17 Sel.java     // --release 17 so pre-4.0.33 ASM can read the 
class
java -cp groovy-<version>.jar:. groovy.ui.GroovyMain repro.groovy
{code}

||call||3.0.25||4.0.3||4.0.4||5.0.0 - 6.0.0||
|{{m(d)}}|double...|double...|double...|double...|
|{{m(z, d)}}|double...|double...|*AMBIGUOUS*|*AMBIGUOUS*|
|{{m2(z, d)}}|AMBIGUOUS|AMBIGUOUS|AMBIGUOUS|*float(2)*|

Expected: {{double}} in all three, as in 4.0.3.

h2. Cause

Two independent changes, each bisected to a single release.

*4.0.4 -- BigDecimal became applicable to float.* 
{{ParameterTypes.isValidVargsMethod}} swapped its applicability predicate in 
ef7026317f ("GROOVY-8045: support coercion for variadic parameter"):

{code:java}
// 4.0.3
if (MetaClassHelper.isAssignableFrom(clazz, arguments[i])) continue;
// 4.0.4
if (!componentType.isAssignableFrom(argumentTypes[i])) { return false; }
{code}

{{MetaClassHelper.isAssignableFrom}} and {{CachedClass.isAssignableFrom}} 
disagree about {{BigDecimal}} -> {{float}}. With a class exposing *only* 
{{float...}}, so no competing candidate and no distance comparison involved, 
4.0.3 raises {{MissingMethodException}} for {{f(BigDecimal)}} while 4.0.4 
accepts it. So {{float...}} was previously not a candidate at all and 
{{double...}} won by being the only applicable overload.

Once both are applicable, {{PRIMITIVE_DISTANCE_TABLE}} decides. 
{{getPrimitiveDistance(from, to)}} returns {{TABLE\[toIndex\]\[fromIndex\]}} 
and is called as {{getPrimitiveDistance(parameterClass, argument)}}, so the 
lookup is {{TABLE\[argument\]\[parameter\]}}:

||argument||-> float||-> double||
|{{int}} / {{Integer}}|5|7|
|{{BigDecimal}}|5|3|

For {{(int, BigDecimal)}} the totals are 5+5=10 for {{float}} and 7+3=10 for 
{{double}}: the integral argument's two-point lean toward {{float}} exactly 
cancels the {{BigDecimal}}'s two-point lean toward {{double}}, producing the 
tie reported as ambiguous.

*5.0.0 -- the tie started resolving to float.* The GROOVY-11258 redundant-match 
pruning added to {{MetaClassImpl.doChooseMostSpecificParams}} (absent in 
4.0.33, present in 5.0.0) discards one of the tied matches, so the non-varargs 
form silently selects the narrowing {{float(2)}} instead of erroring.

h2. Scope

Deliberately not reported here: an integral argument also prefers {{float}} 
over {{double}}, but that is long-standing (identical in 3.0.25) and javac 
selects {{float}} too, since JLS 4.10.1 makes {{float}} more specific than 
{{double}}. Only the {{BigDecimal}} behaviour is new, and Java has no 
equivalent because the conversion does not exist implicitly there.

h2. Suggested direction

Either restore the pre-4.0.4 applicability so {{BigDecimal}} is not implicitly 
narrowed to {{float}} while a {{double}} overload exists -- scoping 
GROOVY-8045's coercion change to non-numeric component types -- or weight lossy 
conversions so a narrowing cannot tie with, or be selected over, a widening.

h2. Impact

Found in Deephaven Community Core (Apache-2.0), whose generated 
{{io.deephaven.function.Numeric}} offers {{max}}/{{min}} over the full 
primitive ladder. In a Groovy console {{max(5, 2, 3, 1, 5.0)}} changed from 
{{Double}} to {{Float}} and {{max(1, 5.0)}} began throwing. Libraries that do 
not expose both {{float}} and {{double}} overloads are unaffected.

For us this specifically blocks JDK 27 support. We run fine on 3.0.25 under JDK 
17, 21 and 25, but the two requirements for 27 are mutually exclusive: reading 
JDK 27 class files needs the ASM 9.10.1 bump present only in 4.0.33+ / 5.0.7+, 
while preserving the previous selection needs 4.0.3 or earlier. No Groovy 
release does both, and the shaded {{groovyjarjarasm}} cannot be upgraded 
independently. So we stay on 3.0.25 and cannot add JDK 27 to our test matrix 
until this is resolved or we accept the precision change for our users.

Workaround: a typed literal ({{5.0d}}) or a declared {{double}}, which selects 
{{double}} on every version from 3.0.25 through 6.0.0.

Discussed on [email protected]: 
https://lists.apache.org/thread/79xony935rzccs93kmznzrvd4ros50x1

h2. Related

* GROOVY-1937 (fixed 1.1-beta-2) "Number conversion from BigDecimal to double 
does not work for Varargs" -- established {{BigDecimal}} -> {{double}} for 
varargs.
* GROOVY-5490 (open, 2012) "Double Dispatch Distance Algorithm Incorrectly 
Implemented" -- the same minimum-distance algorithm, for inheritance distance 
among reference types.


> Regression: BigDecimal silently narrowed to float in overload selection since 
> 4.0.4
> -----------------------------------------------------------------------------------
>
>                 Key: GROOVY-12439
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12439
>             Project: Groovy
>          Issue Type: Bug
>          Components: groovy-runtime
>    Affects Versions: 4.0.4, 5.1.3, 6.0.0
>         Environment: Temurin JDK 17, 21, 25 and 27 on Linux x64 — behaviour 
> is identical on all four, so this is not JDK-specific. Plain dynamic Groovy: 
> no @CompileStatic or @TypeChecked, no metaprogramming, no third-party 
> libraries.
>            Reporter: Stanley Brubaker
>            Priority: Critical
>
> A varargs call that selected a {{double}} overload up to 4.0.3 now silently 
> selects a narrowing {{float}} one, keeping about 7 significant digits instead 
> of 16 -- or, when exactly one integral argument accompanies the 
> {{BigDecimal}}, fails with {{GroovyRuntimeException}} instead. Which of the 
> two you get depends only on how many integral arguments are present. Code 
> that worked on 4.0.3 therefore either loses precision without warning or dies 
> at runtime.
> The root cause is that in 4.0.4 a {{BigDecimal}} argument became applicable 
> to a {{float}} *varargs* parameter; scalar {{float}} parameters already 
> accepted it. From 5.0.0 a tie between scalar overloads also resolves to 
> {{float}}. So where both {{float}} and {{double}} overloads exist, the 
> outcome now depends only on argument count and call shape: {{double}}, an 
> ambiguity error, or a silently narrowing {{float}}.
> h2. Reproducer
> {code:java}
> public class Sel {
>     public static String m(float... x)          { return "float...";  }
>     public static String m(double... x)         { return "double..."; }
>     public static String m2(float a, float b)   { return "float(2)";  }
>     public static String m2(double a, double b) { return "double(2)"; }
> }
> {code}
> {code:groovy}
> import static Sel.*
> z = 1
> d = 5.0          // untyped decimal literal, therefore BigDecimal
> println m(d)
> println m(z, d)
> println m(5, z, d)
> println m2(z, d)
> {code}
> {code}
> javac --release 17 Sel.java     // --release 17 so pre-4.0.33 ASM can read 
> the class
> java -cp groovy-<version>.jar:. groovy.ui.GroovyMain repro.groovy
> {code}
> ||call||3.0.25||4.0.3||4.0.4||5.0.0 - 6.0.0||
> |{{m(d)}}|double...|double...|double...|double...|
> |{{m(z, d)}}|double...|double...|*AMBIGUOUS*|*AMBIGUOUS*|
> |{{m(5, z, d)}}|double...|double...|*float...*|*float...*|
> |{{m2(z, d)}}|AMBIGUOUS|AMBIGUOUS|AMBIGUOUS|*float(2)*|
> Expected {{double}} in all three failing cases. They regress differently:
> ||call||behaviour||
> |{{m(d)}}|{{double...}} on every version 3.0.25 - 6.0.0. Unchanged, included 
> as a control.|
> |{{m(z, d)}}|{{double...}} up to 4.0.3, ambiguity error from 4.0.4. One 
> integral argument gives a 10/10 tie.|
> |{{m(5, z, d)}}|{{double...}} up to 4.0.3, silently narrowing {{float...}} 
> from 4.0.4. Two integral arguments outvote the {{BigDecimal}}: 15 against 17.|
> |{{m2(z, d)}}|ambiguity error 3.0.25 - 4.0.33, then silently resolves to the 
> narrowing {{float(2)}} from 5.0.0.|
> h2. Cause
> Two independent changes, each bisected to a single release.
> *4.0.4 -- BigDecimal became applicable to float varargs.* 
> {{ParameterTypes.isValidVargsMethod}} swapped its applicability predicate in 
> ef7026317f ("GROOVY-8045: support coercion for variadic parameter"):
> {code:java}
> // 4.0.3
> if (MetaClassHelper.isAssignableFrom(clazz, arguments[i])) continue;
> // 4.0.4
> if (!componentType.isAssignableFrom(argumentTypes[i])) { return false; }
> {code}
> {{MetaClassHelper.isAssignableFrom}} and {{CachedClass.isAssignableFrom}} 
> disagree about {{BigDecimal}} -> {{float}}. With a class exposing *only* 
> {{float...}}, so no competing candidate and no distance comparison involved, 
> 4.0.3 raises {{MissingMethodException}} for {{f(BigDecimal)}} while 4.0.4 
> accepts it. So {{float...}} was previously not a candidate at all and 
> {{double...}} won by being the only applicable overload.
> Note this predicate is on the varargs path only, so {{BigDecimal}} -> 
> {{float}} was already applicable for *scalar* parameters before 4.0.4. That 
> is why {{m2(z, d)}} was ambiguous as far back as 3.0.25 and why only the 
> varargs row changed in 4.0.4.
> Once both are applicable, {{PRIMITIVE_DISTANCE_TABLE}} decides. 
> {{getPrimitiveDistance(from, to)}} returns {{TABLE\[toIndex\]\[fromIndex\]}} 
> and is called as {{getPrimitiveDistance(parameterClass, argument)}}, so the 
> lookup is {{TABLE\[argument\]\[parameter\]}}:
> ||argument||-> float||-> double||
> |{{int}} / {{Integer}}|5|7|
> |{{BigDecimal}}|5|3|
> For {{(int, BigDecimal)}} the totals are 5+5=10 for {{float}} and 7+3=10 for 
> {{double}}: the integral argument's two-point lean toward {{float}} exactly 
> cancels the {{BigDecimal}}'s two-point lean toward {{double}}, producing the 
> tie reported as ambiguous.
> *5.0.0 -- the scalar tie started resolving to float.* This coincides with the 
> GROOVY-11258 redundant-match pruning added to 
> {{MetaClassImpl.doChooseMostSpecificParams}} (absent in 4.0.33, present in 
> 5.0.0). It removes a tied match whenever another match's parameter types can 
> be passed to it ({{parametersAreCompatible(innerTypes, outerTypes)}}); since 
> {{float}} widens to {{double}}, {{double(2)}} is discarded and the narrowing 
> {{float(2)}} is kept. The same rule cannot apply to the varargs pair, because 
> {{float[]}} and {{double[]}} are not assignable to each other -- which is 
> consistent with {{m(z, d)}} still being ambiguous on 5.x.
> So there are two distinct problems:
> * in 4.0.4 a working {{double}} selection became either a silent narrowing to 
> {{float}} or an error, depending on how many integral arguments accompany the 
> {{BigDecimal}} (varargs), and
> * in 5.0.0 a long-standing *detected* ambiguity became a silent narrowing 
> conversion (scalar).
> The second is arguably the more serious, since it replaces a loud failure 
> with silent precision loss.
> h2. Scope
> Deliberately not reported here: an integral argument also prefers {{float}} 
> over {{double}}, but that is long-standing (identical in 3.0.25) and javac 
> selects {{float}} too, since JLS 4.10.1 makes {{float}} more specific than 
> {{double}}. Only the {{BigDecimal}} behaviour is new, and Java has no 
> equivalent because the conversion does not exist implicitly there.
> h2. Suggested direction
> Restoring the pre-4.0.4 varargs applicability -- scoping GROOVY-8045's 
> coercion change to non-numeric component types -- would fix the varargs rows 
> but not {{m2(z, d)}}, which narrows through the 5.0.0 tie-break on parameters 
> that were always applicable. Addressing both probably means weighting lossy 
> conversions in the distance calculation, so that a narrowing such as 
> {{BigDecimal}} -> {{float}} can neither tie with nor be selected over a 
> widening such as {{BigDecimal}} -> {{double}}.
> h2. Impact
> Found in Deephaven Community Core (Apache-2.0), whose generated 
> {{io.deephaven.function.Numeric}} offers {{max}}/{{min}} over the full 
> primitive ladder. In a Groovy console {{max(5, 2, 3, 1, 5.0)}} changed from 
> {{Double}} to {{Float}} and {{max(1, 5.0)}} began throwing. Libraries that do 
> not expose both {{float}} and {{double}} overloads are unaffected.
> For us this specifically blocks JDK 27 support. We run fine on 3.0.25 under 
> JDK 17, 21 and 25, but the two requirements for 27 are mutually exclusive: 
> reading JDK 27 class files needs the ASM 9.10.1 bump present only in 4.0.33+ 
> / 5.0.7+, while preserving the previous selection needs 4.0.3 or earlier. No 
> Groovy release does both, and the shaded {{groovyjarjarasm}} cannot be 
> upgraded independently. So we stay on 3.0.25 and cannot add JDK 27 to our 
> test matrix until this is resolved or we accept the precision change for our 
> users.
> Workaround: a typed literal ({{5.0d}}) or a declared {{double}}, which 
> selects {{double}} on every version from 3.0.25 through 6.0.0.
> Discussed on [email protected]: 
> https://lists.apache.org/thread/79xony935rzccs93kmznzrvd4ros50x1
> h2. Related
> * GROOVY-1937 (fixed 1.1-beta-2) "Number conversion from BigDecimal to double 
> does not work for Varargs" -- established {{BigDecimal}} -> {{double}} for 
> varargs.
> * GROOVY-5490 (open, 2012) "Double Dispatch Distance Algorithm Incorrectly 
> Implemented" -- the same minimum-distance algorithm, for inheritance distance 
> among reference types.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to