Scott Murphy Heiberg created GROOVY-12240:
---------------------------------------------
Summary: Initialize argument-less enum constants with a direct
constructor call
Key: GROOVY-12240
URL: https://issues.apache.org/jira/browse/GROOVY-12240
Project: Groovy
Issue Type: Improvement
Components: Compiler
Affects Versions: 5.0.8
Reporter: Scott Murphy Heiberg
{{EnumVisitor}} creates every enum constant through a synthetic helper:
{code:groovy}
def $INIT(Object[] para) {
return this(*para)
}
{code}
{{this(*para)}} is a spread constructor call, so
{{InvocationWriter.makeDirectConstructorCall}} refuses it — it bails on
{{SpreadExpression}}, and again on {{!controller.isConstructor()}} — and the
body compiles to {{ScriptBytecodeAdapter.despreadList}} plus
{{selectConstructorAndTransformArguments}}. The meta class then picks the
constructor at run time by reflecting over {{getDeclaredConstructors()}}. The
static initializer reaches {{$INIT}} itself through a dynamic call site.
For {{enum Colors { RED, GREEN, BLUE }}} that is the entire constant-creation
path, even though the only arguments are the compiler-supplied name and
ordinal, both known at compile time.
Where reflection over the enum is not available, the class cannot initialize at
all. In a GraalVM native image built without reachability metadata for the
enum, {{getDeclaredConstructors()}} returns nothing and class initialization
throws:
{noformat}
groovy.lang.GroovyRuntimeException: Could not find matching constructor for:
com.example.MyEnum(String, Integer)
{noformat}
(note the boxed {{Integer}} — the ordinal has been through {{Object[]}}). This
kills the application in a static initializer before any user code runs.
{{@CompileStatic}} does not help. Groovy already compiles the *call site*
statically (StaticTypeCheckingVisitor, GROOVY-10845); it is {{$INIT}}'s own
body that is necessarily dynamic.
h3. Proposal
When every constant of an enum is a plain identifier, the arguments are
provably {{[name, ordinal]}}, and the static initializer can call the enum's
{{(String, int)}} constructor directly:
{noformat}
static {}; static {};
0: ldc // class Colors 0: new // class Colors
2: ldc // String RED 3: dup
4: iconst_0 4: ldc // String RED
5: invokestatic Integer.valueOf 6: iconst_0
8: invokedynamic invoke:(Class;String; 7: invokespecial
"<init>":(Ljava/lang/String;I)V
Integer;)Object; 10: putstatic Field
RED:LColors;
13: invokedynamic cast:(Object;)LColors;
18: putstatic Field RED:LColors;
{noformat}
The same shape javac emits for a Java enum: 21 bytes and two invokedynamic call
sites per constant become 13 bytes and none.
Anything else — constants with arguments, named-argument form, anonymous
constant bodies, or a mixture — keeps the existing {{$INIT}} path, as does any
enum lacking a {{(String, int)}} constructor (checked at bytecode generation,
after {{Verifier}} has run). {{$INIT}} itself is unchanged and still generated
for every enum.
h3. Scope, stated honestly
This is a *compile-time* change. It only helps code compiled by a Groovy that
carries the fix; bytecode already compiled by an earlier Groovy keeps its
{{$INIT}} path whichever Groovy runs it. Confirmed by building a GraalVM native
image of an application against a patched Groovy: the framework's own enums,
compiled by an earlier Groovy, still failed with {{Could not find matching
constructor}} until reachability metadata was restored.
Enums whose constants take arguments ({{RED(255, 0, 0)}}) are not addressed and
still require reachability metadata in a native image. Fixing those would mean
relaxing {{InvocationWriter.makeDirectConstructorCall}} to work outside a
constructor, which is a much wider change.
Related to GROOVY-12234.
I have a patch with tests ({{./gradlew :test}} passes in full: 16,548 tests, 0
failures) and will open a pull request against this issue.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)