Paul King created GROOVY-12761:
----------------------------------

             Summary: SC: code in an instance initializer block is compiled 
dynamically (method calls since 5.0.0; anonymous inner classes created there 
since 6.0.0)
                 Key: GROOVY-12761
                 URL: https://issues.apache.org/jira/browse/GROOVY-12761
             Project: Groovy
          Issue Type: Bug
          Components: Static compilation
    Affects Versions: 6.0.0, 5.1.3
            Reporter: Paul King
            Assignee: Paul King


In a {{@CompileStatic}} class, method calls in an instance initializer block 
are dispatched dynamically, and since 6.0.0 so is the whole of any anonymous 
inner class created in such a block. The same code in a constructor or a field 
initializer is statically compiled. 4.0.x compiles all of it statically.

This shows up as:
* overloads chosen from the run-time argument types instead of the static ones
* {{IncompatibleClassChangeError}} when a closure in the block calls a method 
on a non-Groovy delegate (GROOVY-12581)

{code:groovy}
class V {
    String kind
    V(Object o) { kind = 'Object' }
    V(String s) { kind = 'String' }
    static Object obj() { 'x' }
    static String m(Object o) { 'Object' }
    static String m(String s) { 'String' }
}
interface Greeter { String greet() }

@groovy.transform.CompileStatic
class C {
    List<String> fromBlock = [], fromCtor = [];
    {
        fromBlock << V.m(V.obj())
        fromBlock << new V(V.obj()).kind
        fromBlock << new Greeter() { String greet() { new V(V.obj()).kind + 
V.m(V.obj()) } }.greet()
        fromBlock << { -> new V(V.obj()).kind }()
    }
    C() {
        fromCtor << V.m(V.obj())
        fromCtor << new V(V.obj()).kind
        fromCtor << new Greeter() { String greet() { new V(V.obj()).kind + 
V.m(V.obj()) } }.greet()
        fromCtor << { -> new V(V.obj()).kind }()
    }
}
def c = new C()
println "block ${c.fromBlock}  ctor ${c.fromCtor}"

@groovy.transform.CompileStatic
class D { String r; { r = new StringBuilder('a').with { append('b') 
}.toString() } }
println new D().r
{code}

The constructor prints {{[Object, Object, ObjectObject, Object]}} on every 
version. The initializer block prints:

||Version||static call||new||anonymous class||new in a closure||{{D}}||
|4.0.33|Object|Object|ObjectObject|Object|ab|
|5.0.8, 
5.1.3|*String*|Object|ObjectObject|Object|*IncompatibleClassChangeError*|
|6.0.0-beta-3, 
6.0.0-RC-1|*String*|Object|ObjectObject|Object|*IncompatibleClassChangeError*|
|6.0.0-RC-3, 6.0.0, 
6.0.1-SNAPSHOT|*String*|Object|*StringString*|Object|*IncompatibleClassChangeError*|

h3. Cause

{{StaticTypeCheckingVisitor.visitObjectInitializerStatements}} type checks the 
initializer blocks under a placeholder {{ConstructorNode}} (GROOVY-5450) that 
has no declaring class. {{StaticCompilationVisitor.isStaticallyCompiled}} 
therefore returns {{false}} for it.

* Since GROOVY-6925 (5.0.0), 
{{StaticCompilationVisitor.visitMethodCallExpression}} returns early when 
{{isStaticallyCompiled(getEnclosingDeclaration())}} is {{false}}. The calls in 
the block get no method target, and the "Target method for method call 
expression hasn't been set" check is skipped as well. The writer then emits 
dynamic calls ({{ScriptBytecodeAdapter.invokeMethodN}}, or 
{{invokeMethodOnCurrentN}} for an implicit-this call routed to a closure 
delegate, which assumes a {{GroovyObject}} receiver and gives the 
{{IncompatibleClassChangeError}}).
* Since GROOVY-12363 (6.0.0-RC-2), an anonymous inner class takes its 
{{STATIC_COMPILE_NODE}} flag from {{isStaticallyCompiled(enclosingMethod)}}. 
For a class created in an initializer block that is the placeholder, so the 
whole anonymous class is compiled dynamically.
* Constructor calls written directly in the block are not affected, because 
{{StaticInvocationWriter.writeInvokeConstructor}} reads the 
{{DIRECT_METHOD_CALL_TARGET}} metadata itself. Closures in the block are not 
affected either.

h3. Proposed fix

Give the placeholder its declaring class:

{code:java}
ConstructorNode init = new ConstructorNode(0, null, null, new 
BlockStatement(node.getObjectInitializerStatements(), null));
init.setDeclaringClass(node);
{code}

With this change the initializer block makes the same choices as the 
constructor in all columns above, and {{D}} prints {{ab}}. The 
{{groovy.transform.stc}}, {{org.codehaus.groovy.classgen.asm.sc}}, {{bugs}} and 
{{groovy.bugs}} test packages pass on master (6,882 tests). Two tests are 
proposed in {{ClosuresSTCTest}} (also run statically compiled by 
{{ClosuresStaticCompileTest}}): the closure-delegate case from GROOVY-12581, 
and a check that the initializer block and the constructor select the same 
overloads. Both fail without the change.

The placeholder code is the same on {{GROOVY_5_1_X}}, {{GROOVY_6_0_X}} and 
master, so the change applies to each.

h3. Compatibility

This restores 4.0 behaviour, but code in an initializer block of a 
{{@CompileStatic}} class that has relied on dynamic dispatch since 5.0 will 
change. It may select a different overload, and meta-class or category changes 
will no longer apply to those calls. It deserves a COMPATIBILITY note on any 
line it goes to.

h3. Relationship to GROOVY-12581

GROOVY-12581 reports the {{IncompatibleClassChangeError}} symptom. PR #3107 
fixes that symptom in {{StaticInvocationWriter}} by switching to the 
{{invokeMethod}} adapter once the receiver is replaced. That is not needed once 
the calls have targets again, and on its own it leaves the calls dynamic. 
Choice of fix for 5.1.x pending team review.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to