You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/08/07 07:37:08 UTC
(groovy) branch GROOVY_5_1_X updated: GROOVY-12235: Interface default method without dynamic calls references unemitted call-site helper under indy=false
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_5_1_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY_5_1_X by this push:
new 2916f63d2b GROOVY-12235: Interface default method without dynamic calls references unemitted call-site helper under indy=false
2916f63d2b is described below
commit 2916f63d2b3d982b5d9da46b03775c6ae76956f3
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Aug 6 08:28:39 2026 +1000
GROOVY-12235: Interface default method without dynamic calls references unemitted call-site helper under indy=false
---
.../groovy/classgen/AsmClassGenerator.java | 11 ++--
.../groovy/classgen/asm/CallSiteWriter.java | 10 ++++
src/test/groovy/bugs/Groovy12235.groovy | 66 ++++++++++++++++++++++
3 files changed, 82 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index 5e233297ad..76421ca8d7 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -1480,11 +1480,12 @@ public class AsmClassGenerator extends ClassGenerator {
protected void createInterfaceSyntheticStaticFields() {
var icl = controller.getInterfaceClassLoadingClass();
- // GROOVY-11982: also materialise the helper when there are call sites
- // (e.g. dynamic code in default methods under indy=false), otherwise
- // CallSiteWriter routes INVOKESTATIC at a class that was never emitted
- boolean hasCallSites = !controller.getCallSiteWriter().getCallSites().isEmpty();
- if (referencedClasses.isEmpty() && !hasCallSites) return;
+ // GROOVY-11982: also materialise the helper when a call-site array
+ // prologue was emitted (every default/static method body under
+ // indy=false, even one that registers no call sites -- GROOVY-12235),
+ // otherwise CallSiteWriter routes INVOKESTATIC at a class never emitted
+ boolean hasCallSitePrologue = controller.getCallSiteWriter().isPrologueEmitted();
+ if (referencedClasses.isEmpty() && !hasCallSitePrologue) return;
addInnerClass(icl);
for (Map.Entry<String, ClassNode> entry : referencedClasses.entrySet()) {
// generate a field node
diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java
index ed4bfd63e0..b5dc4164d7 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java
@@ -101,6 +101,7 @@ public class CallSiteWriter {
public static final String CONSTRUCTOR = "<$constructor$>";
private final List<String> callSites = new ArrayList<String>(32);
private int callSiteArrayVarIndex = -1;
+ private boolean prologueEmitted;
private final WriterController controller;
public CallSiteWriter(WriterController wc) {
@@ -122,9 +123,18 @@ public class CallSiteWriter {
mv.visitMethodInsn(INVOKESTATIC, controller.getClassName(), GET_CALLSITE_METHOD, GET_CALLSITE_DESC, false);
controller.getOperandStack().push(CALLSITE_ARRAY_TYPE);
callSiteArrayVarIndex = controller.getCompileStack().defineTemporaryVariable("$local$callSiteArray", CALLSITE_ARRAY_TYPE, true);
+ prologueEmitted = true;
}
}
+ /**
+ * Returns true if a call-site array loading prologue has been emitted for
+ * at least one method of the current class.
+ */
+ public boolean isPrologueEmitted() {
+ return prologueEmitted;
+ }
+
public void generateCallSiteArray() {
if (!controller.getClassNode().isInterface()) {
controller.getClassVisitor().visitField(MOD_PRIVSS, CALLSITE_FIELD, REF_DESC, null, null);
diff --git a/src/test/groovy/bugs/Groovy12235.groovy b/src/test/groovy/bugs/Groovy12235.groovy
new file mode 100644
index 0000000000..8cfea49eb9
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy12235.groovy
@@ -0,0 +1,66 @@
+/*
+ * 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 bugs
+
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.junit.jupiter.api.Test
+
+/**
+ * Follow-up to GROOVY-11982: under {@code indy=false} the call-site array
+ * prologue ({@code INVOKESTATIC $getCallSiteArray()}) is emitted at the top
+ * of every method body before the body is visited, so an interface method
+ * whose body registers no call sites (e.g. {@code return null}) still
+ * references the synthetic helper class ({@code MyInterface$1}) that owns
+ * the prologue for interfaces. The helper must be materialised whenever a
+ * prologue was emitted — not only when named call sites were registered —
+ * otherwise the first invocation throws
+ * {@code NoClassDefFoundError: MyInterface$1}.
+ */
+final class Groovy12235 {
+
+ @Test
+ void testInterfaceDefaultMethodWithoutDynamicCodeNonIndy() {
+ CompilerConfiguration config = new CompilerConfiguration()
+ config.optimizationOptions.put('indy', false)
+ new GroovyShell(config).evaluate '''
+ interface MyInterface {
+ default Object defaultValue() {
+ return null
+ }
+ }
+ class MyImpl implements MyInterface {
+ }
+ assert new MyImpl().defaultValue() == null
+ '''
+ }
+
+ @Test
+ void testInterfaceStaticMethodWithoutDynamicCodeNonIndy() {
+ CompilerConfiguration config = new CompilerConfiguration()
+ config.optimizationOptions.put('indy', false)
+ new GroovyShell(config).evaluate '''
+ interface Util {
+ static Object nothing() {
+ return null
+ }
+ }
+ assert Util.nothing() == null
+ '''
+ }
+}