Skip to content

Commit e4123d9

Browse files
committed
GROOVY-7785: StackoverflowException when using too many chained method calls
(cherry picked from commit f1d6e5f)
1 parent 8acb53b commit e4123d9

2 files changed

Lines changed: 391 additions & 8 deletions

File tree

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

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
2626
import org.codehaus.groovy.ast.expr.EmptyExpression;
2727
import org.codehaus.groovy.ast.expr.Expression;
28+
import org.codehaus.groovy.ast.expr.MethodCallExpression;
2829
import org.codehaus.groovy.ast.expr.PropertyExpression;
2930
import org.codehaus.groovy.ast.tools.WideningCategories;
3031
import org.codehaus.groovy.classgen.AsmClassGenerator;
@@ -42,8 +43,11 @@
4243
import java.lang.invoke.CallSite;
4344
import java.lang.invoke.MethodHandles.Lookup;
4445
import java.lang.invoke.MethodType;
46+
import java.util.ArrayDeque;
47+
import java.util.Deque;
4548
import java.util.List;
4649

50+
import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
4751
import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression;
4852
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
4953
import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
@@ -54,16 +58,16 @@
5458
import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
5559
import static org.codehaus.groovy.classgen.asm.BytecodeHelper.doCast;
5660
import static org.codehaus.groovy.classgen.asm.BytecodeHelper.getTypeDescription;
57-
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
58-
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
59-
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
60-
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SPREAD_CALL;
61-
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.THIS_CALL;
6261
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.CAST;
6362
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.GET;
6463
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INIT;
6564
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INTERFACE;
6665
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.METHOD;
66+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
67+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
68+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
69+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SPREAD_CALL;
70+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.THIS_CALL;
6771
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
6872
import static org.objectweb.asm.Opcodes.IFNULL;
6973

@@ -113,12 +117,87 @@ private String prepareIndyCall(final Expression receiver, final boolean implicit
113117

114118
// load normal receiver as first argument
115119
compileStack.pushImplicitThis(implicitThis);
116-
receiver.visit(controller.getAcg());
120+
// GROOVY-7785: use iterative approach to avoid stack overflow for chained method calls
121+
visitReceiverOfMethodCall(receiver);
117122
compileStack.popImplicitThis();
118123

119124
return "(" + getTypeDescription(operandStack.getTopOperand());
120125
}
121126

127+
/**
128+
* Visits receiver expression, using iterative approach for method call chains.
129+
* GROOVY-7785: Flattens deep recursive AST structures to avoid stack overflow.
130+
*/
131+
private void visitReceiverOfMethodCall(final Expression receiver) {
132+
// Collect chain of simple method calls that can use indy optimization
133+
Deque<MethodCallExpression> chain = new ArrayDeque<>();
134+
Expression current = receiver;
135+
while (current instanceof MethodCallExpression) {
136+
MethodCallExpression mce = (MethodCallExpression) current;
137+
if (mce.isSpreadSafe() || mce.isImplicitThis() || isSuperExpression(mce.getObjectExpression()) || isThisExpression(mce.getObjectExpression())) {
138+
break;
139+
}
140+
String name = getMethodName(mce.getMethod());
141+
if (name == null || "call".equals(name)) break; // dynamic name or functional interface call
142+
chain.push(mce);
143+
current = mce.getObjectExpression();
144+
}
145+
146+
AsmClassGenerator acg = controller.getAcg();
147+
current.visit(acg);
148+
149+
while (!chain.isEmpty()) {
150+
MethodCallExpression call = chain.pop();
151+
acg.onLineNumber(call, "visitMethodCallExpression: \"" + call.getMethod() + "\":");
152+
finishIndyCallForChain(call);
153+
controller.getAssertionWriter().record(call.getMethod());
154+
}
155+
}
156+
157+
/**
158+
* Completes an indy call for a chained method with receiver already on stack.
159+
*/
160+
private void finishIndyCallForChain(final MethodCallExpression call) {
161+
OperandStack operandStack = controller.getOperandStack();
162+
AsmClassGenerator acg = controller.getAcg();
163+
Expression arguments = call.getArguments();
164+
boolean safe = call.isSafe();
165+
166+
StringBuilder sig = new StringBuilder("(").append(getTypeDescription(operandStack.getTopOperand()));
167+
Label end = null;
168+
if (safe && !isPrimitiveType(operandStack.getTopOperand())) {
169+
operandStack.dup();
170+
end = operandStack.jump(IFNULL);
171+
}
172+
173+
int nArgs = 1;
174+
List<Expression> args = makeArgumentList(arguments).getExpressions();
175+
boolean spread = AsmClassGenerator.containsSpreadExpression(arguments);
176+
if (spread) {
177+
acg.despreadList(args, true);
178+
sig.append(getTypeDescription(Object[].class));
179+
} else {
180+
for (Expression arg : args) {
181+
arg.visit(acg);
182+
if (arg instanceof CastExpression) {
183+
operandStack.box();
184+
acg.loadWrapper(arg);
185+
sig.append(getTypeDescription(Wrapper.class));
186+
} else {
187+
sig.append(getTypeDescription(operandStack.getTopOperand()));
188+
}
189+
nArgs++;
190+
}
191+
}
192+
sig.append(")Ljava/lang/Object;");
193+
194+
int flags = safe ? SAFE_NAVIGATION : 0;
195+
if (spread) flags |= SPREAD_CALL;
196+
controller.getMethodVisitor().visitInvokeDynamicInsn(METHOD.getCallSiteName(), sig.toString(), BSM, getMethodName(call.getMethod()), flags);
197+
operandStack.replace(OBJECT_TYPE, nArgs);
198+
if (end != null) controller.getMethodVisitor().visitLabel(end);
199+
}
200+
122201
private void finishIndyCall(final Handle bsmHandle, final String methodName, final String sig, final int numberOfArguments, final Object... bsmArgs) {
123202
CompileStack compileStack = controller.getCompileStack();
124203
OperandStack operandStack = controller.getOperandStack();

0 commit comments

Comments
 (0)