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)

Reply via email to