You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "ASF GitHub Bot (Jira)" <ji...@apache.org> on 2026/08/07 20:57:00 UTC
[jira] [Commented] (GROOVY-12240) Initialize argument-less enum constants with a direct constructor call
[ https://issues.apache.org/jira/browse/GROOVY-12240?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18102941#comment-18102941 ]
ASF GitHub Bot commented on GROOVY-12240:
-----------------------------------------
codeconsole opened a new pull request, #2772:
URL: https://github.com/apache/groovy/pull/2772
https://issues.apache.org/jira/browse/GROOVY-12240
### Motivation
`EnumVisitor` creates every enum constant through a synthetic helper:
```groovy
def $INIT(Object[] para) {
return this(*para)
}
```
`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 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
```
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.example.MyEnum(String, Integer)
```
(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.
### 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.
```
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;
```
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:
- every constant is a plain identifier — no arguments, no named arguments, no class body;
- the enum is not abstract and has no `EnumConstantClassNode` inner classes;
- the enum declares no constructor, or declares one callable with no user-supplied argument;
- and, checked at bytecode generation once every transform has run, a `(String,int)` constructor actually exists.
Anything else keeps the existing `$INIT` path.
### How the fallback works
`EnumConstantInit` is a `BytecodeExpression` that holds the original `$INIT` call. It hands that call to every visitor except `AsmClassGenerator`, and hands it to `AsmClassGenerator` too 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 with `Could not find matching constructor`. It has no `(String,int)` constructor, so it keeps `$INIT` and keeps failing in exactly that way. There is a test for it.
`$INIT` is 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 `$INIT` path 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 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 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 via `ExpandoMetaClass` before 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 `$INIT` is 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 (correct `iconst`/`bipush`/`sipush` selection at 5/6/127/128/399).
`./gradlew :test` passes in full: 16,548 tests, 0 failures.
Related to GROOVY-12234.
> 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
> Priority: Minor
>
> {{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)