GROOVY-12240: Initialize argument-less enum constants with a direct constructor call - #2772
Open
codeconsole wants to merge 1 commit into
Open
GROOVY-12240: Initialize argument-less enum constants with a direct constructor call#2772codeconsole wants to merge 1 commit into
codeconsole wants to merge 1 commit into
Conversation
…onstructor call
EnumVisitor routes every enum constant through the synthetic $INIT(Object[])
helper, whose body is a spread constructor call. That compiles to
ScriptBytecodeAdapter.despreadList plus selectConstructorAndTransformArguments,
so the meta class picks the constructor at run time by reflecting over
getDeclaredConstructors(); the static initializer in turn reaches $INIT itself
through a dynamic call site.
For a constant that supplies no arguments of its own the arguments are only the
compiler-supplied name and ordinal, both of which are known at compile time.
Emit a direct call to the (String,int) constructor of the enum for those, so
the static initializer needs neither the meta class nor reflection. That
matters where reflection over the enum is not available: in a GraalVM native
image built without reachability metadata for the enum, getDeclaredConstructors()
is empty and class initialization fails with
groovy.lang.GroovyRuntimeException: Could not find matching constructor
for: com.example.MyEnum(String, Integer)
@CompileStatic does not help, because it is $INIT's own body that is dynamic.
The direct call is used only when every constant of the enum is a plain
identifier. Constants with arguments, with named arguments or with a class
body keep the $INIT path, as do abstract enums, the classes generated for
constant bodies and enums without a constructor that takes just the name and
the ordinal. $INIT is still generated in every case, and only the bytecode
generator is shown the direct call, so type checking and every other visitor
see the same tree as before.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2772 +/- ##
==================================================
+ Coverage 69.9869% 69.9971% +0.0102%
- Complexity 35529 35553 +24
==================================================
Files 1557 1558 +1
Lines 131686 131724 +38
Branches 24174 24178 +4
==================================================
+ Hits 92163 92203 +40
+ Misses 31189 31174 -15
- Partials 8334 8347 +13
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 4d53a4e Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://issues.apache.org/jira/browse/GROOVY-12240
Motivation
EnumVisitorcreates every enum constant through a synthetic helper:this(*para)is a spread constructor call, soInvocationWriter.makeDirectConstructorCallrefuses it — it bails onSpreadExpression, and again on!controller.isConstructor()— and the body compiles toScriptBytecodeAdapter.despreadListplusselectConstructorAndTransformArguments. The meta class then picks the constructor at run time by reflecting overgetDeclaredConstructors(). The static initializer reaches$INITitself through a dynamic call site.For
enum Colors { RED, GREEN, BLUE }that is the whole 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 isn't available the class simply cannot initialize. In a GraalVM native image built without reachability metadata for the enum,
getDeclaredConstructors()returns nothing and class initialization throws(note the boxed
Integer— the ordinal has been throughObject[]). This kills the application in a static initializer before any user code runs.@CompileStaticdoes not help: Groovy already compiles the call site statically (StaticTypeCheckingVisitor, GROOVY-10845); it is$INIT's own body that is necessarily dynamic.Change
When every constant of an enum is a plain identifier, the arguments are provably
[name, ordinal], and the static initializer now calls the enum's(String,int)constructor directly.The same shape javac emits for a Java enum: 21 bytes and two indy call sites per constant become 13 bytes and none.
When the new path applies
Only when all of the following hold:
EnumConstantClassNodeinner classes;(String,int)constructor actually exists.Anything else keeps the existing
$INITpath.How the fallback works
EnumConstantInitis aBytecodeExpressionthat holds the original$INITcall. It hands that call to every visitor exceptAsmClassGenerator, and hands it toAsmClassGeneratortoo when the expected constructor isn't present. So type checking, scope resolution and AST transforms see exactly the tree they see today, and a shape that cannot use the direct call degrades to today's bytecode rather than to a different failure.A concrete case:
@TupleConstructor(defaults = false) enum E { ONE; String value }compiles today and fails at class initialization withCould not find matching constructor. It has no(String,int)constructor, so it keeps$INITand keeps failing in exactly that way. There is a test for it.$INITis unchanged and still generated for every enum.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
$INITpath whichever Groovy runs it. I confirmed this 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 withCould not find matching constructoruntil 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 relaxingInvocationWriter.makeDirectConstructorCallto work outside a constructor, which is a much wider change and deliberately left alone.One semantic narrowing worth reviewer attention: a plain enum's
<clinit>no longer touches the meta class, so anything relying on intercepting an enum constructor viaExpandoMetaClassbefore class initialization would no longer see it. I believe this is unreachable in practice — the enum is initialized once, before any such hook could be installed, and Java enums offer no equivalent — but it is a real change.Tests
EnumConstantInitBytecodeTest(new) — asserts the emitted<clinit>instruction sequence for the direct case, that$INITis still generated with its usual body, and that constants with arguments / named arguments / a body / a mix, and the missing-constructor case, all keep$INIT.gls.enums.EnumTest— behaviour coverage for the new path: values, ordinals,valueOf,next/previous,MIN_VALUE/MAX_VALUE, ranges,EnumSet,compareTo, serialization identity, and enums with an explicit no-arg or all-defaults constructor.Also verified by hand across packaged, nested and doubly-nested enums,
@CompileStatic,@TypeChecked, and a 400-constant enum (correcticonst/bipush/sipushselection at 5/6/127/128/399)../gradlew :testpasses in full: 16,548 tests, 0 failures.Related to GROOVY-12234.