diff --git a/src/main/java/org/codehaus/groovy/classgen/EnumConstantInit.java b/src/main/java/org/codehaus/groovy/classgen/EnumConstantInit.java
new file mode 100644
index 00000000000..ff8c94b417e
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/classgen/EnumConstantInit.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.classgen;
+
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.GroovyCodeVisitor;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.classgen.asm.BytecodeHelper;
+import org.objectweb.asm.MethodVisitor;
+
+import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
+import static org.objectweb.asm.Opcodes.DUP;
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+import static org.objectweb.asm.Opcodes.NEW;
+
+/**
+ * Initializes an enum constant that supplies no arguments of its own by calling the
+ * {@code (String,int)} constructor of the enum directly.
+ *
+ * The {@code $INIT} helper generated by {@link EnumVisitor} spreads an {@code Object[]}
+ * over the constructors of the enum, which means the meta class selects the constructor
+ * at run time by reflecting over {@code getDeclaredConstructors()}. Where that reflection
+ * is unavailable, e.g. in a GraalVM native image for which the enum was not registered,
+ * the static initializer of the enum fails. When the arguments are known at compile time
+ * to be exactly the name and the ordinal, the constructor can be selected there instead.
+ *
+ * Only the bytecode generator sees the direct call: every other visitor is given the
+ * {@code $INIT} call, which is also emitted if the expected constructor turns out not to
+ * be present once all transforms have run.
+ */
+final class EnumConstantInit extends BytecodeExpression {
+
+ private static final Parameter[] NAME_AND_ORDINAL = params(param(ClassHelper.STRING_TYPE, "name"), param(int_TYPE, "ordinal"));
+
+ private final ClassNode enumClass;
+ private final String name;
+ private final int ordinal;
+ private final Expression initCall;
+
+ EnumConstantInit(final ClassNode enumClass, final String name, final int ordinal, final Expression initCall) {
+ super(enumClass.getPlainNodeReference());
+ this.enumClass = enumClass;
+ this.name = name;
+ this.ordinal = ordinal;
+ this.initCall = initCall;
+ }
+
+ @Override
+ public String getText() {
+ return initCall.getText();
+ }
+
+ @Override
+ public void visit(final GroovyCodeVisitor visitor) {
+ if (visitor instanceof AsmClassGenerator && enumClass.getDeclaredConstructor(NAME_AND_ORDINAL) != null) {
+ super.visit(visitor); // i.e. visitBytecodeExpression(this)
+ } else {
+ initCall.visit(visitor);
+ }
+ }
+
+ @Override
+ public void visit(final MethodVisitor mv) {
+ String owner = BytecodeHelper.getClassInternalName(enumClass);
+ mv.visitTypeInsn(NEW, owner);
+ mv.visitInsn(DUP);
+ mv.visitLdcInsn(name);
+ BytecodeHelper.pushConstant(mv, ordinal);
+ mv.visitMethodInsn(INVOKESPECIAL, owner, "", "(Ljava/lang/String;I)V", false);
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java b/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java
index 7eb22de349d..22bf9334cd3 100644
--- a/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java
+++ b/src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java
@@ -264,6 +264,7 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final
// static init
List fields = enumClass.getFields();
+ boolean directInit = canInitDirectly(enumClass, fields);
List arrayInit = new ArrayList<>();
List block = new ArrayList<>();
int index = -1;
@@ -320,7 +321,9 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final
}
}
arrayInit.add(fieldX(field));
- block.add(assignS(fieldX(field), callX(enumType, "$INIT", args)));
+ Expression init = callX(enumType, "$INIT", args);
+ if (directInit) init = new EnumConstantInit(enumClass, field.getName(), index, init);
+ block.add(assignS(fieldX(field), init));
}
if (!isAIC) {
@@ -338,6 +341,31 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final
enumClass.addStaticInitializerStatements(block, true);
}
+ /**
+ * Determines whether the constants of the given enum can be initialized with a direct
+ * constructor call rather than with a call to the synthetic {@code $INIT} helper.
+ *
+ * This is only the case when every constant is a plain identifier and so supplies no
+ * arguments of its own; the constructor arguments are then known to be exactly the
+ * compiler-supplied name and ordinal. Constants declared with arguments, with named
+ * arguments or with a class body keep the {@code $INIT} path, as do enums whose own
+ * constructor cannot accept just the name and the ordinal.
+ *
+ * @param enumClass the enum being completed
+ * @param fields the fields of {@code enumClass}, before any initial value is cleared
+ * @return {@code true} if a direct constructor call may be attempted
+ */
+ private static boolean canInitDirectly(final ClassNode enumClass, final List fields) {
+ // an abstract enum or one that is extended has constants with a body, i.e. subclasses
+ if (isAnonymousInnerClass(enumClass) || enumClass.isAbstract() || !isNotExtended(enumClass)) return false;
+ // GROOVY-10811: a declared constructor must be callable with no user-supplied argument
+ if (!enumClass.getDeclaredConstructors().isEmpty() && !hasNoArgConstructor(enumClass)) return false;
+ for (FieldNode field : fields) {
+ if (field.isEnum() && field.getInitialExpression() != null) return false;
+ }
+ return true;
+ }
+
private void addError(final AnnotatedNode an, final String msg) {
getSourceUnit().getErrorCollector().addErrorAndContinue(
new SyntaxErrorMessage(
diff --git a/src/test/groovy/gls/enums/EnumTest.groovy b/src/test/groovy/gls/enums/EnumTest.groovy
index dc5b92085d3..c28aea1f9b9 100644
--- a/src/test/groovy/gls/enums/EnumTest.groovy
+++ b/src/test/groovy/gls/enums/EnumTest.groovy
@@ -959,6 +959,58 @@ final class EnumTest extends CompilableTestSupport {
}
'''
}
+
+ // constants that supply no arguments of their own are created with a direct
+ // constructor call instead of through the synthetic $INIT helper
+ @Test
+ void testConstantsWithoutArguments() {
+ assert Weekday.values()*.name() == ['MON', 'TUE', 'WED']
+ assert Weekday.values()*.ordinal() == [0, 1, 2]
+ assert Weekday.valueOf('TUE') == Weekday.TUE
+ assert Weekday.TUE.declaringClass == Weekday
+ assert Weekday.MIN_VALUE == Weekday.MON
+ assert Weekday.MAX_VALUE == Weekday.WED
+ assert Weekday.MON.next() == Weekday.TUE
+ assert Weekday.MON.previous() == Weekday.WED
+ assert Weekday.TUE in (Weekday.MON..Weekday.WED)
+ assert EnumSet.allOf(Weekday).size() == 3
+ assert Weekday.MON.compareTo(Weekday.WED) < 0
+ }
+
+ @Test
+ void testConstantsWithoutArgumentsAreSerializable() {
+ def buffer = new ByteArrayOutputStream()
+ new ObjectOutputStream(buffer).writeObject(Weekday.TUE)
+ def restored = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject()
+ assert restored.is(Weekday.TUE)
+ }
+
+ @Test
+ void testConstantsWithoutArgumentsRunTheDeclaredConstructor() {
+ assert Tagged.values()*.tag == ['tagged', 'tagged']
+ }
+
+ @Test
+ void testConstantsWithoutArgumentsWhenConstructorHasDefaults() {
+ assert Defaulted.values()*.label == ['none', 'none']
+ }
+}
+
+enum Weekday {
+ MON, TUE, WED
+}
+
+enum Tagged {
+ ALPHA, BETA
+ private final String tag
+ Tagged() { tag = 'tagged' }
+ String getTag() { tag }
+}
+
+enum Defaulted {
+ ONE, TWO
+ final String label
+ Defaulted(String label = 'none') { this.label = label }
}
enum UsCoin {
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/asm/EnumConstantInitBytecodeTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/asm/EnumConstantInitBytecodeTest.groovy
new file mode 100644
index 00000000000..e900a1e6e3b
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/EnumConstantInitBytecodeTest.groovy
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.classgen.asm
+
+import org.junit.jupiter.api.Test
+
+/**
+ * Checks how the static initializer of an enum creates its constants.
+ *
+ * A constant that supplies no arguments of its own is created with a direct call to the
+ * {@code (String,int)} constructor of the enum. Every other constant keeps going through
+ * the synthetic {@code $INIT} helper, which spreads an {@code Object[]} over the
+ * constructors of the enum and so has the meta class select one reflectively.
+ */
+final class EnumConstantInitBytecodeTest extends AbstractBytecodeTestCase {
+
+ private List staticInitializerOf(final String source) {
+ compile(method: '', classNamePattern: 'E', source)
+ bodyOf('static ()V')
+ }
+
+ private List bodyOf(final String header) {
+ def all = sequence.instructions
+ int start = all.findIndexOf { it == header }
+ assert start >= 0: "method not found: $header\n$sequence"
+ int end = all.findIndexOf(start) { it.startsWith('MAXSTACK') }
+ all[(start + 1).. code) {
+ assert code.join('\n').contains([
+ 'NEW E',
+ 'DUP',
+ 'LDC "ONE"',
+ 'ICONST_0',
+ 'INVOKESPECIAL E. (Ljava/lang/String;I)V',
+ 'PUTSTATIC E.ONE : LE;'
+ ].join('\n'))
+ assert !code.any { it.contains('$INIT') }
+ assert !code.any { it.contains('selectConstructorAndTransformArguments') }
+ }
+
+ private static void assertInitHelperCall(final List code) {
+ assert code.any { it.contains('$INIT') }
+ assert !code.any { it.startsWith('NEW E') }
+ }
+
+ @Test
+ void testConstantsWithoutArgumentsCallConstructorDirectly() {
+ def code = staticInitializerOf '''
+ enum E { ONE, TWO }
+ '''
+ assertDirectConstructorCall(code)
+ }
+
+ @Test
+ void testInitHelperIsStillGenerated() {
+ staticInitializerOf '''
+ enum E { ONE, TWO }
+ '''
+ def header = sequence.instructions.find { it.contains(' $INIT([Ljava/lang/Object;)LE;') }
+ assert header != null
+ assert bodyOf(header).any { it.contains('selectConstructorAndTransformArguments') }
+ }
+
+ @Test
+ void testExplicitNoArgConstructorCallsConstructorDirectly() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE, TWO
+ private final String tag
+ E() { tag = 'x' }
+ }
+ '''
+ assertDirectConstructorCall(code)
+ }
+
+ @Test
+ void testConstructorWithDefaultsCallsConstructorDirectly() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE, TWO
+ final String tag
+ E(String tag = 'x') { this.tag = tag }
+ }
+ '''
+ assertDirectConstructorCall(code)
+ }
+
+ @Test
+ void testCompileStaticConstantsCallConstructorDirectly() {
+ def code = staticInitializerOf '''
+ @groovy.transform.CompileStatic
+ enum E { ONE, TWO }
+ '''
+ assertDirectConstructorCall(code)
+ }
+
+ @Test
+ void testConstantsWithArgumentsUseInitHelper() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE(1), TWO(2)
+ final int value
+ E(int value) { this.value = value }
+ }
+ '''
+ assertInitHelperCall(code)
+ }
+
+ @Test
+ void testConstantsWithNamedArgumentsUseInitHelper() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE(value: 1), TWO(value: 2)
+ int value
+ }
+ '''
+ assertInitHelperCall(code)
+ }
+
+ @Test
+ void testConstantsWithABodyUseInitHelper() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE { int twice() { 2 } },
+ TWO { int twice() { 4 } }
+ abstract int twice()
+ }
+ '''
+ assertInitHelperCall(code)
+ }
+
+ @Test
+ void testMixOfConstantsWithAndWithoutArgumentsUsesInitHelper() {
+ def code = staticInitializerOf '''
+ enum E {
+ ONE, TWO(2)
+ final int value
+ E(int value = 0) { this.value = value }
+ }
+ '''
+ assertInitHelperCall(code)
+ }
+
+ // the transform leaves the enum without a constructor that takes just the name and
+ // the ordinal, so the direct call is not available and $INIT is emitted as before
+ @Test
+ void testMissingNameAndOrdinalConstructorUsesInitHelper() {
+ def code = staticInitializerOf '''
+ @groovy.transform.TupleConstructor(defaults = false)
+ enum E {
+ ONE
+ String value
+ }
+ '''
+ assertInitHelperCall(code)
+ }
+}