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
+        '''
+    }
+}