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)