Skip to content

Commit e998ac3

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

File tree

2 files changed

+290
-10
lines changed

2 files changed

+290
-10
lines changed

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

Lines changed: 130 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,133 @@ 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 the chain of method calls that can be handled by indy
134+
List<MethodCallExpression> chain = new ArrayList<>();
135+
Expression current = receiver;
136+
while (current instanceof MethodCallExpression mce && canUseIndyForChain(mce)) {
137+
chain.add(mce);
138+
current = mce.getObjectExpression();
139+
}
140+
141+
if (chain.isEmpty()) {
142+
// Not a chainable method call or chain cannot be optimized, use normal visit
143+
receiver.visit(controller.getAcg());
144+
return;
145+
}
146+
147+
// Process the innermost non-chainable receiver first
148+
current.visit(controller.getAcg());
149+
150+
// Process each method call in the chain, from innermost to outermost
151+
AsmClassGenerator acg = controller.getAcg();
152+
for (int i = chain.size() - 1; i >= 0; i -= 1) {
153+
MethodCallExpression mce = chain.get(i);
154+
acg.onLineNumber(mce, "visitMethodCallExpression (chained): \"" + mce.getMethod() + "\":");
155+
// Process this method call with its receiver already on the stack
156+
makeIndyCallWithReceiverOnStack(mce);
157+
controller.getAssertionWriter().record(mce.getMethod());
158+
}
159+
}
160+
161+
/**
162+
* Check if a method call can be handled in the chained call optimization.
163+
* Only simple method calls that go through the standard indy path can be optimized.
164+
*/
165+
private boolean canUseIndyForChain(final MethodCallExpression call) {
166+
// Spread safe calls need special handling and cannot be optimized
167+
if (call.isSpreadSafe()) return false;
168+
// Super calls have different invocation semantics and should not be optimized
169+
if (isSuperExpression(call.getObjectExpression())) return false;
170+
// This calls and implicit this calls have special context handling
171+
if (isThisExpression(call.getObjectExpression())) return false;
172+
if (call.isImplicitThis()) return false;
173+
// Dynamic method names (non-constant) cannot be handled
174+
String methodName = getMethodName(call.getMethod());
175+
if (methodName == null) return false;
176+
// "call" method invocations may need special handling for functional interfaces (GROOVY-8466)
177+
if ("call".equals(methodName)) return false;
178+
return true;
179+
}
180+
181+
/**
182+
* Process a method call expression assuming its receiver is already on the operand stack.
183+
*/
184+
private void makeIndyCallWithReceiverOnStack(final MethodCallExpression call) {
185+
MethodCallerMultiAdapter adapter = invokeMethod;
186+
Expression receiver = call.getObjectExpression();
187+
if (isSuperExpression(receiver)) {
188+
adapter = invokeMethodOnSuper;
189+
} else if (isThisExpression(receiver)) {
190+
adapter = invokeMethodOnCurrent;
191+
}
192+
193+
String methodName = getMethodName(call.getMethod());
194+
if (methodName == null) {
195+
// fallback to normal path which will handle dynamic method names
196+
call.visit(controller.getAcg());
197+
return;
198+
}
199+
200+
Expression arguments = call.getArguments();
201+
boolean safe = call.isSafe();
202+
boolean containsSpreadExpression = AsmClassGenerator.containsSpreadExpression(arguments);
203+
204+
OperandStack operandStack = controller.getOperandStack();
205+
StringBuilder sig = new StringBuilder("(" + getTypeDescription(operandStack.getTopOperand()));
206+
207+
Label end = null;
208+
if (safe && !isPrimitiveType(operandStack.getTopOperand())) {
209+
operandStack.dup();
210+
end = operandStack.jump(IFNULL);
211+
}
212+
213+
// load arguments
214+
int numberOfArguments = 1;
215+
List<Expression> args = makeArgumentList(arguments).getExpressions();
216+
AsmClassGenerator acg = controller.getAcg();
217+
if (containsSpreadExpression) {
218+
acg.despreadList(args, true);
219+
sig.append(getTypeDescription(Object[].class));
220+
} else {
221+
for (Expression arg : args) {
222+
arg.visit(acg);
223+
if (arg instanceof CastExpression) {
224+
operandStack.box();
225+
acg.loadWrapper(arg);
226+
sig.append(getTypeDescription(Wrapper.class));
227+
} else {
228+
sig.append(getTypeDescription(operandStack.getTopOperand()));
229+
}
230+
numberOfArguments += 1;
231+
}
232+
}
233+
234+
sig.append(")Ljava/lang/Object;");
235+
236+
String callSiteName = METHOD.getCallSiteName();
237+
int flags = getMethodCallFlags(adapter, safe, containsSpreadExpression);
238+
239+
// Note: callSiteName is the invoke-dynamic instruction name, methodName is passed via BSM args
240+
controller.getMethodVisitor().visitInvokeDynamicInsn(callSiteName, sig.toString(), BSM, methodName, flags);
241+
operandStack.replace(OBJECT_TYPE, numberOfArguments);
242+
243+
if (end != null) controller.getMethodVisitor().visitLabel(end);
244+
}
245+
122246
private void finishIndyCall(final Handle bsmHandle, final String methodName, final String sig, final int numberOfArguments, final Object... bsmArgs) {
123247
CompileStack compileStack = controller.getCompileStack();
124248
OperandStack operandStack = controller.getOperandStack();

0 commit comments

Comments
 (0)