Skip to content

Commit 812e18a

Browse files
committed
GROOVY-7785: StackoverflowException when using too many chained method calls
1 parent a1897e0 commit 812e18a

File tree

2 files changed

+386
-10
lines changed

2 files changed

+386
-10
lines changed

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

Lines changed: 83 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,85 @@ 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+
for (; current instanceof MethodCallExpression mce
136+
&& !mce.isSpreadSafe() && !mce.isImplicitThis()
137+
&& !isSuperExpression(mce.getObjectExpression())
138+
&& !isThisExpression(mce.getObjectExpression()); current = mce.getObjectExpression()) {
139+
String name = getMethodName(mce.getMethod());
140+
if (name == null || "call".equals(name)) break; // dynamic name or functional interface call
141+
chain.push(mce);
142+
}
143+
144+
AsmClassGenerator acg = controller.getAcg();
145+
current.visit(acg);
146+
147+
while (!chain.isEmpty()) {
148+
MethodCallExpression call = chain.pop();
149+
acg.onLineNumber(call, "visitMethodCallExpression: \"" + call.getMethod() + "\":");
150+
finishIndyCallForChain(call);
151+
controller.getAssertionWriter().record(call.getMethod());
152+
}
153+
}
154+
155+
/**
156+
* Completes an indy call for a chained method with receiver already on stack.
157+
*/
158+
private void finishIndyCallForChain(final MethodCallExpression call) {
159+
OperandStack operandStack = controller.getOperandStack();
160+
AsmClassGenerator acg = controller.getAcg();
161+
Expression arguments = call.getArguments();
162+
boolean safe = call.isSafe();
163+
164+
StringBuilder sig = new StringBuilder("(").append(getTypeDescription(operandStack.getTopOperand()));
165+
Label end = null;
166+
if (safe && !isPrimitiveType(operandStack.getTopOperand())) {
167+
operandStack.dup();
168+
end = operandStack.jump(IFNULL);
169+
}
170+
171+
int nArgs = 1;
172+
List<Expression> args = makeArgumentList(arguments).getExpressions();
173+
boolean spread = AsmClassGenerator.containsSpreadExpression(arguments);
174+
if (spread) {
175+
acg.despreadList(args, true);
176+
sig.append(getTypeDescription(Object[].class));
177+
} else {
178+
for (Expression arg : args) {
179+
arg.visit(acg);
180+
if (arg instanceof CastExpression) {
181+
operandStack.box();
182+
acg.loadWrapper(arg);
183+
sig.append(getTypeDescription(Wrapper.class));
184+
} else {
185+
sig.append(getTypeDescription(operandStack.getTopOperand()));
186+
}
187+
nArgs++;
188+
}
189+
}
190+
sig.append(")Ljava/lang/Object;");
191+
192+
int flags = safe ? SAFE_NAVIGATION : 0;
193+
if (spread) flags |= SPREAD_CALL;
194+
controller.getMethodVisitor().visitInvokeDynamicInsn(METHOD.getCallSiteName(), sig.toString(), BSM, getMethodName(call.getMethod()), flags);
195+
operandStack.replace(OBJECT_TYPE, nArgs);
196+
if (end != null) controller.getMethodVisitor().visitLabel(end);
197+
}
198+
122199
private void finishIndyCall(final Handle bsmHandle, final String methodName, final String sig, final int numberOfArguments, final Object... bsmArgs) {
123200
CompileStack compileStack = controller.getCompileStack();
124201
OperandStack operandStack = controller.getOperandStack();

0 commit comments

Comments
 (0)