Skip to content

Commit 4cde1dc

Browse files
committed
GEP-15: avoid double dispatch
1 parent e4a9bb8 commit 4cde1dc

5 files changed

Lines changed: 554 additions & 6 deletions

File tree

src/main/java/org/codehaus/groovy/classgen/asm/indy/IndyBinHelper.java

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,48 @@
1818
*/
1919
package org.codehaus.groovy.classgen.asm.indy;
2020

21+
import org.codehaus.groovy.ast.ClassNode;
22+
import org.codehaus.groovy.ast.expr.BinaryExpression;
2123
import org.codehaus.groovy.ast.expr.ConstantExpression;
2224
import org.codehaus.groovy.ast.expr.EmptyExpression;
2325
import org.codehaus.groovy.ast.expr.Expression;
2426
import org.codehaus.groovy.ast.expr.MethodCallExpression;
27+
import org.codehaus.groovy.classgen.AsmClassGenerator;
2528
import org.codehaus.groovy.classgen.asm.BinaryExpressionHelper;
29+
import org.codehaus.groovy.classgen.asm.CompileStack;
2630
import org.codehaus.groovy.classgen.asm.InvocationWriter;
31+
import org.codehaus.groovy.classgen.asm.OperandStack;
2732
import org.codehaus.groovy.classgen.asm.WriterController;
33+
import org.codehaus.groovy.vmplugin.v8.IndyCompoundAssign;
34+
import org.codehaus.groovy.vmplugin.v8.IndyInterface;
35+
import org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType;
36+
import org.objectweb.asm.Handle;
37+
import org.objectweb.asm.MethodVisitor;
38+
39+
import java.lang.invoke.CallSite;
40+
import java.lang.invoke.MethodHandles.Lookup;
41+
import java.lang.invoke.MethodType;
42+
43+
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
44+
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
45+
import static org.codehaus.groovy.classgen.asm.BytecodeHelper.getTypeDescription;
46+
import static org.codehaus.groovy.syntax.Types.LEFT_SQUARE_BRACKET;
47+
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
2848

2949
/**
3050
* Binary-expression helper that routes prefix and postfix operations through the indy invocation path.
3151
*/
3252
public class IndyBinHelper extends BinaryExpressionHelper {
3353

54+
// GEP-15: compound-assignment rides the shared IndyInterface bootstrap as its
55+
// own CallType, so it inherits the standard call-site lifecycle/PIC.
56+
private static final String BSM_DESCRIPTOR = MethodType.methodType(
57+
CallSite.class, Lookup.class, String.class, MethodType.class, String.class, int.class
58+
).toMethodDescriptorString();
59+
60+
private static final Handle BSM = new Handle(H_INVOKESTATIC,
61+
IndyInterface.class.getName().replace('.', '/'), "bootstrap", BSM_DESCRIPTOR, false);
62+
3463
/**
3564
* Creates an indy-aware binary-expression helper.
3665
*/
@@ -42,10 +71,71 @@ public IndyBinHelper(WriterController wc) {
4271
@Override
4372
protected void writePostOrPrefixMethod(int op, String method, Expression expression, Expression orig) {
4473
getController().getInvocationWriter().makeCall(
45-
orig, EmptyExpression.INSTANCE,
46-
new ConstantExpression(method),
47-
MethodCallExpression.NO_ARGUMENTS,
48-
InvocationWriter.invokeMethod,
74+
orig, EmptyExpression.INSTANCE,
75+
new ConstantExpression(method),
76+
MethodCallExpression.NO_ARGUMENTS,
77+
InvocationWriter.invokeMethod,
4978
false, false, false);
5079
}
80+
81+
/**
82+
* GEP-15: emit {@code invokedynamic} to {@link IndyInterface#bootstrap}
83+
* with the {@link CallType#COMPOUND_ASSIGN} call type (resolved by
84+
* {@link IndyCompoundAssign}), replacing the uncached static call into
85+
* {@code ScriptBytecodeAdapter.compoundAssign}. Subscript LHS (e.g.
86+
* {@code a[i] += b}) is out of GEP-15 scope and stays on the legacy path.
87+
*/
88+
@Override
89+
protected void evaluateCompoundAssign(final String assignName, final String baseName, final BinaryExpression expression) {
90+
Expression leftExpression = expression.getLeftExpression();
91+
if (leftExpression instanceof BinaryExpression bexp
92+
&& bexp.getOperation().getType() == LEFT_SQUARE_BRACKET) {
93+
super.evaluateCompoundAssign(assignName, baseName, expression); // legacy getAt/putAt path
94+
return;
95+
}
96+
97+
WriterController controller = getController();
98+
AsmClassGenerator acg = controller.getAcg();
99+
OperandStack operandStack = controller.getOperandStack();
100+
CompileStack compileStack = controller.getCompileStack();
101+
MethodVisitor mv = controller.getMethodVisitor();
102+
Expression rightExpression = expression.getRightExpression();
103+
104+
// Primitive fast path: when both operands are statically primitive, pass
105+
// them UNBOXED (descriptor built from their types) instead of forcing the
106+
// (Object,Object)Object site. The return stays Object, so the store-back
107+
// coercion below is unchanged — no widening/narrowing rules, every operator
108+
// stays correct — we merely save the operand boxing the Object site forces.
109+
ClassNode lhsType = controller.getTypeChooser().resolveType(leftExpression, controller.getClassNode());
110+
ClassNode rhsType = controller.getTypeChooser().resolveType(rightExpression, controller.getClassNode());
111+
boolean primitiveOperands = isPrimitiveType(lhsType) && isPrimitiveType(rhsType);
112+
113+
compileStack.pushLHS(false);
114+
leftExpression.visit(acg);
115+
if (!primitiveOperands) operandStack.box();
116+
ClassNode receiverType = operandStack.getTopOperand();
117+
rightExpression.visit(acg);
118+
if (!primitiveOperands) operandStack.box();
119+
ClassNode argType = operandStack.getTopOperand();
120+
compileStack.popLHS();
121+
122+
// callType name selects COMPOUND_ASSIGN; the bootstrap's name constant
123+
// carries both operator names packed together; flags are unused (0). Only
124+
// the both-primitive case specialises the descriptor; otherwise keep the
125+
// plain (Object,Object)Object site the resolver's guards are built for
126+
// (box() is a no-op for reference types, so reference operands must not
127+
// leak their static subtype into the descriptor).
128+
String descriptor = primitiveOperands
129+
? "(" + getTypeDescription(receiverType) + getTypeDescription(argType) + ")Ljava/lang/Object;"
130+
: "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;";
131+
mv.visitInvokeDynamicInsn(CallType.COMPOUND_ASSIGN.getCallSiteName(), descriptor,
132+
BSM, IndyCompoundAssign.packNames(assignName, baseName), 0);
133+
operandStack.replace(OBJECT_TYPE, 2);
134+
135+
// Store the returned value back into the LHS and leave it as the expression value.
136+
operandStack.dup();
137+
compileStack.pushLHS(true);
138+
leftExpression.visit(acg);
139+
compileStack.popLHS();
140+
}
51141
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.vmplugin.v8;
20+
21+
import groovy.lang.MetaClass;
22+
import org.codehaus.groovy.GroovyBugError;
23+
import org.codehaus.groovy.reflection.ClassInfo;
24+
import org.codehaus.groovy.runtime.InvokerHelper;
25+
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
26+
27+
import java.lang.invoke.MethodHandle;
28+
import java.lang.invoke.MethodHandles;
29+
import java.lang.invoke.MethodType;
30+
31+
/**
32+
* GEP-15: resolver for dynamic compound-assignment operators
33+
* ({@code +=}, {@code -=}, ...).
34+
*
35+
* <p>This class holds only the GEP-15-specific <em>policy</em>; the call-site
36+
* <em>lifecycle</em> is owned by {@link IndyInterface}. A compound-assignment
37+
* {@code op=} is emitted as an {@code invokedynamic} to
38+
* {@link IndyInterface#bootstrap} with call type
39+
* {@link IndyInterface.CallType#COMPOUND_ASSIGN}, so it rides the same boot
40+
* handle, per-receiver-class inline cache, monomorphic-promotion and
41+
* deopt-storm protection as a normal method call. {@link IndyInterface#fallback}
42+
* routes resolution here via {@link #resolve}.
43+
*
44+
* <p>What remains GEP-15-specific:
45+
* <ul>
46+
* <li>the two operator names ({@code assignName}/{@code baseName}) packed into
47+
* the bootstrap {@code name} and unpacked here;</li>
48+
* <li>a {@code respondsTo} probe to pick {@code *Assign} vs base — run only on
49+
* a cache miss;</li>
50+
* <li>the in-place return-receiver composition for the assign branch;</li>
51+
* <li>a {@code (receiver class, arg class)} guard (arg class is part of the
52+
* key so overloads stay correct), under the shared MOP switch point;</li>
53+
* <li>a generic fall back to {@link ScriptBytecodeAdapter#compoundAssign} for
54+
* null/unresolved receivers.</li>
55+
* </ul>
56+
*
57+
* <p>The actual invocation is built by {@link Selector#selectInvokeHandle}, so
58+
* selection/coercion/vargs/category/exception handling match a normal call.
59+
*
60+
* <p>WARNING: internal, indy-only. Not for use outside this package.
61+
*
62+
* @since 6.0.0
63+
*/
64+
public final class IndyCompoundAssign {
65+
66+
/** Separator packing {@code assignName} and {@code baseName} into one bootstrap constant (NUL cannot appear in a JVM method name, even a Groovy quoted one). */
67+
public static final char NAME_SEPARATOR = '\u0000';
68+
69+
private static final MethodHandle GUARD; // (Class,Class,Object,Object) -> boolean
70+
private static final MethodHandle COMPOUND_ASSIGN; // (Object,Object,String,String) -> Object
71+
72+
/** (Object result, Object receiver, Object arg) -> receiver; folded over the assign invoke. */
73+
private static final MethodHandle RETURN_RECEIVER;
74+
75+
static {
76+
try {
77+
MethodHandles.Lookup l = MethodHandles.lookup();
78+
GUARD = l.findStatic(IndyCompoundAssign.class, "guard",
79+
MethodType.methodType(boolean.class, Class.class, Class.class, Object.class, Object.class));
80+
COMPOUND_ASSIGN = l.findStatic(ScriptBytecodeAdapter.class, "compoundAssign",
81+
MethodType.methodType(Object.class, Object.class, Object.class, String.class, String.class));
82+
83+
MethodHandle pick = MethodHandles.identity(Object.class); // (receiver) -> receiver
84+
pick = MethodHandles.dropArguments(pick, 1, Object.class); // (receiver, arg) -> receiver
85+
RETURN_RECEIVER = MethodHandles.dropArguments(pick, 0, Object.class); // (result, receiver, arg) -> receiver
86+
} catch (ReflectiveOperationException e) {
87+
throw new GroovyBugError(e);
88+
}
89+
}
90+
91+
private IndyCompoundAssign() {
92+
}
93+
94+
/**
95+
* Packs the two operator names into a single bootstrap {@code name} constant.
96+
* Called from code generation.
97+
*/
98+
public static String packNames(final String assignName, final String baseName) {
99+
return assignName + NAME_SEPARATOR + baseName;
100+
}
101+
102+
/**
103+
* Resolves the invocation for one receiver/arg shape and returns a
104+
* {@link MethodHandleWrapper} for {@link IndyInterface}'s inline cache. The
105+
* wrapper's target handle is {@code (Object receiver, Object arg) -> Object},
106+
* guarded on the receiver/arg classes and the shared MOP switch point with
107+
* the call site's fallback (re-resolve) path as the else-branch.
108+
*
109+
* @param callSite the compound-assignment call site (type {@code (Object,Object)->Object})
110+
* @param sender the sending class
111+
* @param packedNames {@code assignName} and {@code baseName} joined by {@link #NAME_SEPARATOR}
112+
* @param arguments the runtime arguments: {@code [receiver, arg]}
113+
*/
114+
public static MethodHandleWrapper resolve(final CacheableCallSite callSite, final Class<?> sender,
115+
final String packedNames, final Object[] arguments) {
116+
int sep = packedNames.indexOf(NAME_SEPARATOR);
117+
if (sep < 0) throw new GroovyBugError("compound-assign bootstrap name not packed with NAME_SEPARATOR: " + packedNames);
118+
String assignName = packedNames.substring(0, sep);
119+
String baseName = packedNames.substring(sep + 1);
120+
121+
Object receiver = arguments[0];
122+
Object arg = arguments[1];
123+
124+
// Primitive fast path: codegen only emits a primitive-typed site when BOTH
125+
// operands are primitive, so the receiver is a genuine primitive (no user
126+
// *Assign, no per-instance metaclass, no receiver/arg polymorphism). Require
127+
// both params primitive here to match that invariant — then resolving the
128+
// base operator directly (IndyMath gives a primitive handle via Selector)
129+
// under the MOP switch point only, and caching unconditionally, is sound.
130+
MethodType st = callSite.type();
131+
if (st.parameterType(0).isPrimitive() && st.parameterType(1).isPrimitive()) {
132+
MethodHandle invoke = Selector.selectInvokeHandle(callSite, sender, baseName, arguments);
133+
MethodHandle guarded = IndyInterface.switchPoint.guardWithTest(invoke, callSite.getFallbackTarget());
134+
return wrap(guarded, true);
135+
}
136+
137+
if (receiver == null) return genericWrapper(assignName, baseName); // legacy helper handles null receiver
138+
139+
MetaClass mc = InvokerHelper.getMetaClass(receiver);
140+
boolean useAssign;
141+
String name;
142+
if (!mc.respondsTo(receiver, assignName, new Object[]{arg}).isEmpty()) {
143+
useAssign = true;
144+
name = assignName;
145+
} else if (!mc.respondsTo(receiver, baseName, new Object[]{arg}).isEmpty()) {
146+
useAssign = false;
147+
name = baseName;
148+
} else {
149+
return genericWrapper(assignName, baseName); // neither responds: legacy helper raises MissingMethodException
150+
}
151+
152+
MethodHandle invoke = Selector.selectInvokeHandle(callSite, sender, name, new Object[]{receiver, arg});
153+
if (useAssign) invoke = MethodHandles.foldArguments(RETURN_RECEIVER, invoke);
154+
155+
// Guard on (receiver class, arg class); else-branch re-resolves via the
156+
// call site's fallback (select) path, overwriting the inline cache entry.
157+
MethodHandle elseTarget = callSite.getFallbackTarget();
158+
Class<?> rc = receiver.getClass();
159+
Class<?> ac = (arg == null) ? null : arg.getClass();
160+
MethodHandle test = MethodHandles.insertArguments(GUARD, 0, rc, ac);
161+
MethodHandle guarded = MethodHandles.guardWithTest(test, invoke, elseTarget);
162+
// a metaclass change invalidates the site, exactly as for a normal indy call.
163+
guarded = IndyInterface.switchPoint.guardWithTest(guarded, elseTarget);
164+
165+
// Per-instance metaclasses make a class-keyed cache unsound, so mark such
166+
// shapes uncacheable (the wrapper is still used for the current call) —
167+
// mirrors Selector's own cacheability rule.
168+
boolean cacheable = !ClassInfo.getClassInfo(rc).hasPerInstanceMetaClasses();
169+
return wrap(guarded, cacheable);
170+
}
171+
172+
/** Uncacheable wrapper delegating to the legacy helper (identical semantics for the declined shape). */
173+
private static MethodHandleWrapper genericWrapper(final String assignName, final String baseName) {
174+
MethodHandle generic = MethodHandles.insertArguments(COMPOUND_ASSIGN, 2, assignName, baseName);
175+
return wrap(generic, false);
176+
}
177+
178+
/** Wraps a {@code (Object,Object)->Object} target into the cached + relink form IndyInterface expects. */
179+
private static MethodHandleWrapper wrap(final MethodHandle target, final boolean cacheable) {
180+
MethodHandle cached = target.asSpreader(Object[].class, 2).asType(MethodType.methodType(Object.class, Object[].class));
181+
return new MethodHandleWrapper(cached, target, null, cacheable);
182+
}
183+
184+
/** Guard: receiver and argument runtime classes both match the cached shape. */
185+
@SuppressWarnings("unused")
186+
public static boolean guard(final Class<?> rc, final Class<?> ac, final Object receiver, final Object arg) {
187+
if (receiver == null || receiver.getClass() != rc) return false;
188+
return (ac == null) ? (arg == null) : (arg != null && arg.getClass() == ac);
189+
}
190+
}
666 Bytes
Binary file not shown.

src/main/java/org/codehaus/groovy/vmplugin/v8/Selector.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,22 @@ public boolean setInterceptor() {
11911191
*/
11921192
@Override
11931193
public void setCallSiteTarget() {
1194+
buildInvokeHandle();
1195+
setGuards(args[0]);
1196+
doCallSiteTargetSet();
1197+
}
1198+
1199+
/**
1200+
* Builds the (unguarded) invocation handle into {@link #handle}: select the
1201+
* metaclass and target method, make a handle, and apply the vargs/coercion/
1202+
* wrapping/null-receiver/spreading/exception transformations. This is the
1203+
* portion of {@link #setCallSiteTarget()} before guard installation; it is
1204+
* factored out so the GEP-15 compound-assignment path can reuse Selector's
1205+
* real method selection while managing its own guarding and caching shell.
1206+
*
1207+
* @see #selectInvokeHandle(CacheableCallSite, Class, String, Object[])
1208+
*/
1209+
void buildInvokeHandle() {
11941210
if (!setNullForSafeNavigation() && !setInterceptor()) {
11951211
getMetaClass();
11961212
setSelectionBase();
@@ -1209,11 +1225,32 @@ public void setCallSiteTarget() {
12091225

12101226
addExceptionHandler();
12111227
}
1212-
setGuards(args[0]);
1213-
doCallSiteTargetSet();
12141228
}
12151229
}
12161230

1231+
/**
1232+
* GEP-15 support: builds the <em>unguarded</em> invocation handle that a normal
1233+
* method call site would use for {@code methodName} on the given receiver/args
1234+
* ({@code arguments[0]} is the receiver). The result has type
1235+
* {@code callSite.type()}. Guard and switch-point wrapping are intentionally
1236+
* omitted — the caller (compound-assignment) applies its own per-shape guard
1237+
* and shares the global MOP {@link IndyInterface#switchPoint}. The caller must
1238+
* have already established that {@code methodName} resolves for this receiver
1239+
* (e.g. via {@code respondsTo}); this routes the actual invocation through the
1240+
* same selection, coercion and wrapping path as a normal call.
1241+
*
1242+
* @param callSite a call site supplying the desired {@code (receiver,arg)->Object} type
1243+
* @param sender the sending class for visibility/MOP decisions
1244+
* @param methodName the resolved method name (the chosen {@code *Assign} or base operator)
1245+
* @param arguments the runtime arguments, receiver first
1246+
* @return the unguarded invocation handle of type {@code callSite.type()}
1247+
*/
1248+
static MethodHandle selectInvokeHandle(CacheableCallSite callSite, Class<?> sender, String methodName, Object[] arguments) {
1249+
MethodSelector selector = new MethodSelector(callSite, sender, methodName, CallType.METHOD, Boolean.FALSE, Boolean.FALSE, Boolean.FALSE, arguments);
1250+
selector.buildInvokeHandle();
1251+
return selector.handle;
1252+
}
1253+
12171254
//--------------------------------------------------------------------------
12181255

12191256
/**

0 commit comments

Comments
 (0)