Skip to content

Commit 99462ef

Browse files
committed
GROOVY-7785: StackoverflowException when using too many chained method calls
1 parent 4d7fc0a commit 99462ef

File tree

2 files changed

+251
-10
lines changed

2 files changed

+251
-10
lines changed

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

Lines changed: 88 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,10 @@
4243
import java.lang.invoke.CallSite;
4344
import java.lang.invoke.MethodHandles.Lookup;
4445
import java.lang.invoke.MethodType;
46+
import java.util.ArrayList;
4547
import java.util.List;
4648

49+
import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression;
4750
import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression;
4851
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
4952
import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
@@ -54,16 +57,16 @@
5457
import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
5558
import static org.codehaus.groovy.classgen.asm.BytecodeHelper.doCast;
5659
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;
6260
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.CAST;
6361
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.GET;
6462
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INIT;
6563
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INTERFACE;
6664
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.METHOD;
65+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.GROOVY_OBJECT;
66+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.IMPLICIT_THIS;
67+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SAFE_NAVIGATION;
68+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.SPREAD_CALL;
69+
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.THIS_CALL;
6770
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
6871
import static org.objectweb.asm.Opcodes.IFNULL;
6972

@@ -113,12 +116,91 @@ private String prepareIndyCall(final Expression receiver, final boolean implicit
113116

114117
// load normal receiver as first argument
115118
compileStack.pushImplicitThis(implicitThis);
116-
receiver.visit(controller.getAcg());
119+
// GROOVY-7785: use iterative approach to avoid stack overflow for chained method calls
120+
visitReceiverOfMethodCall(receiver);
117121
compileStack.popImplicitThis();
118122

119123
return "(" + getTypeDescription(operandStack.getTopOperand());
120124
}
121125

126+
/**
127+
* Visit receiver expression iteratively to avoid stack overflow for deeply nested method call chains.
128+
* For chained calls like a().b().c()...z(), the AST forms a deep right-recursive structure where
129+
* each method call's receiver is another method call. This method flattens the chain and processes
130+
* it iteratively from the innermost receiver outward.
131+
*/
132+
private void visitReceiverOfMethodCall(final Expression receiver) {
133+
// Collect chain of simple method calls that can use indy optimization
134+
List<MethodCallExpression> chain = new ArrayList<>();
135+
Expression current = receiver;
136+
while (current instanceof MethodCallExpression mce
137+
&& !mce.isSpreadSafe() && !mce.isImplicitThis()
138+
&& !isSuperExpression(mce.getObjectExpression())
139+
&& !isThisExpression(mce.getObjectExpression())) {
140+
String name = getMethodName(mce.getMethod());
141+
if (name == null || "call".equals(name)) break; // dynamic name or functional interface call
142+
chain.add(mce);
143+
current = mce.getObjectExpression();
144+
}
145+
146+
if (chain.isEmpty()) {
147+
receiver.visit(controller.getAcg());
148+
return;
149+
}
150+
151+
// Visit innermost receiver, then process chain from innermost to outermost
152+
current.visit(controller.getAcg());
153+
AsmClassGenerator acg = controller.getAcg();
154+
for (int i = chain.size() - 1; i >= 0; i--) {
155+
MethodCallExpression mce = chain.get(i);
156+
acg.onLineNumber(mce, "visitMethodCallExpression: \"" + mce.getMethod() + "\":");
157+
finishIndyCallForChain(mce);
158+
controller.getAssertionWriter().record(mce.getMethod());
159+
}
160+
}
161+
162+
/** Complete an indy call for a chained method, assuming receiver is already on stack. */
163+
private void finishIndyCallForChain(final MethodCallExpression call) {
164+
OperandStack operandStack = controller.getOperandStack();
165+
AsmClassGenerator acg = controller.getAcg();
166+
Expression arguments = call.getArguments();
167+
boolean safe = call.isSafe();
168+
169+
StringBuilder sig = new StringBuilder("(" + getTypeDescription(operandStack.getTopOperand()));
170+
Label end = null;
171+
if (safe && !isPrimitiveType(operandStack.getTopOperand())) {
172+
operandStack.dup();
173+
end = operandStack.jump(IFNULL);
174+
}
175+
176+
int nArgs = 1;
177+
List<Expression> args = makeArgumentList(arguments).getExpressions();
178+
boolean spread = AsmClassGenerator.containsSpreadExpression(arguments);
179+
if (spread) {
180+
acg.despreadList(args, true);
181+
sig.append(getTypeDescription(Object[].class));
182+
} else {
183+
for (Expression arg : args) {
184+
arg.visit(acg);
185+
if (arg instanceof CastExpression) {
186+
operandStack.box();
187+
acg.loadWrapper(arg);
188+
sig.append(getTypeDescription(Wrapper.class));
189+
} else {
190+
sig.append(getTypeDescription(operandStack.getTopOperand()));
191+
}
192+
nArgs++;
193+
}
194+
}
195+
sig.append(")Ljava/lang/Object;");
196+
197+
int flags = safe ? SAFE_NAVIGATION : 0;
198+
if (spread) flags |= SPREAD_CALL;
199+
controller.getMethodVisitor().visitInvokeDynamicInsn(METHOD.getCallSiteName(), sig.toString(), BSM, getMethodName(call.getMethod()), flags);
200+
operandStack.replace(OBJECT_TYPE, nArgs);
201+
if (end != null) controller.getMethodVisitor().visitLabel(end);
202+
}
203+
122204
private void finishIndyCall(final Handle bsmHandle, final String methodName, final String sig, final int numberOfArguments, final Object... bsmArgs) {
123205
CompileStack compileStack = controller.getCompileStack();
124206
OperandStack operandStack = controller.getOperandStack();

0 commit comments

Comments
 (0)