You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Jochen Theodorou (Jira)" <ji...@apache.org> on 2021/11/20 09:12:00 UTC

[jira] [Commented] (GROOVY-10099) A single null argument to a varargs parameter is received as null

    [ https://issues.apache.org/jira/browse/GROOVY-10099?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17446770#comment-17446770 ] 

Jochen Theodorou commented on GROOVY-10099:
-------------------------------------------

Since this popped up for me...
We actually do have some kind of compile time type information for the dynamic calls, just that they are not very extensive and maybe some are incorrect atm. But if you do 
{code:Java}
String x = null
foo(x)
{code}
then we know even in dynamic groovy at compile time, that foo is called with something of static type String. For invokedynamic we would here then produce a callsite that takes a String. And this is actually part of why indy code has problems at times - it needs to do a lot of type lookups and "zero transformations" just to satisfy the lambda logic for the JVM optimizations. But what we then do, because of how the MOP works, there is no place for this information to be carried along. And at the time of method selection we do not know this anymore. If indy is the only way to call a method (and that is the case now afaik), we could now do small changes to for example transport the MethodType or something similar of the call site. And then we could do a more educated guess for selecting something for null. 

This is of course totally ignoring the fact, that if we are not making calls with static typed variables, but with complex expressions.... no, scratch that.. a simple foo(x.bar(y)) is actually already enough. In dynamic Groovy we *cannot* know the return type of x.bar(y). Any educated guess for a null is then over.

Dynamic Groovy is dynamic typed and that has many implication that deviate it from static Java. We try to be near Java, but we cannot always do that.

So to come back to [~Rachel Greenham]: It is no faux-pas in principle to be using type data known at compile time. In fact the "primitive optimizations" I added back in I think 1.6 where exactly doing that, use and promote types to bypass the Meta Object Protocol (MOP) and have thus fast code that works on primitives and calculations using them. They are working under the assumption, that runtime meta class transformations are active and have a guard for that. That means I actually let the compiler produce 2 paths in the bytecode, of which one is considering types and the other is not, depending on if somebody hooks into the MOP or not. But looking at semantics the program was still supposed to produce the same result. Sometimes considering the type for a null call and sometimes not, because we cannot statically be sure what it is... well that is really something we should not do - unless we have really really good reasons for it.

> A single null argument to a varargs parameter is received as null
> -----------------------------------------------------------------
>
>                 Key: GROOVY-10099
>                 URL: https://issues.apache.org/jira/browse/GROOVY-10099
>             Project: Groovy
>          Issue Type: Bug
>         Environment: Observed on Groovy 3.0.8 on macOS Big Sur (Intel), but I don't think that's relevant; it'll be everywhere.
>            Reporter: Rachel Greenham
>            Priority: Major
>              Labels: varargs
>             Fix For: 4.x
>
>         Attachments: VarArgsTest.groovy, VarArgsTest.jsh
>
>          Time Spent: 3h 20m
>  Remaining Estimate: 0h
>
> (NB: I would set the priority to P2 default to be triaged, but I seem not to have that option, so I left it at the default I was presented with.)
> When calling a method with a varargs parameter with a single value, that value is wrapped into an array of length 1. This is the behaviour in Java, and is the expected behaviour, and _most_ of the time is the behaviour in Groovy too.
> But when that single value is null, Groovy will instead just pass the null into the method. Java will not do this: You'll get an array with a single null in it. Because Groovy's behaviour is unexpected, especially when interfacing with Java code, NullPointerExceptions can often ensue.
> Adding to the inconsistencies, if the Groovy code calling the method is in a {{@CompileStatic}} context, it behaves just like Java, and the method (whether or not _it_ is statically compiled or a dynamic Groovy method) receives an array with a null in it.
> So the behaviour in Groovy is inconsistent, both with itself in a {{@CompileStatic}} situation, and with Java, and is requiring workarounds in Java code to handle this normally-impossible eventuality. (Even if no varargs parameter is given you get an empty array, as in fact you do in Groovy too.)
> This may be an "early instalment weirdness": There's an ancient ticket, from not long after varargs were introduced into Java, which appears to have argued - successfully at the time - that the normal behaviour is a bug: https://issues.apache.org/jira/browse/GROOVY-1026 
> Further adding to the confusion may be that Groovy usually elides the difference between an {{Object[]}} parameter and an {{Object...}} parameter: They both behave the same.
> The offending code appears to be in org.codehaus.groovy.reflection.ParameterTypes.java in method fitToVars, lines 200-215 in master at the time of writing, which even includes a comment that "if the last argument is null, then we don't have to do anything", with which I respectfully disagree. :) That behaviour should be to return an array with a single null in it (Handily, MetaClassHelper.ARRAY_WITH_NULL saves having to make a new one.)
> In principle it's an easy fix (although I've left tagging to others as this is my first issue here), but there'd be an obvious nervousness about changing behaviour like this when there might be a lot of old code out there depending on it behaving the way it does now. OTOH the way it behaves now is breaking the expectations of those of us coming to Groovy from a lifetime of Java...
> Attachments:
> VarArgsTest.groovy - a script saved from, and runnable in, groovyConsole, demonstrating the behaviour. The behaviour is the same regardless of whether the console is launched with the -indy option. (The issue was initially observed in indy.) The dynamic portion of the test, when run, ends in a NullPointerException as Arrays.asList is not expecting a null varargs parameter. Output seen (-indy or  not):
>  
> {code:java}
> name: the static name 1
> params is null? false
> params length is 1
> [blah]
> name: the static name 2
> params is null? false
> params length is 2
> [blah, blue]
> name: the static name 3 with blah=null
> params is null? false
> params length is 1
> [null]
> Arrays.asList(blah)? [null]
> name: the dynamic name 1
> params is null? false
> params length is 1
> [blah]
> name: the dynamic name 2
> params is null? false
> params length is 2
> [blah, blue]
> name: the dynamic name 3 with blah=null
> params is null? true
> Exception thrown
> java.lang.NullPointerException
> ...{code}
> (etc. stack trace not shown for formatting reasons.)
> VarArgsTest.jsh - a jshell script demonstrating Java's behaviour, very similar to the groovy test, but omitting the dynamic portion of the test for obvious reasons. (The statements in the Groovy script ending in semicolons are left that way precisely to mark that they're identical to the Java test.) Runnable with
>  
> {code:java}
> jshell PRINTING VarArgsTest.jsh
> {code}
> Output seen:
> {code:java}
> name: the static name 1
> params is null? false
> params length is 1
> [blah]
> name: the static name 2
> params is null? false
> params length is 2
> [blah, blue]
> name: the static name 3 with blah=null
> params is null? false
> params length is 1
> [null]
> Arrays.asList(blah)? [null]
> {code}



--
This message was sent by Atlassian Jira
(v8.20.1#820001)