Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
254c08c
adding a lock free MRU field to CacheableCallSite to avoid SoftRefere…
blackdrag May 20, 2026
1acf9b9
GROOVY-12023: add a PIC with configurable size for indy in fron of th…
blackdrag May 20, 2026
539e4a4
GROOVY-12023: add tests to ensure PIC functionality and fix one exist…
blackdrag May 20, 2026
b48a206
GROOVY-12023: adding documentation created by AI
blackdrag May 20, 2026
7cbbf97
GROOVY-12023: fix multithreading
blackdrag May 21, 2026
cb74b15
GROOVY-12023: further fixes for multi threading and fixing some minor…
blackdrag May 21, 2026
83d1cf4
move heavy method selection code out of synchronized block
blackdrag May 21, 2026
916340f
GROOVY-12023: avoid overhead through stream generation
blackdrag May 21, 2026
6122b9b
GROOVY-12023: remove exception handler where not required
blackdrag May 21, 2026
f314dc8
GROOVY-12023: optimize DGM handling by skipping their wrapper structu…
blackdrag May 23, 2026
3b211ad
GROOVY-12023: fix ScriptToTreeNodeAdapterTest failures - revert Cache…
blackdrag May 23, 2026
e12ad16
GROOVY-12023: restrict CachedClass path to catch exception only if me…
blackdrag May 23, 2026
c642935
GROOVY-12023: narrow exception handling to cater only for GroovyObjec…
blackdrag May 23, 2026
164311f
GROOVY-12023: adding missing licenses to new tests
blackdrag May 23, 2026
2ad2cc5
GROOVY-12023: improving inlining for DTT operations
blackdrag May 25, 2026
6df1d26
GROOVY-12023: fixing wrong isAssignableFrom check
blackdrag May 25, 2026
7747e96
fixing switchpoint related bug in PIC logic
blackdrag May 25, 2026
ebab001
GROOVY-12023: fix double switchpoint usage
blackdrag May 25, 2026
da154cf
GROOVY-12023: Move call-site flags into CacheableCallSite and fix spr…
blackdrag May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ The phase enum is the right anchor for any documentation that talks
about "when X happens during compilation". Quoting the phase names
verbatim keeps the reference precise; paraphrasing tends to drift.

## Runtime: invokedynamic (Indy)

Since Groovy 2.0, dynamic method dispatch can be performed using the `invokedynamic`
instruction. The core of this implementation lives in `org.codehaus.groovy.vmplugin.v8`.

| Class | Role |
|---|---|
| `IndyInterface` | Bootstrap methods and optimization lifecycle management. |
| `CacheableCallSite` | The stateful call site holding the PIC chain, MRU entry, and LRU cache. |
| `Selector` | Logic for finding the target method/property and constructing the guarded `MethodHandle`. |
| `MethodHandleWrapper` | Combines a `MethodHandle` with metadata like hit counts and target description. |

### Caching Hierarchy
To maximize performance, `CacheableCallSite` uses three levels of caching:
1. **PIC Chain (Level 1)**: A bounded chain of guarded handles in the call-site target (JIT-optimized).
2. **MRU Entry (Level 2)**: A lock-free volatile field for the most recent hit shape.
3. **LRU Cache (Level 3)**: A synchronized, soft-referenced map for megamorphic fallback.

Detailed technical documentation of this hierarchy can be found in the Javadoc of `CacheableCallSite`.

### Parser (phase 2)

- Grammar lives in `src/antlr/GroovyLexer.g4` and
Expand Down
613 changes: 613 additions & 0 deletions gradle/verification-metadata.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.io.IOException;
import java.io.Serial;
import java.io.Serializable;
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
Expand Down Expand Up @@ -70,6 +71,19 @@ public CachedClass getDeclaringClass() {
return declaringClass;
}

/**
* Returns a {@link MethodHandle} pointing directly to the underlying target method,
* or {@code null} if not available.
* <p>
* Generated DGM adapter classes override this to provide a pre-computed handle that
* avoids the boxing overhead of {@link #invoke(Object, Object[])}.
*
* @return a method handle for direct invocation, or {@code null}
*/
public MethodHandle getTargetMethodHandle() {
return null;
}

public static class Proxy extends GeneratedMetaMethod {
private volatile MetaMethod proxy;
private final String className;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ public class DefaultTypeTransformation {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static byte byteUnbox(final Object value) {
if (value instanceof Byte b) {
return b;
}
Number n = castToNumber(value, byte.class);
return n.byteValue();
}
Expand All @@ -111,9 +114,11 @@ public static byte byteUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to char
*/
public static char charUnbox(final Object value) {
if (value instanceof Character c) {
return c;
}
if (value == null) return '\u0000'; // GROOVY-11371
var ch = ShortTypeHandling.castToChar(value);
return ch;
return ShortTypeHandling.castToChar(value);
}

/**
Expand All @@ -126,6 +131,9 @@ public static char charUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static short shortUnbox(final Object value) {
if (value instanceof Short s) {
return s;
}
Number n = castToNumber(value, short.class);
return n.shortValue();
}
Expand All @@ -140,6 +148,9 @@ public static short shortUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static int intUnbox(final Object value) {
if (value instanceof Integer i) {
return i;
}
Number n = castToNumber(value, int.class);
return n.intValue();
}
Expand All @@ -153,6 +164,9 @@ public static int intUnbox(final Object value) {
* @return the boolean value
*/
public static boolean booleanUnbox(final Object value) {
if (value instanceof Boolean b) {
return b;
}
return castToBoolean(value);
}

Expand All @@ -166,6 +180,9 @@ public static boolean booleanUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static long longUnbox(final Object value) {
if (value instanceof Long l) {
return l;
}
Number n = castToNumber(value, long.class);
return n.longValue();
}
Expand All @@ -181,6 +198,9 @@ public static long longUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static float floatUnbox(final Object value) {
if (value instanceof Float f) {
return f;
}
if (value == null) return Float.NaN; // GROOVY-11371
Number n = castToNumber(value, float.class);
return n.floatValue();
Expand All @@ -197,6 +217,9 @@ public static float floatUnbox(final Object value) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static double doubleUnbox(final Object value) {
if (value instanceof Double d) {
return d;
}
if (value == null) return Double.NaN; // GROOVY-11371
Number n = castToNumber(value, double.class);
return n.doubleValue();
Expand Down Expand Up @@ -333,11 +356,15 @@ public static Number castToNumber(Object object) {
* @throws GroovyCastException if the object cannot be converted to a number
*/
public static Number castToNumber(Object object, Class type) {
if (object instanceof Number) {
return (Number) object;
if (object instanceof Number number) {
return number;
}
if (object instanceof Character) {
char c = (Character) object;
return castToNumberFallback(object, type);
}

private static Number castToNumberFallback(Object object, Class<?> type) {
if (object instanceof Character cObj) {
char c = cObj;
return (int) c;
}
if (object instanceof GString) {
Expand Down Expand Up @@ -370,6 +397,10 @@ public static boolean castToBoolean(Object object) {
return (Boolean) object;
}

return castToBooleanFallback(object);
}

private static boolean castToBooleanFallback(Object object) {
// if the object isn't null and no Boolean, try to call an asBoolean() method on the object
return (Boolean) InvokerHelper.invokeMethod(object, "asBoolean", InvokerHelper.EMPTY_ARGS);
}
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/org/codehaus/groovy/tools/DgmConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@

import static org.objectweb.asm.Opcodes.AALOAD;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.ICONST_1;
Expand All @@ -55,6 +58,7 @@
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.PUTSTATIC;
import static org.objectweb.asm.Opcodes.RETURN;

/**
Expand All @@ -64,6 +68,8 @@
public class DgmConverter {

private static final System.Logger LOGGER = System.getLogger(DgmConverter.class.getName());
private static final String TARGET = "TARGET";
private static final String METHOD_HANDLE_CLASS_NAME = "Ljava/lang/invoke/MethodHandle;";

/**
* Generates DGM adapter classes into the target directory.
Expand Down Expand Up @@ -117,12 +123,16 @@

final String methodDescriptor = BytecodeHelper.getMethodDescriptor(returnType, method.getNativeParameterTypes());

createTargetMethodHandleField(cw, method, className);

createInvokeMethod(method, cw, returnType, methodDescriptor);

createDoMethodInvokeMethod(method, cw, className, returnType, methodDescriptor);

createIsValidMethodMethod(method, cw, className);

createGetTargetMethodHandleMethod(cw, className);

cw.visitEnd();

final byte[] bytes = cw.toByteArray();
Expand Down Expand Up @@ -272,4 +282,57 @@
BytecodeHelper.doCast(mv, type);
}
}

private static void createTargetMethodHandleField(ClassWriter cw, CachedMethod method, String className) {
// private static final java.lang.invoke.MethodHandle TARGET
cw.visitField(ACC_PRIVATE + ACC_STATIC + ACC_FINAL,
TARGET, METHOD_HANDLE_CLASS_NAME, null, null).visitEnd();

// static initializer
MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();

// Lookup lookup = java.lang.invoke.MethodHandles.lookup()
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup",
"()Ljava/lang/invoke/MethodHandles$Lookup;", false);
mv.visitVarInsn(ASTORE, 0);

// Class ownerClass = <declaring class>.class
String ownerInternal = BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass());
mv.visitLdcInsn(org.objectweb.asm.Type.getObjectType(ownerInternal));
mv.visitVarInsn(ASTORE, 1);

// String methodName = "<method name>"
mv.visitLdcInsn(method.getName());
mv.visitVarInsn(ASTORE, 2);

// MethodType methodType = MethodType.methodType(<return>, <param1>, <param2>, ...)
mv.visitLdcInsn(org.objectweb.asm.Type.getMethodType(method.getDescriptor()));
mv.visitVarInsn(ASTORE, 3);

// TARGET = lookup.findStatic(ownerClass, methodName, methodType)
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic",
"(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false);
mv.visitFieldInsn(PUTSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME);

mv.visitInsn(RETURN);
mv.visitMaxs(4, 4);
mv.visitEnd();
}

private static void createGetTargetMethodHandleMethod(ClassWriter cw, String className) {
MethodVisitor mv;
// public MethodHandle getTargetMethodHandle() { return TARGET; }

Check warning on line 329 in src/main/java/org/codehaus/groovy/tools/DgmConverter.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=apache_groovy&issues=AZ5UOjq7i-BUK6nuG577&open=AZ5UOjq7i-BUK6nuG577&pullRequest=2549
mv = cw.visitMethod(ACC_PUBLIC, "getTargetMethodHandle",
"()Ljava/lang/invoke/MethodHandle;", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, className, TARGET, METHOD_HANDLE_CLASS_NAME);
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
}
Loading
Loading