Paul King created GROOVY-12710:
----------------------------------
Summary: Compiler overflows the stack on static final fields whose
initializers refer to each other
Key: GROOVY-12710
URL: https://issues.apache.org/jira/browse/GROOVY-12710
Project: Groovy
Issue Type: Bug
Reporter: Paul King
h2. Problem
When {{static final}} fields have initializers that refer back to each other,
directly or through other fields, the compiler throws
{{java.lang.StackOverflowError}} and the compile is aborted. This happens with
ordinary dynamic Groovy; no AST transform or type checking is involved.
{code:groovy}
class Cycle {
static final String R = S
static final String S = R
}
println Cycle.R
{code}
Actual:
{noformat}
Caught: java.lang.StackOverflowError
at
org.apache.groovy.ast.tools.ExpressionUtils.transformInlineConstants(ExpressionUtils.java)
at
org.apache.groovy.ast.tools.ExpressionUtils.transformInlineConstants(ExpressionUtils.java)
...
at
org.codehaus.groovy.classgen.Verifier.addFieldInitialization(Verifier.java)
at
org.codehaus.groovy.classgen.Verifier.addInitialization(Verifier.java)
at org.codehaus.groovy.classgen.Verifier.visitClass(Verifier.java)
{noformat}
Every variant below overflows, on 2.5.23, 3.0.25, 4.0.32, 5.1.3, 6.0.0 and
master, compiled dynamically, with {{@TypeChecked}} or with {{@CompileStatic}}.
For comparison, the Java equivalents with javac:
||Fields||Groovy||javac, then running||
|{{static final String R = S; static final String S =
R}}|StackOverflowError|"illegal forward reference"|
|{{static final String R = Cycle.S; static final String S =
Cycle.R}}|StackOverflowError|compiles; prints {{null null}}|
|{{class A { static final String X = B.Y }}}, {{class B { static final String Y
= A.X }}}|StackOverflowError|compiles; prints {{null null}}|
|{{interface I { String R = J.S }}}, {{interface J { String S = I.R
}}}|StackOverflowError|compiles; prints {{null null}}|
|{{static final String R = R}}|StackOverflowError|"self-reference in
initializer"|
|{{static final String R = Cycle.R}}|StackOverflowError|compiles; prints
{{null}}|
|{{static final int A = Cycle.B + 1; static final int B = Cycle.A +
1}}|StackOverflowError|compiles; prints {{1 2}}|
Using such a field as an annotation attribute ({{@Ann(Cycle.R)}}) also
overflows.
Instance fields, and {{static}} fields that aren't {{final}}, with the same
cyclic initializers compile and run, initialized in declaration order.
h2. Cause
{{ExpressionUtils.transformInlineConstants(Expression, ClassNode)}} inlines the
value of a {{static final}} field by calling itself on that field's
initializer, both for a plain name ({{S}}) and for a qualified one
({{Cycle.S}}). It keeps no record of the fields already being inlined, so a
chain of initializers that leads back to an earlier field recurses until the
stack overflows. The single-argument {{transformInlineConstants(Expression)}}
delegates to it for fields, so it loops the same way.
Many parts of the compiler use these methods, so the cycle can be reached from
several places:
* class generation ({{Verifier.addFieldInitialization}}), which is where the
plain example above fails;
* annotation handling ({{AnnotationVisitor}}, {{AnnotationConstantsVisitor}});
* {{ResolveVisitor}} and {{StaticImportVisitor}};
* {{StaticCompilationTransformer}};
* the joint-compilation stub generator;
* groovydoc.
h2. Proposed fix
Keep track of the fields being inlined during one call, and when a field is
reached again, stop and return the expression unchanged. Its value is then not
a compile-time constant, so the field is initialized at runtime in declaration
order, as for the non-final case and as in Java. An annotation attribute that
refers to such a field then gets the usual error for a value that isn't a
constant, instead of overflowing.
Groovy accepts forward references to static fields, while javac rejects the
plain forward reference ({{R = S}}) and the plain self-reference ({{R = R}}).
Compiling all the forms, with the runtime values shown above, keeps Groovy
consistent with what it already does for non-final fields. Whether to report
the plain forms as errors instead, to match Java, is a separate decision.
h2. Tests
* Each row of the table above compiles, and gives the value javac's version
prints.
* An annotation attribute referring to a cyclic field reports a
constant-expression error and doesn't overflow.
* A non-cyclic chain ({{static final String R = S}}, {{static final String S =
'x'}}) is still inlined as a constant, for example still usable as an
annotation attribute.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)