|
25 | 25 | import org.codehaus.groovy.ast.expr.ConstructorCallExpression; |
26 | 26 | import org.codehaus.groovy.ast.expr.EmptyExpression; |
27 | 27 | import org.codehaus.groovy.ast.expr.Expression; |
| 28 | +import org.codehaus.groovy.ast.expr.MethodCallExpression; |
28 | 29 | import org.codehaus.groovy.ast.expr.PropertyExpression; |
29 | 30 | import org.codehaus.groovy.ast.tools.WideningCategories; |
30 | 31 | import org.codehaus.groovy.classgen.AsmClassGenerator; |
|
42 | 43 | import java.lang.invoke.CallSite; |
43 | 44 | import java.lang.invoke.MethodHandles.Lookup; |
44 | 45 | import java.lang.invoke.MethodType; |
| 46 | +import java.util.ArrayList; |
45 | 47 | import java.util.List; |
46 | 48 |
|
| 49 | +import static org.apache.groovy.ast.tools.ExpressionUtils.isSuperExpression; |
47 | 50 | import static org.apache.groovy.ast.tools.ExpressionUtils.isThisExpression; |
48 | 51 | import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE; |
49 | 52 | import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE; |
|
54 | 57 | import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX; |
55 | 58 | import static org.codehaus.groovy.classgen.asm.BytecodeHelper.doCast; |
56 | 59 | 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; |
62 | 60 | import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.CAST; |
63 | 61 | import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.GET; |
64 | 62 | import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INIT; |
65 | 63 | import static org.codehaus.groovy.vmplugin.v8.IndyInterface.CallType.INTERFACE; |
66 | 64 | 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; |
67 | 70 | import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; |
68 | 71 | import static org.objectweb.asm.Opcodes.IFNULL; |
69 | 72 |
|
@@ -113,12 +116,133 @@ private String prepareIndyCall(final Expression receiver, final boolean implicit |
113 | 116 |
|
114 | 117 | // load normal receiver as first argument |
115 | 118 | 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); |
117 | 121 | compileStack.popImplicitThis(); |
118 | 122 |
|
119 | 123 | return "(" + getTypeDescription(operandStack.getTopOperand()); |
120 | 124 | } |
121 | 125 |
|
| 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 | + |
122 | 246 | private void finishIndyCall(final Handle bsmHandle, final String methodName, final String sig, final int numberOfArguments, final Object... bsmArgs) { |
123 | 247 | CompileStack compileStack = controller.getCompileStack(); |
124 | 248 | OperandStack operandStack = controller.getOperandStack(); |
|
0 commit comments