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;