Skip to content

Commit c58eca9

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

File tree

3 files changed

+340
-12
lines changed

3 files changed

+340
-12
lines changed

src/main/java/org/codehaus/groovy/classgen/asm/InvocationWriter.java

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,9 +384,9 @@ protected void makeUncachedCall(Expression origin, ClassExpression sender, Expre
384384
controller.getSuperMethodNames().add(methodName); // for MOP method
385385
}
386386

387-
// receiver
387+
// receiver - GROOVY-7785: use iterative approach for chained spread-safe calls
388388
compileStack.pushImplicitThis(implicitThis);
389-
receiver.visit(acg);
389+
visitReceiverOfMethodCall(receiver, spreadSafe);
390390
operandStack.box();
391391
compileStack.popImplicitThis();
392392

@@ -425,6 +425,84 @@ protected void makeUncachedCall(Expression origin, ClassExpression sender, Expre
425425
operandStack.replace(ClassHelper.OBJECT_TYPE, operandsToRemove);
426426
}
427427

428+
/**
429+
* Visits receiver expression, using iterative approach for spread-safe method call chains.
430+
* GROOVY-7785: Flattens deep recursive AST structures to avoid stack overflow.
431+
*/
432+
private void visitReceiverOfMethodCall(final Expression receiver, final boolean spreadSafe) {
433+
if (!spreadSafe || !(receiver instanceof MethodCallExpression mce) || !mce.isSpreadSafe()) {
434+
receiver.visit(controller.getAcg());
435+
return;
436+
}
437+
438+
List<MethodCallExpression> chain = new ArrayList<>();
439+
Expression current = receiver;
440+
while (current instanceof MethodCallExpression cmce && cmce.isSpreadSafe()) {
441+
chain.add(cmce);
442+
current = cmce.getObjectExpression();
443+
}
444+
Expression innermost = current;
445+
446+
innermost.visit(controller.getAcg());
447+
448+
AsmClassGenerator acg = controller.getAcg();
449+
for (int i = chain.size() - 1; i >= 0; i--) {
450+
MethodCallExpression call = chain.get(i);
451+
acg.onLineNumber(call, "visitMethodCallExpression: \"" + call.getMethod() + "\":");
452+
processChainedSpreadSafeCall(call);
453+
controller.getAssertionWriter().record(call.getMethod());
454+
}
455+
}
456+
457+
/**
458+
* Processes a spread-safe method call with receiver already on stack.
459+
*/
460+
private void processChainedSpreadSafeCall(final MethodCallExpression call) {
461+
OperandStack operandStack = controller.getOperandStack();
462+
AsmClassGenerator acg = controller.getAcg();
463+
CompileStack compileStack = controller.getCompileStack();
464+
465+
operandStack.box();
466+
compileStack.pushLHS(false);
467+
468+
// Push sender and swap with receiver: [receiver] -> [sender, receiver]
469+
new ClassExpression(controller.getClassNode()).visit(acg);
470+
controller.getMethodVisitor().visitInsn(SWAP);
471+
472+
// message
473+
Expression message = new CastExpression(ClassHelper.STRING_TYPE, call.getMethod());
474+
message.visit(acg);
475+
operandStack.box();
476+
477+
// arguments
478+
Expression arguments = call.getArguments();
479+
boolean containsSpread = AsmClassGenerator.containsSpreadExpression(arguments);
480+
int numberOfArguments = containsSpread ? -1 : AsmClassGenerator.argumentSize(arguments);
481+
int operandsToRemove = 3; // sender + receiver + message
482+
483+
if (numberOfArguments > MethodCallerMultiAdapter.MAX_ARGS || containsSpread) {
484+
ArgumentListExpression ae = makeArgumentList(arguments);
485+
if (containsSpread) {
486+
acg.despreadList(ae.getExpressions(), true);
487+
} else {
488+
ae.visit(acg);
489+
}
490+
} else if (numberOfArguments > 0) {
491+
operandsToRemove += numberOfArguments;
492+
TupleExpression te = (TupleExpression) arguments;
493+
for (int i = 0; i < numberOfArguments; i++) {
494+
Expression arg = te.getExpression(i);
495+
arg.visit(acg);
496+
operandStack.box();
497+
if (arg instanceof CastExpression) acg.loadWrapper(arg);
498+
}
499+
}
500+
501+
invokeMethod.call(controller.getMethodVisitor(), numberOfArguments, call.isSafe(), call.isSpreadSafe());
502+
compileStack.popLHS();
503+
operandStack.replace(ClassHelper.OBJECT_TYPE, operandsToRemove);
504+
}
505+
428506
/**
429507
* if Class.forName(x) is recognized, make a direct method call
430508
*/

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

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

0 commit comments

Comments
 (0)