You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/06/14 10:22:45 UTC

(groovy) branch master updated: GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)

This is an automated email from the ASF dual-hosted git repository.

daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 579c8e45fd GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)
579c8e45fd is described below

commit 579c8e45fd9b0d492ed25ce90d742c4043ecd095
Author: Jochen Theodorou <bl...@gmx.org>
AuthorDate: Sun Jun 14 12:22:39 2026 +0200

    GROOVY-12069: introduce methodhandles in dgm wrappers (#2598)
    
    * GROOVY-12069: add method handle to dgm class
    
    * GROOVY-12069: cleanup warnings
    
    * GROOVY-12069: adjust bytecode generation of dgm to remove surplus store/load, add handling of Generated method in Selector for methods
    
    * GROOVY-12069: break up big setHandleForMetaMethod method
---
 .../groovy/reflection/GeneratedMetaMethod.java     |  15 +-
 .../org/codehaus/groovy/tools/DgmConverter.java    | 130 +++--
 .../org/codehaus/groovy/vmplugin/v8/Selector.java  | 176 ++++---
 .../codehaus/groovy/tools/TestDgmConverter.java    | 566 ++++++++++++++++++++-
 4 files changed, 759 insertions(+), 128 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
index e8253c3bf0..7d263f529b 100644
--- a/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
+++ b/src/main/java/org/codehaus/groovy/reflection/GeneratedMetaMethod.java
@@ -20,7 +20,6 @@ package org.codehaus.groovy.reflection;
 
 import groovy.lang.GroovyRuntimeException;
 import groovy.lang.MetaMethod;
-
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.DataInputStream;
@@ -29,6 +28,7 @@ import java.io.FileOutputStream;
 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;
@@ -70,6 +70,19 @@ public abstract class GeneratedMetaMethod extends MetaMethod {
         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;
diff --git a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
index 59639388ee..b41e49a6fc 100644
--- a/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
+++ b/src/main/java/org/codehaus/groovy/tools/DgmConverter.java
@@ -41,11 +41,14 @@ import static java.lang.System.Logger.Level.INFO;
 
 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;
@@ -55,7 +58,9 @@ import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
 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;
+import static org.objectweb.asm.Type.getMethodType;
 
 /**
  * Generates {@code GeneratedMetaMethod} adapter classes and metadata for the
@@ -64,6 +69,8 @@ import static org.objectweb.asm.Opcodes.RETURN;
 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.
@@ -79,36 +86,29 @@ public class DgmConverter {
             targetDirectory = args[1];
             if (!targetDirectory.endsWith("/")) targetDirectory += "/";
         }
-        List<CachedMethod> cachedMethodsList = new ArrayList<CachedMethod>();
-        for (Class aClass : DefaultGroovyMethods.DGM_LIKE_CLASSES) {
+        List<CachedMethod> cachedMethodsList = new ArrayList<>();
+        for (Class<?> aClass : DefaultGroovyMethods.DGM_LIKE_CLASSES) {
             Collections.addAll(cachedMethodsList, ReflectionCache.getCachedClass(aClass).getMethods());
         }
         final CachedMethod[] cachedMethods = cachedMethodsList.toArray(CachedMethod.EMPTY_ARRAY);
 
-        List<GeneratedMetaMethod.DgmMethodRecord> records = new ArrayList<GeneratedMetaMethod.DgmMethodRecord>();
+        List<GeneratedMetaMethod.DgmMethodRecord> records = new ArrayList<>();
 
         int cur = 0;
         for (CachedMethod method : cachedMethods) {
-            if (!method.isStatic() || !method.isPublic())
-                continue;
+            if (skipMethod(method)) continue;
 
-            if (method.getAnnotation(Deprecated.class) != null)
-                continue;
-
-            if (method.getParameterTypes().length == 0)
-                continue;
-
-            final Class returnType = method.getReturnType();
+            final Class<?> returnType = method.getReturnType();
 
             final String className = "org/codehaus/groovy/runtime/dgm$" + cur++;
 
-            GeneratedMetaMethod.DgmMethodRecord record = new GeneratedMetaMethod.DgmMethodRecord();
-            records.add(record);
+            GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord = new GeneratedMetaMethod.DgmMethodRecord();
+            records.add(dgmMethodRecord);
 
-            record.methodName = method.getName();
-            record.returnType = method.getReturnType();
-            record.parameters = method.getNativeParameterTypes();
-            record.className = className;
+            dgmMethodRecord.methodName = method.getName();
+            dgmMethodRecord.returnType = method.getReturnType();
+            dgmMethodRecord.parameters = method.getNativeParameterTypes();
+            dgmMethodRecord.className = className;
 
             ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
             cw.visit(CompilerConfiguration.DEFAULT.getBytecodeVersion(), ACC_PUBLIC, className, null, "org/codehaus/groovy/reflection/GeneratedMetaMethod", null);
@@ -117,12 +117,16 @@ public class DgmConverter {
 
             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();
@@ -141,6 +145,16 @@ public class DgmConverter {
             LOGGER.log(INFO, "Saved {0} dgm records to: {1}/META-INF/dgminfo", cur, targetDirectory);
     }
 
+    private static boolean skipMethod(CachedMethod method) {
+        if (!method.isStatic() || !method.isPublic())
+            return true;
+
+        if (method.getAnnotation(Deprecated.class) != null)
+            return true;
+
+        return method.getParameterTypes().length == 0;
+    }
+
     private static void createConstructor(ClassWriter cw) {
         MethodVisitor mv;
         mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;Lorg/codehaus/groovy/reflection/CachedClass;Ljava/lang/Class;[Ljava/lang/Class;)V", null, null);
@@ -188,7 +202,10 @@ public class DgmConverter {
         }
     }
 
-    private static void createDoMethodInvokeMethod(CachedMethod method, ClassWriter cw, String className, Class returnType, String methodDescriptor) {
+    /**
+     * Generates bytecode for method invocation with argument coercion
+     */
+    private static void createDoMethodInvokeMethod(CachedMethod method, ClassWriter cw, String className, Class<?> returnType, String methodDescriptor) {
         MethodVisitor mv;
         mv = cw.visitMethod(ACC_PUBLIC + ACC_FINAL, "doMethodInvoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
         mv.visitCode();
@@ -207,7 +224,7 @@ public class DgmConverter {
 
             // cast argument to parameter class, inclusive unboxing
             // for methods with primitive types
-            Class type = method.getParameterTypes()[1].getTheClass();
+            Class<?> type = method.getParameterTypes()[1].getTheClass();
             BytecodeHelper.doCast(mv, type);
         } else {
             mv.visitVarInsn(ALOAD, 0);
@@ -216,9 +233,14 @@ public class DgmConverter {
             mv.visitVarInsn(ASTORE, 2);
             mv.visitVarInsn(ALOAD, 1);
             BytecodeHelper.doCast(mv, method.getParameterTypes()[0].getTheClass());
-            loadParameters(method, 2, mv);
+            loadParameters(method, mv);
         }
+        writeMethodCall(method, returnType, methodDescriptor, mv);
+    }
+
+    private static void writeMethodCall(CachedMethod method, Class<?> returnType, String methodDescriptor, MethodVisitor mv) {
         mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()), method.getName(), methodDescriptor, false);
+        // Handles primitive return types via autoboxing
         if (returnType == void.class) {
             mv.visitInsn(ACONST_NULL);
         } else if (returnType.isPrimitive()) {
@@ -230,23 +252,17 @@ public class DgmConverter {
         mv.visitEnd();
     }
 
-    private static void createInvokeMethod(CachedMethod method, ClassWriter cw, Class returnType, String methodDescriptor) {
+    /**
+     * Generates bytecode for method invocation with return handling
+     */
+    private static void createInvokeMethod(CachedMethod method, ClassWriter cw, Class<?> returnType, String methodDescriptor) {
         MethodVisitor mv;
         mv = cw.visitMethod(ACC_PUBLIC, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", null, null);
         mv.visitCode();
         mv.visitVarInsn(ALOAD, 1);
         BytecodeHelper.doCast(mv, method.getParameterTypes()[0].getTheClass());
-        loadParameters(method, 2, mv);
-        mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass()), method.getName(), methodDescriptor, false);
-        if (returnType == void.class) {
-            mv.visitInsn(ACONST_NULL);
-        } else if (returnType.isPrimitive()) {
-            Class<?> wrapperType = TypeUtil.autoboxType(returnType);
-            mv.visitMethodInsn(INVOKESTATIC, BytecodeHelper.getClassInternalName(wrapperType), "valueOf", "(" + BytecodeHelper.getTypeDescription(returnType) + ")" + BytecodeHelper.getTypeDescription(wrapperType), false);
-        }
-        mv.visitInsn(ARETURN);
-        mv.visitMaxs(0, 0);
-        mv.visitEnd();
+        loadParameters(method, mv);
+        writeMethodCall(method, returnType, methodDescriptor, mv);
     }
 
     /**
@@ -254,22 +270,62 @@ public class DgmConverter {
      * method from an {@code Object[]} local variable.
      *
      * @param method the cached method whose parameters are being loaded
-     * @param argumentIndex the local-variable slot containing the argument array
      * @param mv the visitor receiving the bytecode instructions
      */
-    protected static void loadParameters(CachedMethod method, int argumentIndex, MethodVisitor mv) {
+    protected static void loadParameters(CachedMethod method, MethodVisitor mv) {
         CachedClass[] parameters = method.getParameterTypes();
         int size = parameters.length - 1;
         for (int i = 0; i < size; i++) {
             // unpack argument from Object[]
-            mv.visitVarInsn(ALOAD, argumentIndex);
+            mv.visitVarInsn(ALOAD, 2);
             BytecodeHelper.pushConstant(mv, i);
             mv.visitInsn(AALOAD);
 
             // cast argument to parameter class, inclusive unboxing
             // for methods with primitive types
-            Class type = parameters[i + 1].getTheClass();
+            Class<?> type = parameters[i + 1].getTheClass();
             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);
+        // Class ownerClass = <declaring class>.class
+        String ownerInternal = BytecodeHelper.getClassInternalName(method.getDeclaringClass().getTheClass());
+        mv.visitLdcInsn(org.objectweb.asm.Type.getObjectType(ownerInternal));
+        // String methodName = "<method name>"
+        mv.visitLdcInsn(method.getName());
+        // MethodType methodType = MethodType.methodType(<return>, <param1>, <param2>, ...)
+        mv.visitLdcInsn(getMethodType(method.getDescriptor()));
+        // TARGET = lookup.findStatic(ownerClass, methodName, methodType)
+        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(0, 0);
+        mv.visitEnd();
+    }
+
+    private static void createGetTargetMethodHandleMethod(ClassWriter cw, String className) {
+        MethodVisitor mv;
+        // public MethodHandle getTargetMethodHandle() { return TARGET; }
+        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();
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
index 12ce177122..4510eb934e 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -31,6 +31,18 @@ import groovy.lang.MetaMethod;
 import groovy.lang.MissingMethodException;
 import groovy.lang.ProxyMetaClass;
 import groovy.transform.Internal;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
 import org.codehaus.groovy.GroovyBugError;
 import org.codehaus.groovy.reflection.CachedField;
 import org.codehaus.groovy.reflection.CachedMethod;
@@ -40,7 +52,6 @@ import org.codehaus.groovy.reflection.stdclasses.CachedSAMClass;
 import org.codehaus.groovy.runtime.ArrayTypeUtils;
 import org.codehaus.groovy.runtime.GeneratedClosure;
 import org.codehaus.groovy.runtime.GroovyCategorySupport;
-import org.codehaus.groovy.runtime.GroovyCategorySupport.CategoryMethod;
 import org.codehaus.groovy.runtime.MetaClassHelper;
 import org.codehaus.groovy.runtime.NullObject;
 import org.codehaus.groovy.runtime.dgmimpl.NumberNumberMetaMethod;
@@ -55,19 +66,6 @@ import org.codehaus.groovy.runtime.wrappers.Wrapper;
 import org.codehaus.groovy.vmplugin.VMPlugin;
 import org.codehaus.groovy.vmplugin.VMPluginFactory;
 
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.lang.reflect.Array;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Objects;
-
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.ARRAYLIST_CONSTRUCTOR;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.BEAN_CONSTRUCTOR_PROPERTY_SETTER;
 import static org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.CLASS_FOR_NAME;
@@ -744,85 +742,107 @@ public abstract class Selector {
 
         /**
          * Creates a MethodHandle using a before selected MetaMethod.
-         * If the MetaMethod has reflective information available, then
-         * we will use that information to create the target MethodHandle.
-         * If that is not the case we will produce a handle, which will use the
-         * MetaMethod itself for invocation.
+         * NumberNumberMetaMethod and GeneratedMetaMethod can be handled in a simplified way,
+         * ReflectionMetaMethod will be unwrapped and unreflected for a handle. If
+         * all that does not apply, we will fall back to using the MetaMethod itself.
          */
         public void setHandleForMetaMethod() {
+            isCategoryMethod = (method instanceof GroovyCategorySupport.CategoryMethod);
+            if (setHandleForSimpleCases()) return;
+
             MetaMethod metaMethod = method;
-            isCategoryMethod = (method instanceof CategoryMethod);
+            if (metaMethod instanceof ReflectionMetaMethod rmm) {
+                if (LOG_ENABLED) LOG.info("meta method is reflective method");
+                metaMethod = rmm.getCachedMethod();
+            }
 
-            if (metaMethod instanceof NumberNumberMetaMethod
-                    || (method instanceof GeneratedMetaMethod && ("next".equals(name) || "previous".equals(name)))) {
-                if (LOG_ENABLED) LOG.info("meta method is number method");
-                if (IndyMath.chooseMathMethod(this, metaMethod)) {
-                    catchException = false;
-                    if (LOG_ENABLED) LOG.info("indy math successful");
-                    return;
-                }
+            if (metaMethod instanceof CachedMethod cm) {
+                setHandleForGeneralCachedMethod(cm, metaMethod);
+            } else if (method != null) {
+                setHandleForGenericMetaMethod();
             }
+        }
 
-            boolean isCategoryTypeMethod = (metaMethod instanceof NewInstanceMetaMethod);
-            if (LOG_ENABLED) LOG.info("meta method is category type method: " + isCategoryTypeMethod);
-            boolean isStaticCategoryTypeMethod = (metaMethod instanceof NewStaticMetaMethod);
-            if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isCategoryTypeMethod);
+        private void setHandleForGenericMetaMethod() {
+            if (LOG_ENABLED) LOG.info("meta method is generic meta method");
+            // generic meta method invocation path
+            handle = META_METHOD_INVOKER;
+            handle = handle.bindTo(method);
+            if (spread) {
+                args = originalArguments;
+                skipSpreadCollector = true;
+            } else {
+                // wrap arguments from call site in Object[]
+                handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1);
+            }
+            currentType = removeWrapper(targetType);
+            if (LOG_ENABLED) LOG.info("bound method name to META_METHOD_INVOKER");
+        }
 
-            if (metaMethod instanceof ReflectionMetaMethod) {
-                if (LOG_ENABLED) LOG.info("meta method is reflective method");
-                metaMethod = ((ReflectionMetaMethod) metaMethod).getCachedMethod();
+        private void setHandleForGeneralCachedMethod(CachedMethod cm, MetaMethod metaMethod) {
+            boolean isCategoryTypeMethod = (method instanceof NewInstanceMetaMethod);
+            if (LOG_ENABLED) LOG.info("meta method is category type method: " + isCategoryTypeMethod);
+            boolean isStaticCategoryTypeMethod = (method instanceof NewStaticMetaMethod);
+            if (LOG_ENABLED) LOG.info("meta method is static category type method: " + isStaticCategoryTypeMethod);
+
+            isVargs = metaMethod.isVargsMethod();
+            VMPlugin vmplugin = VMPluginFactory.getPlugin();
+            cm = (CachedMethod) vmplugin.transformMetaMethod(mc, cm, sender);
+            setBaseHandleForCachedMethod(cm);
+            if (isStaticCategoryTypeMethod) {
+                handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY);
+                handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0));
+            } else if (!isCategoryTypeMethod && cm.isStatic()) {
+                // drop the receiver, which might be a Class (invocation on Class)
+                // or it might be an object (static method invocation on instance)
+                // Object.class handles both cases at once
+                handle = MethodHandles.dropArguments(handle, 0, Object.class);
             }
+        }
 
-            if (metaMethod instanceof CachedMethod cm) {
-                isVargs = metaMethod.isVargsMethod();
-                VMPlugin vmplugin = VMPluginFactory.getPlugin();
-                cm = (CachedMethod) vmplugin.transformMetaMethod(mc, cm, sender);
-                try {
-                    var declaringClass = cm.getDeclaringClass().getTheClass();
-                    int parameterCount = cm.getParamsCount();
-                    if (parameterCount == 0 && "clone".equals(name) && declaringClass == Object.class) {
-                        var receiverClass = getCorrectedReceiver().getClass();
-                        if (receiverClass.isArray()) { // GROOVY-10733, et al.
-                            handle = MethodHandles.publicLookup().findVirtual(receiverClass, "clone", MethodType.methodType(Object.class));
-                        } else { // GROOVY-10319
-                            handle = MethodHandles.throwException(Object.class, CloneNotSupportedException.class) // prevent illegal access
-                                                                    .bindTo(new CloneNotSupportedException());
-                            handle = MethodHandles.dropArguments(handle, 0, Object.class); // discard receiver
-                        }
-                    } else if (parameterCount == 1 && "forName".equals(name) && declaringClass == Class.class) {
-                        handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader());
-                    } else {
-                        handle = unreflect(cm.getCachedMethod());
+        private void setBaseHandleForCachedMethod(CachedMethod cm) {
+            try {
+                var declaringClass = cm.getDeclaringClass().getTheClass();
+                int parameterCount = cm.getParamsCount();
+                if (parameterCount == 0 && "clone".equals(name) && declaringClass == Object.class) {
+                    var receiverClass = getCorrectedReceiver().getClass();
+                    if (receiverClass.isArray()) { // GROOVY-10733, et al.
+                        handle = MethodHandles.publicLookup().findVirtual(receiverClass, "clone", MethodType.methodType(Object.class));
+                    } else { // GROOVY-10319
+                        handle = MethodHandles.throwException(Object.class, CloneNotSupportedException.class) // prevent illegal access
+                                                                .bindTo(new CloneNotSupportedException());
+                        handle = MethodHandles.dropArguments(handle, 0, Object.class); // discard receiver
                     }
-                } catch (ReflectiveOperationException e) {
-                    throw new GroovyBugError(e);
-                }
-                if (isStaticCategoryTypeMethod) {
-                    handle = MethodHandles.insertArguments(handle, 0, SINGLE_NULL_ARRAY);
-                    handle = MethodHandles.dropArguments(handle, 0, targetType.parameterType(0));
-                } else if (!isCategoryTypeMethod && cm.isStatic()) {
-                    // drop the receiver, which might be a Class (invocation on Class)
-                    // or it might be an object (static method invocation on instance)
-                    // Object.class handles both cases at once
-                    handle = MethodHandles.dropArguments(handle, 0, Object.class);
-                }
-            } else if (method != null) {
-                if (LOG_ENABLED) LOG.info("meta method is dgm helper");
-                // generic meta method invocation path
-                handle = META_METHOD_INVOKER;
-                handle = handle.bindTo(method);
-                if (spread) {
-                    args = originalArguments;
-                    skipSpreadCollector = true;
+                } else if (parameterCount == 1 && "forName".equals(name) && declaringClass == Class.class) {
+                    handle = MethodHandles.insertArguments(CLASS_FOR_NAME, 1, Boolean.TRUE, sender.getClassLoader());
                 } else {
-                    // wrap arguments from call site in Object[]
-                    handle = handle.asCollector(Object[].class, targetType.parameterCount() - 1);
+                    handle = unreflect(cm.getCachedMethod());
                 }
-                currentType = removeWrapper(targetType);
-                if (LOG_ENABLED) LOG.info("bound method name to META_METHOD_INVOKER");
+            } catch (ReflectiveOperationException e) {
+                throw new GroovyBugError(e);
             }
         }
 
+        private boolean setHandleForSimpleCases() {
+            if (method instanceof NumberNumberMetaMethod
+                    || (method instanceof GeneratedMetaMethod && ("next".equals(name) || "previous".equals(name)))) {
+                if (LOG_ENABLED) LOG.info("meta method is number method");
+                if (IndyMath.chooseMathMethod(this, method)) {
+                    catchException = false;
+                    if (LOG_ENABLED) LOG.info("indy math successful");
+                    return true;
+                }
+            }
+            if (method instanceof GeneratedMetaMethod gmm && gmm.getTargetMethodHandle() != null) {
+                if (LOG_ENABLED) LOG.info("meta method is generated method");
+                handle = gmm.getTargetMethodHandle();
+                catchException = false;
+                isVargs = method.isVargsMethod();
+                return true;
+            }
+            return false;
+        }
+
         /**
          * Unreflects a cached reflective method against the call-site lookup.
          *
diff --git a/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java b/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
index 30bb647d66..e35d5776a0 100644
--- a/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
+++ b/src/test/groovy/org/codehaus/groovy/tools/TestDgmConverter.java
@@ -19,36 +19,106 @@
 package org.codehaus.groovy.tools;
 
 import groovy.lang.MetaMethod;
-import junit.framework.TestCase;
-import org.codehaus.groovy.runtime.DefaultGroovyMethods;
-import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl;
-
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import junit.framework.TestCase;
+import org.codehaus.groovy.reflection.CachedClass;
+import org.codehaus.groovy.reflection.GeneratedMetaMethod;
+import org.codehaus.groovy.reflection.ReflectionCache;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+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;
 
+/**
+ * Tests for {@link DgmConverter} covering generated adapter class structure,
+ * functional behavior, bytecode correctness, and DGM record serialization.
+ */
 public class TestDgmConverter extends TestCase {
 
     private static final String REFERENCE_CLASS = "/org/codehaus/groovy/runtime/dgm$0.class";
+    private static final ClassLoader DGM_CLASS_LOADER = DefaultGroovyMethods.class.getClassLoader();
+
+    /**
+     * Finds the first dgm$ class file on the classpath for inspection.
+     */
+    private static File findFirstDgmClassFile() throws URISyntaxException {
+        File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
+        File[] files = dgmClassDirectory.listFiles();
+        assertNotNull("dgm$ class directory not found", files);
+        for (File file : files) {
+            if (file.getName().startsWith("dgm$")) {
+                return file;
+            }
+        }
+        fail("No dgm$ class files found");
+        return null; // unreachable
+    }
+
+    /**
+     * Loads all dgm$ class files sorted by name.
+     */
+    private static List<File> findAllDgmClassFiles() throws URISyntaxException {
+        File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
+        File[] files = dgmClassDirectory.listFiles();
+        assertNotNull("dgm$ class directory not found", files);
+        List<File> dgmFiles = new ArrayList<>();
+        for (File file : files) {
+            if (file.getName().startsWith("dgm$")) {
+                dgmFiles.add(file);
+            }
+        }
+        dgmFiles.sort((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()));
+        return dgmFiles;
+    }
+
+    /**
+     * Reads the raw bytes of a class file.
+     */
+    private static byte[] readClassBytes(File classFile) throws IOException {
+        return Files.readAllBytes(classFile.toPath());
+    }
 
-    public void testConverter () throws URISyntaxException {
-        File dgmClassDirectory = new File(TestDgmConverter.class.getResource(REFERENCE_CLASS).toURI()).getParentFile();
+    // ==================== Existing tests (preserved) ====================
+
+    public void testConverter() throws URISyntaxException {
+        File dgmClassDirectory = new File(Objects.requireNonNull(TestDgmConverter.class.getResource(REFERENCE_CLASS)).toURI()).getParentFile();
 
         final File[] files = dgmClassDirectory.listFiles();
+        assertNotNull(files);
         Arrays.sort(files, (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getName(), o2.getName()));
-        for (int i = 0; i < files.length; i++) {
-            File file = files[i];
+        for (File file : files) {
             final String name = file.getName();
             if (name.startsWith("dgm$")) {
                 final String className = "org.codehaus.groovy.runtime." + name.substring(0, name.length() - ".class".length());
                 try {
-                    Class cls = Class.forName(className, false, DefaultGroovyMethods.class.getClassLoader());
-                    Constructor[] declaredConstructors = cls.getDeclaredConstructors();
+                    Class<?> cls = Class.forName(className, false, DefaultGroovyMethods.class.getClassLoader());
+                    Constructor<?>[] declaredConstructors = cls.getDeclaredConstructors();
                     assertEquals(1, declaredConstructors.length);
-                    Constructor constructor = declaredConstructors[0];
-                    final MetaMethod metaMethod = (MetaMethod) constructor.newInstance(null,null, null, null);
+                    Constructor<?> constructor = declaredConstructors[0];
+                    final MetaMethod metaMethod = (MetaMethod) constructor.newInstance(null, null, null, null);
+                    assertNotNull(metaMethod);
                 } catch (ClassNotFoundException e) {
                     fail("Failed to load " + className);
                 } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
@@ -63,4 +133,476 @@ public class TestDgmConverter extends TestCase {
         int instanceMethods = metaClassRegistry.getInstanceMethods().size();
         assertTrue(instanceMethods > 0);
     }
+
+    // ==================== Bytecode structure tests ====================
+
+    /**
+     * Verifies every generated dgm$ class extends GeneratedMetaMethod.
+     */
+    public void testGeneratedClassesExtendGeneratedMetaMethod() throws Exception {
+        for (File file : findAllDgmClassFiles()) {
+            String name = file.getName().replace(".class", "");
+            Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+            assertTrue(
+                    cls.getName() + " should extend GeneratedMetaMethod",
+                    GeneratedMetaMethod.class.isAssignableFrom(cls)
+            );
+        }
+    }
+
+    /**
+     * Verifies every generated dgm$ class has exactly one public constructor
+     * with the expected parameter types: (String, CachedClass, Class, Class[]).
+     */
+    public void testGeneratedClassesHaveCorrectConstructor() throws Exception {
+        for (File file : findAllDgmClassFiles()) {
+            String name = file.getName().replace(".class", "");
+            Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+            Constructor<?>[] constructors = cls.getDeclaredConstructors();
+            assertEquals(cls.getName() + " should have exactly one constructor", 1, constructors.length);
+            Constructor<?> ctor = constructors[0];
+            Class<?>[] paramTypes = ctor.getParameterTypes();
+            assertEquals(cls.getName() + " constructor should have 4 parameters", 4, paramTypes.length);
+            assertEquals(String.class, paramTypes[0]);
+            assertEquals(CachedClass.class, paramTypes[1]);
+            assertEquals(Class.class, paramTypes[2]);
+            assertEquals(Class[].class, paramTypes[3]);
+        }
+    }
+
+    /**
+     * Uses ASM to verify that a generated dgm$ class has a private static final
+     * MethodHandle field named TARGET.
+     */
+    public void testGeneratedClassHasTargetMethodHandleField() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] found = {false};
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
+                if ("TARGET".equals(name)) {
+                    assertEquals("Ljava/lang/invoke/MethodHandle;", descriptor);
+                    assertEquals("TARGET should be private", ACC_PRIVATE, access & ACC_PRIVATE);
+                    assertEquals("TARGET should be static", ACC_STATIC, access & ACC_STATIC);
+                    assertEquals("TARGET should be final", ACC_FINAL, access & ACC_FINAL);
+                    found[0] = true;
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("TARGET MethodHandle field should exist", found[0]);
+    }
+
+    /**
+     * Uses ASM to verify that a generated dgm$ class has a static initializer
+     * that calls MethodHandles.lookup() and Lookup.findStatic().
+     */
+    public void testStaticInitializerUsesMethodHandlesLookup() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] hasClinit = {false};
+        final boolean[] hasLookup = {false};
+        final boolean[] hasFindStatic = {false};
+
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                if ("<clinit>".equals(name)) {
+                    hasClinit[0] = true;
+                    return new MethodVisitor(Opcodes.ASM9) {
+                        @Override
+                        public void visitMethodInsn(int opcode, String owner, String mname, String mdescriptor, boolean isInterface) {
+                            if ("java/lang/invoke/MethodHandles".equals(owner) && "lookup".equals(mname)) {
+                                hasLookup[0] = true;
+                            }
+                            if ("java/lang/invoke/MethodHandles$Lookup".equals(owner) && "findStatic".equals(mname)) {
+                                hasFindStatic[0] = true;
+                            }
+                        }
+                    };
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("Static initializer (<clinit>) should exist", hasClinit[0]);
+        assertTrue("<clinit> should call MethodHandles.lookup()", hasLookup[0]);
+        assertTrue("<clinit> should call Lookup.findStatic()", hasFindStatic[0]);
+    }
+
+    /**
+     * Uses ASM to verify that a generated dgm$ class has a getTargetMethodHandle()
+     * public method returning MethodHandle.
+     */
+    public void testGetTargetMethodHandleMethodExists() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] found = {false};
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                if ("getTargetMethodHandle".equals(name)) {
+                    assertEquals("()Ljava/lang/invoke/MethodHandle;", descriptor);
+                    assertEquals("getTargetMethodHandle should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+                    found[0] = true;
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("getTargetMethodHandle method should exist", found[0]);
+    }
+
+    /**
+     * Uses ASM to verify that a generated dgm$ class has an invoke(Object, Object[])
+     * public method.
+     */
+    public void testInvokeMethodExists() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] found = {false};
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                if ("invoke".equals(name)) {
+                    assertEquals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", descriptor);
+                    assertEquals("invoke should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+                    found[0] = true;
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("invoke method should exist", found[0]);
+    }
+
+    /**
+     * Uses ASM to verify that a generated dgm$ class has a doMethodInvoke(Object, Object[])
+     * public final method.
+     */
+    public void testDoMethodInvokeMethodExists() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] found = {false};
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                if ("doMethodInvoke".equals(name)) {
+                    assertEquals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", descriptor);
+                    assertEquals("doMethodInvoke should be public", ACC_PUBLIC, access & ACC_PUBLIC);
+                    assertEquals("doMethodInvoke should be final", ACC_FINAL, access & ACC_FINAL);
+                    found[0] = true;
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("doMethodInvoke method should exist", found[0]);
+    }
+
+    /**
+     * Uses ASM to verify that the constructor of a generated dgm$ class
+     * delegates to GeneratedMetaMethod.<init>.
+     */
+    public void testConstructorDelegatesToGeneratedMetaMethod() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        byte[] bytes = readClassBytes(dgmFile);
+
+        final boolean[] found = {false};
+        new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+            @Override
+            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+                if ("<init>".equals(name)) {
+                    return new MethodVisitor(Opcodes.ASM9) {
+                        @Override
+                        public void visitMethodInsn(int opcode, String owner, String mname, String mdescriptor, boolean isInterface) {
+                            if ("org/codehaus/groovy/reflection/GeneratedMetaMethod".equals(owner)
+                                    && "<init>".equals(mname)) {
+                                found[0] = true;
+                            }
+                        }
+                    };
+                }
+                return null;
+            }
+        }, 0);
+        assertTrue("Constructor should delegate to GeneratedMetaMethod.<init>", found[0]);
+    }
+
+    // ==================== Functional / behavioral tests ====================
+
+    /**
+     * Verifies that getTargetMethodHandle() returns a non-null MethodHandle.
+     */
+    public void testGetTargetMethodHandleReturnsNonNull() throws Exception {
+        File dgmFile = findFirstDgmClassFile();
+        assertNotNull(dgmFile);
+        String name = dgmFile.getName().replace(".class", "");
+        Class<?> cls = Class.forName("org.codehaus.groovy.runtime." + name, true, DGM_CLASS_LOADER);
+
+        Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+        assertNotNull(ctor);
+        // Use reflection to determine the declaring class, return type, and parameter types
+        // from the original DGM method. We can get these from the GeneratedMetaMethod metadata.
+        // Construct with nulls first to get metadata, then check a method handle.
+        // Actually, the MethodHandle is a static field initialized in <clinit>, so we can access
+        // it via reflection on the class.
+        Field targetField = cls.getDeclaredField("TARGET");
+        targetField.setAccessible(true);
+        Object mh = targetField.get(null);
+        assertNotNull("TARGET MethodHandle field should not be null", mh);
+        assertTrue("TARGET should be a MethodHandle", mh instanceof MethodHandle);
+    }
+
+    /**
+     * Verifies that the Target MethodHandle is invokable (smoke test).
+     * Selects a generated class and invokes the MethodHandle through the adapter.
+     */
+    public void testMethodHandleIsInvokable() throws Exception {
+        // Load a known DGM method through the registry and invoke it
+        // to verify end-to-end functionality
+        List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+        assertFalse("DGM records should not be empty", records.isEmpty());
+
+        for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+            try {
+                // record.className already contains the fully qualified path (org/codehaus/groovy/runtime/dgm$N)
+                Class<?> cls = Class.forName(dgmMethodRecord.className.replace('/', '.'),
+                    true, DGM_CLASS_LOADER);
+                Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+
+                // Build the CachedClass[] for parameters
+                CachedClass[] cachedParams = new CachedClass[dgmMethodRecord.parameters.length];
+                for (int i = 0; i < dgmMethodRecord.parameters.length; i++) {
+                    cachedParams[i] = ReflectionCache.getCachedClass(dgmMethodRecord.parameters[i]);
+                }
+
+                MetaMethod adapter = (MetaMethod) ctor.newInstance(
+                    dgmMethodRecord.methodName,
+                    cachedParams[0], // declaring class
+                    dgmMethodRecord.returnType,
+                    dgmMethodRecord.parameters
+                );
+
+                assertEquals(dgmMethodRecord.methodName, adapter.getName());
+                assertEquals(dgmMethodRecord.returnType, adapter.getReturnType());
+                assertEquals(dgmMethodRecord.parameters.length, adapter.getParameterTypes().length);
+
+                // Verify getTargetMethodHandle returns a MethodHandle
+                Method getMhMethod = cls.getMethod("getTargetMethodHandle");
+                MethodHandle mh = (MethodHandle) getMhMethod.invoke(adapter);
+                assertNotNull("MethodHandle should not be null for " + dgmMethodRecord.methodName, mh);
+
+                // Verify the MethodHandle type is compatible
+                assertEquals(dgmMethodRecord.returnType, mh.type().returnType());
+
+                break;
+            } catch (Exception e) {
+                // Some records may not work with null args, skip and try next
+            }
+        }
+    }
+
+    /**
+     * Verifies that the doMethodInvoke method works for a simple non-coercion case.
+     * Uses a concrete DGM method (e.g., String.center) to test end-to-end invocation.
+     */
+    public void testDoMethodInvokeWithConcreteExample() throws Exception {
+        List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+
+        // Find a suitable record for a String method
+        for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+            if (dgmMethodRecord.parameters.length > 1 && dgmMethodRecord.parameters[0] == String.class) {
+                // record.className already contains the fully qualified path (org/codehaus/groovy/runtime/dgm$N)
+                Class<?> cls = Class.forName(dgmMethodRecord.className.replace('/', '.'),
+                        true, DGM_CLASS_LOADER);
+                Constructor<?> ctor = cls.getDeclaredConstructors()[0];
+
+                CachedClass[] cachedParams = new CachedClass[dgmMethodRecord.parameters.length];
+                for (int i = 0; i < dgmMethodRecord.parameters.length; i++) {
+                    cachedParams[i] = ReflectionCache.getCachedClass(dgmMethodRecord.parameters[i]);
+                }
+
+                MetaMethod adapter = (MetaMethod) ctor.newInstance(
+                        dgmMethodRecord.methodName,
+                        cachedParams[0],
+                        dgmMethodRecord.returnType,
+                        dgmMethodRecord.parameters
+                );
+
+                // Test doMethodInvoke with proper arguments
+                // Build Object[] args for the method (excluding the receiver which is the first param)
+                Object[] args = new Object[dgmMethodRecord.parameters.length - 1];
+                // Fill with dummy values matching parameter types where possible
+                for (int i = 0; i < args.length; i++) {
+                    Class<?> pType = dgmMethodRecord.parameters[i + 1];
+                    if (pType == int.class || pType == Integer.class) args[i] = 0;
+                    else if (pType == String.class) args[i] = "";
+                    else if (pType == long.class || pType == Long.class) args[i] = 0L;
+                    else if (pType == boolean.class || pType == Boolean.class) args[i] = false;
+                    else if (pType == char.class || pType == Character.class) args[i] = ' ';
+                    else if (pType == double.class || pType == Double.class) args[i] = 0.0;
+                    else if (pType == float.class || pType == Float.class) args[i] = 0.0f;
+                    else if (pType == short.class || pType == Short.class) args[i] = (short) 0;
+                    else if (pType == byte.class || pType == Byte.class) args[i] = (byte) 0;
+                    else if (pType == Object[].class) args[i] = new Object[0];
+                    else if (pType == int[].class) args[i] = new int[0];
+                    else if (pType == char[].class) args[i] = new char[0];
+                    else args[i] = null;
+                }
+
+                try {
+                    Object result = adapter.doMethodInvoke("test", args);
+                    // Verify we can call it without unexpected exceptions
+                    assertNotNull(result);
+                    break;
+                } catch (Exception e) {
+                    // Some methods may still fail with empty string args, skip and try next
+                }
+            }
+        }
+    }
+
+    // ==================== Filtering logic tests ====================
+
+    /**
+     * Verifies that no generated dgm$ class corresponds to a deprecated method.
+     */
+    public void testNoDeprecatedMethodsInRecords() throws Exception {
+        List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+        for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+            // Load the declaring class from the record to get the actual original method
+            // The record.parameters includes the receiver as the first element
+            // The declaring class is the class that contains the static method
+            Class<?> declaringClass = dgmMethodRecord.parameters[0];
+            for (java.lang.reflect.Method m : declaringClass.getMethods()) {
+                if (m.getName().equals(dgmMethodRecord.methodName)
+                        && java.util.Arrays.equals(m.getParameterTypes(), dgmMethodRecord.parameters)) {
+                    assertFalse(
+                            "Deprecated method " + dgmMethodRecord.methodName + " on " + declaringClass.getName()
+                                    + " should not be in DGM records",
+                            m.isAnnotationPresent(Deprecated.class)
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * Verifies that no zero-parameter methods appear in DGM records.
+     * (DgmConverter filters out methods with 0 parameters.)
+     */
+    public void testNoZeroParamMethodsInRecords() throws Exception {
+        List<GeneratedMetaMethod.DgmMethodRecord> records = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+        assertFalse("DGM records should not be empty", records.isEmpty());
+        for (GeneratedMetaMethod.DgmMethodRecord dgmMethodRecord : records) {
+            assertTrue(
+                    "DGM record " + dgmMethodRecord.methodName + " should have at least 1 parameter",
+                    dgmMethodRecord.parameters.length >= 1
+            );
+        }
+    }
+
+    // ==================== DgmMethodRecord serialization round-trip ====================
+
+    /**
+     * Tests that DgmMethodRecord.saveDgmInfo/loadDgmInfo round-trips correctly.
+     */
+    public void testDgmMethodRecordRoundTrip() throws Exception {
+        List<GeneratedMetaMethod.DgmMethodRecord> original = GeneratedMetaMethod.DgmMethodRecord.loadDgmInfo();
+        assertFalse("Original records should not be empty", original.isEmpty());
+
+        Path tempFile = Files.createTempFile("dgminfo-test", ".dat");
+        try {
+            // Save to a temp file
+            GeneratedMetaMethod.DgmMethodRecord.saveDgmInfo(original, tempFile.toString());
+
+            // Verify a file was created and has content
+            assertTrue("Saved DGM info file should exist", Files.exists(tempFile));
+            assertTrue("Saved DGM info file should not be empty", Files.size(tempFile) > 0);
+
+            // Load back from the temp file by reading the raw bytes and creating a
+            // DataInputStream to verify the format
+            try (var in = new java.io.DataInputStream(new java.io.BufferedInputStream(new FileInputStream(tempFile.toFile())))) {
+                // Read the class table (skipping primitives)
+                int classCount = 0;
+                while (true) {
+                    String name = in.readUTF();
+                    if (name.isEmpty()) break;
+                    in.readInt(); // class id
+                    classCount++;
+                }
+                assertTrue("Class table should contain at least 1 class", classCount > 0);
+                int recordCount = in.readInt();
+                assertEquals("Loaded record count should match", original.size(), recordCount);
+
+                for (int i = 0; i < recordCount; i++) {
+                    String className = in.readUTF();
+                    assertNotNull(className);
+                    String methodName = in.readUTF();
+                    assertNotNull(methodName);
+                    in.readInt(); // return type id
+                    int paramCount = in.readInt();
+                    for (int j = 0; j < paramCount; j++) {
+                        in.readInt(); // param type id
+                    }
+                }
+                assertEquals("File should be fully consumed", -1, in.read());
+            }
+        } finally {
+            Files.deleteIfExists(tempFile);
+        }
+    }
+
+    // ==================== main() argument parsing tests ====================
+
+    /**
+     * Tests that DgmConverter.main() produces output without errors
+     * using the default arguments.
+     */
+    public void testMainWithDefaultArgs() throws Exception {
+        Path tempDir = Files.createTempDirectory("dgm-test-output");
+        try {
+            // Ensure META-INF directory exists (DgmConverter.main() expects it pre-created)
+            Files.createDirectories(tempDir.resolve("META-INF"));
+
+            // Redirect to temp directory
+            DgmConverter.main(new String[]{"--info", tempDir.toString()});
+
+            // Verify META-INF/dgminfo was created
+            Path dgminfo = tempDir.resolve("META-INF/dgminfo");
+            assertTrue("META-INF/dgminfo should be created", Files.exists(dgminfo));
+            assertTrue("META-INF/dgminfo should not be empty", Files.size(dgminfo) > 0);
+
+            // Verify at least one dgm$ class file was created
+            Path dgmClasses = tempDir.resolve("org/codehaus/groovy/runtime");
+            assertTrue("dgm$ class directory should exist", Files.exists(dgmClasses));
+            try (var stream = Files.list(dgmClasses)) {
+                assertTrue("dgm$ class directory should have files",
+                        stream.anyMatch(p -> p.getFileName().toString().startsWith("dgm$")));
+            }
+        } finally {
+            // Clean up
+            deleteRecursively(tempDir);
+        }
+    }
+
+    private static void deleteRecursively(Path dir) throws IOException {
+        if (Files.isDirectory(dir)) {
+            try (var stream = Files.list(dir)) {
+                for (Path child : stream.toList()) {
+                    deleteRecursively(child);
+                }
+            }
+        }
+        Files.deleteIfExists(dir);
+    }
 }