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 38a3cdef14 GROOVY-12138: emit invokedynamic call sites for property 
writes (spike) (#2674)
38a3cdef14 is described below

commit 38a3cdef14c29b904353683803062ba3c78d4466
Author: Paul King <[email protected]>
AuthorDate: Sat Jul 11 22:59:01 2026 +1000

    GROOVY-12138: emit invokedynamic call sites for property writes (spike) 
(#2674)
    
    * GROOVY-12138: emit invokedynamic call sites for property writes (spike)
    
    Property writes were the last un-cached dispatch surface: even in indy
    mode they compiled to static ScriptBytecodeAdapter.setProperty calls,
    paying getMetaClass() plus a full MetaClassImpl.setProperty resolution
    walk on every write, in both indy and classic modes.
    
    Behind the compile-time flag groovy.indy.setproperty (default: off),
    AsmClassGenerator now diverts plain (non-safe, non-spread) property
    writes to a new CallSiteWriter.makeSetPropertySite hook. The default
    implementation is the classic adapter call, so classic mode and
    flag-off builds are unchanged; IndyCallSiteWriter overrides it to emit
    invokedynamic setProperty:(ReceiverType, ValueType)V with the receiver
    first, preserving the runtime's arguments[0]-is-receiver invariant.
    
    The new SetPropertySelector (replacing the 'not supported' GroovyBugError
    for CallType.SET) fast-paths only shapes whose resolution is provably
    sender- and metaclass-independent: a public setter whose backing field
    is private or absent (GROOVY-8283 field-precedence is sender-dependent),
    or a public non-final field, each accepting the runtime value type
    directly. Everything else — Maps (GROOVY-8065/11367), Class receivers,
    categories, overridden/mixed-in/expando setProperty (detected by both a
    reflection probe and a metaclass pickMethod probe; ExpandoMetaClass is
    excluded wholesale, mirroring the GET selector), value coercion,
    listeners, propertyMissing — binds the sender-aware
    ScriptBytecodeAdapter.setProperty as the call-site handle, making
    non-fast-path behavior identical to the classic path by construction.
    Guards, PIC, and hit-count promotion come free from the existing
    call-site machinery; the value participates in the argument-class
    guards so type changes at a site re-select correctly.
    
    Measured: ~42x on a hot property-write loop (6M mixed setter/field
    writes: ~435ms -> ~10ms, written state consumed and verified). Full
    root test suite (15678 tests) passes with the flag enabled for all
    test-code compilation; a 12-scenario semantic stress produces identical
    output compiled with and without the flag.
    
    * GROOVY-12138: address review comments
---
 .../groovy/classgen/AsmClassGenerator.java         |   2 +
 .../groovy/classgen/asm/CallSiteWriter.java        |  16 +++
 .../classgen/asm/indy/IndyCallSiteWriter.java      |  20 +++
 .../classgen/asm/indy/InvokeDynamicWriter.java     |  37 +++++
 .../codehaus/groovy/reflection/CachedField.java    |  27 ++++
 .../v8/IndyGuardsFiltersAndSignatures.java         |  22 ++-
 .../org/codehaus/groovy/vmplugin/v8/Selector.java  | 154 ++++++++++++++++++++-
 7 files changed, 275 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java 
b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index 2e996fe91f..6f92fece54 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -1441,6 +1441,8 @@ public class AsmClassGenerator extends ClassGenerator {
             callSiteWriter.makeGroovyObjectGetPropertySite(objectExpression, 
propertyName, pexp.isSafe(), pexp.isImplicitThis());
         } else if (adapter == getProperty && propertyName != null && 
!pexp.isSpreadSafe()) {
             callSiteWriter.makeGetPropertySite(objectExpression, propertyName, 
pexp.isSafe(), pexp.isImplicitThis());
+        } else if ((adapter == setProperty || adapter == 
setGroovyObjectProperty) && propertyName != null && !pexp.isSpreadSafe()) {
+            callSiteWriter.makeSetPropertySite(pexp, objectExpression, 
propertyName, adapter, adapter == setGroovyObjectProperty); // GROOVY-12138
         } else {
             callSiteWriter.fallbackAttributeOrPropertySite(pexp, 
objectExpression, propertyName, adapter);
         }
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 7838a7f20a..8196cb0030 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/CallSiteWriter.java
@@ -487,4 +487,20 @@ public class CallSiteWriter {
                 expression.isSafe(), expression.isSpreadSafe(), 
expression.isImplicitThis()
         );
     }
+
+    /**
+     * Emits a property-write site (GROOVY-12138). The assigned value is
+     * already on the operand stack. The default implementation is the classic
+     * {@code ScriptBytecodeAdapter} adapter call; the indy writer may override
+     * this to emit an {@code invokedynamic} {@code setProperty} call site.
+     *
+     * @param expression the property expression being assigned
+     * @param objectExpression the receiver expression
+     * @param name the property name
+     * @param adapter the classic set adapter used for the default path
+     * @param groovyObject whether the receiver is statically known to be a 
{@code GroovyObject}
+     */
+    public void makeSetPropertySite(PropertyExpression expression, Expression 
objectExpression, String name, MethodCallerMultiAdapter adapter, boolean 
groovyObject) {
+        fallbackAttributeOrPropertySite(expression, objectExpression, name, 
adapter);
+    }
 }
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/IndyCallSiteWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/IndyCallSiteWriter.java
index 3376c29453..a2efbff62a 100644
--- 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/IndyCallSiteWriter.java
+++ 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/IndyCallSiteWriter.java
@@ -80,4 +80,24 @@ public class IndyCallSiteWriter extends CallSiteWriter {
         InvokeDynamicWriter idw = 
(InvokeDynamicWriter)controller.getInvocationWriter();
         idw.writeGetProperty(receiver, name, safe, implicitThis, true);
     }
+
+    /**
+     * Experimental (GROOVY-12138), guarded by the {@code 
groovy.indy.setproperty}
+     * compile-time flag: emit an {@code invokedynamic} call site for property
+     * writes instead of the static {@code ScriptBytecodeAdapter.setProperty}
+     * call, giving writes the same call-site caching, guarding, and JIT
+     * inlining that reads have had via {@code getProperty} sites.
+     */
+    private static final boolean INDY_SET_PROPERTY = 
org.apache.groovy.util.SystemUtil.getBooleanSafe("groovy.indy.setproperty");
+
+    /** {@inheritDoc} */
+    @Override
+    public void 
makeSetPropertySite(org.codehaus.groovy.ast.expr.PropertyExpression expression, 
Expression objectExpression, String name, 
org.codehaus.groovy.classgen.asm.MethodCallerMultiAdapter adapter, boolean 
groovyObject) {
+        if (INDY_SET_PROPERTY && !expression.isSafe() && 
!expression.isSpreadSafe()) {
+            InvokeDynamicWriter idw = (InvokeDynamicWriter) 
controller.getInvocationWriter();
+            idw.writeSetProperty(objectExpression, name, 
expression.isImplicitThis(), groovyObject);
+        } else {
+            super.makeSetPropertySite(expression, objectExpression, name, 
adapter, groovyObject);
+        }
+    }
 }
diff --git 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
index 83b0d5dd14..d6871526de 100644
--- 
a/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
+++ 
b/src/main/java/org/codehaus/groovy/classgen/asm/indy/InvokeDynamicWriter.java
@@ -63,6 +63,7 @@ import static 
org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.GET;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INIT;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INTERFACE;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.METHOD;
+import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.SET;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
 import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
@@ -324,6 +325,42 @@ public class InvokeDynamicWriter extends InvocationWriter {
         finishIndyCall(BSM, GET.getCallSiteName(), descriptor, 1, 
propertyName, flags);
     }
 
+    /**
+     * Emits an {@code invokedynamic} property-write call site (GROOVY-12138).
+     * On entry, the assigned value sits on the operand stack (the assignment
+     * machinery evaluates the RHS before visiting the LHS). The value is
+     * boxed, the receiver pushed, and the two swapped so the call-site shape
+     * is receiver-first — {@code (ReceiverType, ValueType)void} — preserving
+     * the runtime invariant that {@code arguments[0]} is the receiver. The
+     * void return means no operand-stack result to clean up, matching the
+     * classic adapter path's accounting.
+     */
+    protected void writeSetProperty(final Expression receiver, final String 
propertyName, final boolean implicitThis, final boolean groovyObject) {
+        CompileStack compileStack = controller.getCompileStack();
+        OperandStack operandStack = controller.getOperandStack();
+
+        operandStack.box(); // the assigned value, pushed before the LHS visit
+
+        compileStack.pushLHS(false);
+        compileStack.pushImplicitThis(implicitThis);
+        visitReceiverOfMethodCall(receiver);
+        compileStack.popImplicitThis();
+
+        String receiverDescriptor = 
getTypeDescription(operandStack.getTopOperand());
+        operandStack.swap(); // ... value, receiver -> ... receiver, value
+        String valueDescriptor = 
getTypeDescription(operandStack.getTopOperand());
+        String descriptor = "(" + receiverDescriptor + valueDescriptor + ")V";
+
+        int flags = 0;
+        if (groovyObject) flags |= GROOVY_OBJECT;
+        if (implicitThis) flags |= IMPLICIT_THIS;
+        else if (isThisExpression(receiver)) flags |= THIS_CALL;
+        
controller.getMethodVisitor().visitInvokeDynamicInsn(SET.getCallSiteName(), 
descriptor, BSM, propertyName, flags);
+
+        operandStack.remove(2); // the indy instruction consumed receiver and 
value
+        compileStack.popLHS();
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void writeNormalConstructorCall(final ConstructorCallExpression 
call) {
diff --git a/src/main/java/org/codehaus/groovy/reflection/CachedField.java 
b/src/main/java/org/codehaus/groovy/reflection/CachedField.java
index 0a3b78f93c..77206de2de 100644
--- a/src/main/java/org/codehaus/groovy/reflection/CachedField.java
+++ b/src/main/java/org/codehaus/groovy/reflection/CachedField.java
@@ -143,4 +143,31 @@ public class CachedField extends MetaProperty {
             throw e;
         }
     }
+
+    /**
+     * Creates a method handle that writes this field via the MethodHandles 
API,
+     * the mutating counterpart of {@link 
#asAccessMethod(MethodHandles.Lookup)}.
+     * Attempts to unreflect the field, automatically making it accessible if 
needed.
+     *
+     * @param lookup the method handles lookup context
+     * @return a setter handle of type {@code (declaringClass, fieldType)void}
+     * @throws IllegalAccessException if the field cannot be accessed even 
with accessibility adjustments
+     * @since 6.0.0
+     */
+    public MethodHandle asWriteAccessMethod(final MethodHandles.Lookup lookup) 
throws IllegalAccessException {
+        try {
+            return lookup.unreflectSetter(field);
+        } catch (IllegalAccessException e) {
+            if (!madeAccessible) {
+                try {
+                    makeAccessible();
+                    return lookup.unreflectSetter(field);
+                } catch (IllegalAccessException ignore) {
+                } catch (Throwable t) {
+                    e.addSuppressed(t);
+                }
+            }
+            throw e;
+        }
+    }
 }
diff --git 
a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
 
b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
index b99a395159..816210d3d5 100644
--- 
a/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
+++ 
b/src/main/java/org/codehaus/groovy/vmplugin/v8/IndyGuardsFiltersAndSignatures.java
@@ -65,12 +65,12 @@ public class IndyGuardsFiltersAndSignatures {
             SAME_CLASS, SAME_CLASSES, SAME_MC, IS_NULL, NON_NULL,
             UNWRAP_METHOD, UNWRAP_EXCEPTION,
             HAS_CATEGORY_IN_CURRENT_THREAD_GUARD,
-            GROOVY_OBJECT_INVOKER, 
GROOVY_OBJECT_GET_PROPERTY,META_METHOD_INVOKER,
+            GROOVY_OBJECT_INVOKER, GROOVY_OBJECT_GET_PROPERTY, 
GROOVY_OBJECT_SET_PROPERTY, META_METHOD_INVOKER,
             META_CLASS_INVOKE_METHOD, META_CLASS_INVOKE_STATIC_METHOD,
             BEAN_CONSTRUCTOR_PROPERTY_SETTER,
             META_PROPERTY_GETTER,
             SLOW_META_CLASS_FIND,
-            MOP_GET, MOP_INVOKE_CONSTRUCTOR, MOP_INVOKE_METHOD,
+            MOP_GET, MOP_SET, SET_PROPERTY, MOP_INVOKE_CONSTRUCTOR, 
MOP_INVOKE_METHOD,
             INTERCEPTABLE_INVOKER,
             BOOLEAN_IDENTITY, CLASS_FOR_NAME,
             DTT_CAST_TO_TYPE, SAM_CONVERSION,
@@ -90,6 +90,7 @@ public class IndyGuardsFiltersAndSignatures {
             HAS_CATEGORY_IN_CURRENT_THREAD_GUARD = LOOKUP.findStatic 
(GroovyCategorySupport.class, "hasCategoryInCurrentThread", 
MethodType.methodType(boolean.class));
             GROOVY_OBJECT_INVOKER                = LOOKUP.findStatic 
(IndyGuardsFiltersAndSignatures.class, "invokeGroovyObjectInvoker", 
MethodType.methodType(Object.class, MissingMethodException.class, Object.class, 
String.class, Object[].class));
             GROOVY_OBJECT_GET_PROPERTY           = 
LOOKUP.findVirtual(GroovyObject.class, "getProperty", 
MethodType.methodType(Object.class, String.class));
+            GROOVY_OBJECT_SET_PROPERTY           = 
LOOKUP.findVirtual(GroovyObject.class, "setProperty", 
MethodType.methodType(void.class, String.class, Object.class));
             META_METHOD_INVOKER                  = 
LOOKUP.findVirtual(MetaMethod.class, "doMethodInvoke", 
MethodType.methodType(Object.class, Object.class, Object[].class));
             META_CLASS_INVOKE_METHOD             = 
LOOKUP.findVirtual(MetaClass.class, "invokeMethod", 
MethodType.methodType(Object.class, Class.class, Object.class, String.class, 
Object[].class, boolean.class, boolean.class));
             META_CLASS_INVOKE_STATIC_METHOD      = 
LOOKUP.findVirtual(MetaObjectProtocol.class, "invokeStaticMethod", 
MethodType.methodType(Object.class, Object.class, String.class, 
Object[].class));
@@ -97,6 +98,8 @@ public class IndyGuardsFiltersAndSignatures {
             META_PROPERTY_GETTER                 = 
LOOKUP.findVirtual(MetaProperty.class, "getProperty", OBJECT_FILTER);
             SLOW_META_CLASS_FIND                 = LOOKUP.findStatic 
(InvokerHelper.class, "getMetaClass", MethodType.methodType(MetaClass.class, 
Object.class));
             MOP_GET                              = 
LOOKUP.findVirtual(MetaObjectProtocol.class, "getProperty", 
MethodType.methodType(Object.class, Object.class, String.class));
+            MOP_SET                              = 
LOOKUP.findVirtual(MetaObjectProtocol.class, "setProperty", 
MethodType.methodType(void.class, Object.class, String.class, Object.class));
+            SET_PROPERTY                         = LOOKUP.findStatic 
(IndyGuardsFiltersAndSignatures.class, "setProperty", 
MethodType.methodType(void.class, String.class, Class.class, Object.class, 
Object.class));
             MOP_INVOKE_CONSTRUCTOR               = 
LOOKUP.findVirtual(MetaObjectProtocol.class, "invokeConstructor", 
MethodType.methodType(Object.class, Object[].class));
             MOP_INVOKE_METHOD                    = 
LOOKUP.findVirtual(MetaObjectProtocol.class, "invokeMethod", 
MethodType.methodType(Object.class, Object.class, String.class, 
Object[].class));
             INTERCEPTABLE_INVOKER                = 
LOOKUP.findVirtual(GroovyObject.class, "invokeMethod", 
MethodType.methodType(Object.class, String.class, Object.class));
@@ -120,6 +123,21 @@ public class IndyGuardsFiltersAndSignatures {
      */
     protected static final MethodHandle NULL_REF = 
MethodHandles.constant(Object.class, null);
 
+    /**
+     * Adapter shim for the non-fast-path property write of the indy
+     * {@code SetPropertySelector}. The parameter order — {@code name},
+     * {@code sender}, {@code receiver}, {@code value} — is chosen so that
+     * binding the compile-time-known {@code name} and {@code sender} as a
+     * leading prefix with a single {@link MethodHandles#insertArguments} call
+     * leaves exactly the call-site shape {@code (receiver, value)void}, with 
no
+     * permute required. For now it just reorders into
+     * {@link ScriptBytecodeAdapter#setProperty}; this is also the natural seam
+     * for inlining that resolution logic in the future.
+     */
+    public static void setProperty(String name, Class<?> sender, Object 
receiver, Object value) throws Throwable {
+        ScriptBytecodeAdapter.setProperty(value, sender, receiver, name);
+    }
+
     /**
      * This method is called by the handle to realize the bean constructor
      * with property map.
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 d3e55ea466..9f31e9f43c 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java
@@ -22,6 +22,8 @@ import groovy.lang.Closure;
 import groovy.lang.ExpandoMetaClass;
 import groovy.lang.GroovyInterceptable;
 import groovy.lang.GroovyObject;
+import groovy.lang.MetaBeanProperty;
+import groovy.lang.MetaProperty;
 import groovy.lang.GroovyRuntimeException;
 import groovy.lang.GroovySystem;
 import groovy.lang.MetaClass;
@@ -91,6 +93,7 @@ import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.SAM
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.SAME_CLASSES;
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.SAME_MC;
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.SAM_CONVERSION;
+import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.SET_PROPERTY;
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.UNWRAP_EXCEPTION;
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.UNWRAP_METHOD;
 import static 
org.codehaus.groovy.vmplugin.v8.IndyGuardsFiltersAndSignatures.unwrap;
@@ -178,7 +181,7 @@ public abstract class Selector {
             case INIT      -> new InitSelector(callSite, sender, methodName, 
callType, safeNavigation, thisCall, spreadCall, arguments);
             case METHOD    -> new MethodSelector(callSite, sender, methodName, 
callType, safeNavigation, thisCall, spreadCall, arguments);
             case GET       -> new PropertySelector(callSite, sender, 
methodName, callType, safeNavigation, thisCall, spreadCall, arguments);
-            case SET       -> throw new GroovyBugError("your call tried to do 
a property set, which is not supported.");
+            case SET       -> new SetPropertySelector(callSite, sender, 
methodName, callType, safeNavigation, thisCall, spreadCall, arguments);
             case CAST      -> new CastSelector(callSite, sender, methodName, 
arguments);
             case INTERFACE -> new InterfaceSelector(callSite, sender, 
methodName, callType, safeNavigation, thisCall, spreadCall, arguments);
             default        -> throw new GroovyBugError("unexpected call type");
@@ -441,6 +444,155 @@ public abstract class Selector {
         }
     }
 
+    /**
+     * Property-write based {@link Selector} (GROOVY-12138). Call sites have 
the
+     * shape {@code (receiver, value)void}; {@code args[0]} is the receiver and
+     * {@code args[1]} the value being assigned, so the standard guard 
machinery
+     * (receiver metaclass identity, switch point, argument classes) applies
+     * unchanged — the value-class guard triggers re-selection when the 
assigned
+     * type changes.
+     * <p>
+     * The fast path covers the plain cases only: a non-static
+     * {@link MetaBeanProperty} whose setter accepts the runtime value type
+     * directly, or a writable field of matching type. Everything else — type
+     * coercion, {@code propertyMissing}, maps, static and expando properties —
+     * falls through to {@link MetaObjectProtocol#setProperty}, which is 
exactly
+     * the path all writes took before this selector existed.
+     */
+    private static class SetPropertySelector extends MethodSelector {
+
+        private static final Class<?>[] SET_PROPERTY_PARAMS = {String.class, 
Object.class};
+
+        public SetPropertySelector(CacheableCallSite callSite, Class<?> 
sender, String propertyName, CallType callType, boolean safeNavigation, boolean 
thisCall, boolean spreadCall, Object[] arguments) {
+            super(callSite, sender, propertyName, callType, safeNavigation, 
thisCall, spreadCall, arguments);
+        }
+
+        /**
+         * Property writes are not routed through {@code invokeMethod}, thus 
always returns false.
+         */
+        @Override
+        public boolean setInterceptor() {
+            return false;
+        }
+
+        /**
+         * Chooses the setter or field for a property write from the metaclass.
+         * The fast path is restricted to shapes whose resolution provably does
+         * not depend on the sender (see {@code MetaClassImpl#setProperty}'s
+         * field-vs-setter precedence, GROOVY-8283) or on receiver kind (maps,
+         * GROOVY-8065/GROOVY-11367; overridden {@code setProperty}). Anything
+         * else keeps the exact classic behavior via the adapter fallback in
+         * {@link #setMetaClassCallHandleIfNeeded}.
+         */
+        @Override
+        public void chooseMeta(MetaClassImpl mci) {
+            if (method != null || mci == null) return;
+            // ExpandoMetaClass (also produced by Class.mixin) overrides
+            // setProperty itself; only a plain MetaClassImpl resolution is
+            // safe to mirror here — mirroring the GET selector, which also
+            // keeps EMC off its fast metaclass path
+            if (mci.getClass() != MetaClassImpl.class) return;
+            Object receiver = getCorrectedReceiver();
+            if (receiver instanceof Class || receiver instanceof 
java.util.Map) return;
+            if (GroovyCategorySupport.hasCategoryInCurrentThread()) return; // 
categories can contribute setters
+
+            // a setProperty(String,Object) contributed by the class itself, a
+            // mixin, or an expando closure intercepts every write; only the
+            // GroovyObject interface default is the benign non-intercepting
+            // case (its behavior is the metaclass path this selector mirrors).
+            // Two complementary probes: reflection sees compiled overrides
+            // (including inherited ones), the metaclass sees mixin/expando
+            // contributions that reflection cannot.
+            if (receiver instanceof GroovyObject && 
hasOverriddenSetProperty(receiver.getClass())) return;
+            MetaMethod customSetProperty = mci.pickMethod("setProperty", 
SET_PROPERTY_PARAMS);
+            if (customSetProperty != null
+                    && customSetProperty.getDeclaringClass().getTheClass() != 
GroovyObject.class) {
+                return;
+            }
+
+            MetaProperty mp = mci.getMetaProperty(name);
+            Object value = args[1];
+            if (mp instanceof MetaBeanProperty mbp && !mp.isStatic()) {
+                MetaMethod setter = mbp.getSetter();
+                CachedField field = mbp.getField();
+                if (setter != null) {
+                    // a non-private backing field can take precedence over the
+                    // setter depending on the sender (GROOVY-8283): only the
+                    // private-or-absent-field shape is sender-independent
+                    if ((field == null || field.isPrivate()) && 
acceptsDirectly(setter, value)) {
+                        method = setter;
+                        if (LOG_ENABLED) LOG.info("direct setter selected for 
property write: " + setter);
+                    }
+                    return;
+                }
+                if (field != null) {
+                    setFieldWriteHandle(field, value);
+                }
+            } else if (mp instanceof CachedField cf && !mp.isStatic()) {
+                setFieldWriteHandle(cf, value);
+            }
+            // otherwise (no meta property, listeners, expando, 
propertyMissing, ...): adapter path
+        }
+
+        /**
+         * Mirrors the receiver test of {@code 
ScriptBytecodeAdapter#setProperty}:
+         * a {@code GroovyObject} whose {@code setProperty} is a real compiled
+         * override (not the interface default) intercepts all writes.
+         */
+        private static boolean hasOverriddenSetProperty(Class<?> 
receiverClass) {
+            try {
+                return !receiverClass.getMethod("setProperty", String.class, 
Object.class).isDefault();
+            } catch (ReflectiveOperationException ignore) {
+                return true; // cannot decide: stay on the adapter path
+            }
+        }
+
+        private boolean acceptsDirectly(MetaMethod setter, Object value) {
+            var parameterTypes = setter.getParameterTypes();
+            if (parameterTypes.length != 1 || setter.isVargsMethod()) return 
false;
+            if (!Modifier.isPublic(setter.getModifiers())) return false;
+            Class<?> parameterType = parameterTypes[0].getTheClass();
+            if (value == null) return !parameterType.isPrimitive();
+            return TypeHelper.getWrapperClass(parameterType).isInstance(value);
+        }
+
+        private void setFieldWriteHandle(CachedField field, Object value) {
+            // only public fields are sender-independent; the rest go through
+            // the sender-aware adapter path
+            if (!Modifier.isPublic(field.getModifiers()) || 
Modifier.isFinal(field.getModifiers())) return;
+            Class<?> fieldType = field.getType();
+            boolean accepts = (value == null) ? !fieldType.isPrimitive()
+                                              : 
TypeHelper.getWrapperClass(fieldType).isInstance(value);
+            if (!accepts) return; // needs coercion: adapter path
+            try {
+                // like the property-get field path: lookup for the sender, 
then unreflect
+                @SuppressWarnings("removal")
+                MethodHandles.Lookup lookup = ((Java8) 
VMPluginFactory.getPlugin()).newLookup(sender);
+                handle = field.asWriteAccessMethod(lookup);
+                if (LOG_ENABLED) LOG.info("direct field write handle set for 
property write");
+            } catch (IllegalAccessException e) {
+                throw new GroovyBugError(e);
+            }
+        }
+
+        /**
+         * All remaining property writes go through the exact classic path:
+         * {@code ScriptBytecodeAdapter.setProperty(value, sender, receiver, 
name)},
+         * bound with this call site's sender so sender-aware resolution
+         * (private members from inside the declaring class, precedence rules,
+         * maps, closure retry semantics) is preserved byte-for-byte.
+         */
+        @Override
+        public void setMetaClassCallHandleIfNeeded(boolean standardMetaClass) {
+            if (handle != null) return;
+            useMetaClass = true;
+            if (LOG_ENABLED) LOG.info("set adapter invocation path for 
property set.");
+            // SET_PROPERTY: (String name, Class sender, Object receiver, 
Object value)void
+            // binding the name/sender prefix leaves the call-site shape 
(receiver, value)void
+            handle = MethodHandles.insertArguments(SET_PROPERTY, 0, name, 
sender);
+        }
+    }
+
     private static class InitSelector extends MethodSelector {
         private static final MethodType MT_OBJECT = 
MethodType.methodType(Object.class);
         private boolean beanConstructor;

Reply via email to