Skip to content

Commit fa38779

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

2 files changed

Lines changed: 391 additions & 10 deletions

File tree

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,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,90 @@ 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 mce
136+
&& !mce.isSpreadSafe() && !mce.isImplicitThis()
137+
&& !isSuperExpression(mce.getObjectExpression())
138+
&& !isThisExpression(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+
current = mce.getObjectExpression();
143+
}
144+
145+
if (chain.isEmpty()) {
146+
receiver.visit(controller.getAcg());
147+
return;
148+
}
149+
150+
current.visit(controller.getAcg());
151+
AsmClassGenerator acg = controller.getAcg();
152+
while (!chain.isEmpty()) {
153+
MethodCallExpression call = chain.pop();
154+
acg.onLineNumber(call, "visitMethodCallExpression: \"" + call.getMethod() + "\":");
155+
finishIndyCallForChain(call);
156+
controller.getAssertionWriter().record(call.getMethod());
157+
}
158+
}
159+
160+
/**
161+
* Completes an indy call for a chained method with receiver already on stack.
162+
*/
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("(").append(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)