1 /*
2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package jdk.nashorn.internal.codegen;
27 
28 import static jdk.nashorn.internal.codegen.CompilerConstants.ARGUMENTS_VAR;
29 import static jdk.nashorn.internal.codegen.CompilerConstants.EXPLODED_ARGUMENT_PREFIX;
30 
31 import java.lang.invoke.MethodType;
32 import java.net.URL;
33 import java.util.ArrayDeque;
34 import java.util.ArrayList;
35 import java.util.Deque;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Set;
39 import jdk.nashorn.internal.ir.AccessNode;
40 import jdk.nashorn.internal.ir.CallNode;
41 import jdk.nashorn.internal.ir.Expression;
42 import jdk.nashorn.internal.ir.FunctionNode;
43 import jdk.nashorn.internal.ir.IdentNode;
44 import jdk.nashorn.internal.ir.Node;
45 import jdk.nashorn.internal.ir.visitor.SimpleNodeVisitor;
46 import jdk.nashorn.internal.objects.Global;
47 import jdk.nashorn.internal.runtime.Context;
48 import jdk.nashorn.internal.runtime.logging.DebugLogger;
49 import jdk.nashorn.internal.runtime.logging.Loggable;
50 import jdk.nashorn.internal.runtime.logging.Logger;
51 import jdk.nashorn.internal.runtime.options.Options;
52 
53 /**
54  * An optimization that attempts to turn applies into calls. This pattern
55  * is very common for fake class instance creation, and apply
56  * introduces expensive args collection and boxing
57  *
58  * <pre>
59  * var Class = {
60  *     create: function() {
61  *         return function() { //vararg
62  *             this.initialize.apply(this, arguments);
63  *         }
64  *     }
65  * };
66  *
67  * Color = Class.create();
68  *
69  * Color.prototype = {
70  *    red: 0, green: 0, blue: 0,
71  *    initialize: function(r,g,b) {
72  *        this.red = r;
73  *        this.green = g;
74  *        this.blue = b;
75  *    }
76  * }
77  *
78  * new Color(17, 47, 11);
79  * </pre>
80  */
81 
82 @Logger(name="apply2call")
83 public final class ApplySpecialization extends SimpleNodeVisitor implements Loggable {
84 
85     private static final boolean USE_APPLY2CALL = Options.getBooleanProperty("nashorn.apply2call", true);
86 
87     private final DebugLogger log;
88 
89     private final Compiler compiler;
90 
91     private final Set<Integer> changed = new HashSet<>();
92 
93     private final Deque<List<IdentNode>> explodedArguments = new ArrayDeque<>();
94 
95     private final Deque<MethodType> callSiteTypes = new ArrayDeque<>();
96 
97     private static final String ARGUMENTS = ARGUMENTS_VAR.symbolName();
98 
99     /**
100      * Apply specialization optimization. Try to explode arguments and call
101      * applies as calls if they just pass on the "arguments" array and
102      * "arguments" doesn't escape.
103      *
104      * @param compiler compiler
105      */
ApplySpecialization(final Compiler compiler)106     public ApplySpecialization(final Compiler compiler) {
107         this.compiler = compiler;
108         this.log = initLogger(compiler.getContext());
109     }
110 
111     @Override
getLogger()112     public DebugLogger getLogger() {
113         return log;
114     }
115 
116     @Override
initLogger(final Context context)117     public DebugLogger initLogger(final Context context) {
118         return context.getLogger(this.getClass());
119     }
120 
121     @SuppressWarnings("serial")
122     private static class TransformFailedException extends RuntimeException {
TransformFailedException(final FunctionNode fn, final String message)123         TransformFailedException(final FunctionNode fn, final String message) {
124             super(massageURL(fn.getSource().getURL()) + '.' + fn.getName() + " => " + message, null, false, false);
125         }
126     }
127 
128     @SuppressWarnings("serial")
129     private static class AppliesFoundException extends RuntimeException {
AppliesFoundException()130         AppliesFoundException() {
131             super("applies_found", null, false, false);
132         }
133     }
134 
135     private static final AppliesFoundException HAS_APPLIES = new AppliesFoundException();
136 
hasApplies(final FunctionNode functionNode)137     private boolean hasApplies(final FunctionNode functionNode) {
138         try {
139             functionNode.accept(new SimpleNodeVisitor() {
140                 @Override
141                 public boolean enterFunctionNode(final FunctionNode fn) {
142                     return fn == functionNode;
143                 }
144 
145                 @Override
146                 public boolean enterCallNode(final CallNode callNode) {
147                     if (isApply(callNode)) {
148                         throw HAS_APPLIES;
149                     }
150                     return true;
151                 }
152             });
153         } catch (final AppliesFoundException e) {
154             return true;
155         }
156 
157         log.fine("There are no applies in ", DebugLogger.quote(functionNode.getName()), " - nothing to do.");
158         return false; // no applies
159     }
160 
161     /**
162      * Arguments may only be used as args to the apply. Everything else is disqualified
163      * We cannot control arguments if they escape from the method and go into an unknown
164      * scope, thus we are conservative and treat any access to arguments outside the
165      * apply call as a case of "we cannot apply the optimization".
166      */
checkValidTransform(final FunctionNode functionNode)167     private static void checkValidTransform(final FunctionNode functionNode) {
168 
169         final Set<Expression> argumentsFound = new HashSet<>();
170         final Deque<Set<Expression>> stack = new ArrayDeque<>();
171 
172         //ensure that arguments is only passed as arg to apply
173         functionNode.accept(new SimpleNodeVisitor() {
174 
175             private boolean isCurrentArg(final Expression expr) {
176                 return !stack.isEmpty() && stack.peek().contains(expr); //args to current apply call
177             }
178 
179             private boolean isArguments(final Expression expr) {
180                 if (expr instanceof IdentNode && ARGUMENTS.equals(((IdentNode)expr).getName())) {
181                     argumentsFound.add(expr);
182                     return true;
183                }
184                 return false;
185             }
186 
187             private boolean isParam(final String name) {
188                 for (final IdentNode param : functionNode.getParameters()) {
189                     if (param.getName().equals(name)) {
190                         return true;
191                     }
192                 }
193                 return false;
194             }
195 
196             @Override
197             public Node leaveIdentNode(final IdentNode identNode) {
198                 if (isParam(identNode.getName())) {
199                     throw new TransformFailedException(lc.getCurrentFunction(), "parameter: " + identNode.getName());
200                 }
201                 // it's OK if 'argument' occurs as the current argument of an apply
202                 if (isArguments(identNode) && !isCurrentArg(identNode)) {
203                     throw new TransformFailedException(lc.getCurrentFunction(), "is 'arguments': " + identNode.getName());
204                 }
205                 return identNode;
206             }
207 
208             @Override
209             public boolean enterCallNode(final CallNode callNode) {
210                 final Set<Expression> callArgs = new HashSet<>();
211                 if (isApply(callNode)) {
212                     final List<Expression> argList = callNode.getArgs();
213                     if (argList.size() != 2 || !isArguments(argList.get(argList.size() - 1))) {
214                         throw new TransformFailedException(lc.getCurrentFunction(), "argument pattern not matched: " + argList);
215                     }
216                     callArgs.addAll(callNode.getArgs());
217                 }
218                 stack.push(callArgs);
219                 return true;
220             }
221 
222             @Override
223             public Node leaveCallNode(final CallNode callNode) {
224                 stack.pop();
225                 return callNode;
226             }
227        });
228     }
229 
230     @Override
enterCallNode(final CallNode callNode)231     public boolean enterCallNode(final CallNode callNode) {
232         return !explodedArguments.isEmpty();
233     }
234 
235     @Override
leaveCallNode(final CallNode callNode)236     public Node leaveCallNode(final CallNode callNode) {
237         //apply needs to be a global symbol or we don't allow it
238 
239         final List<IdentNode> newParams = explodedArguments.peek();
240         if (isApply(callNode)) {
241             final List<Expression> newArgs = new ArrayList<>();
242             for (final Expression arg : callNode.getArgs()) {
243                 if (arg instanceof IdentNode && ARGUMENTS.equals(((IdentNode)arg).getName())) {
244                     newArgs.addAll(newParams);
245                 } else {
246                     newArgs.add(arg);
247                 }
248             }
249 
250             changed.add(lc.getCurrentFunction().getId());
251 
252             final CallNode newCallNode = callNode.setArgs(newArgs).setIsApplyToCall();
253 
254             if (log.isEnabled()) {
255                 log.fine("Transformed ",
256                         callNode,
257                         " from apply to call => ",
258                         newCallNode,
259                         " in ",
260                         DebugLogger.quote(lc.getCurrentFunction().getName()));
261             }
262 
263             return newCallNode;
264         }
265 
266         return callNode;
267     }
268 
pushExplodedArgs(final FunctionNode functionNode)269     private void pushExplodedArgs(final FunctionNode functionNode) {
270         int start = 0;
271 
272         final MethodType actualCallSiteType = compiler.getCallSiteType(functionNode);
273         if (actualCallSiteType == null) {
274             throw new TransformFailedException(lc.getCurrentFunction(), "No callsite type");
275         }
276         assert actualCallSiteType.parameterType(actualCallSiteType.parameterCount() - 1) != Object[].class : "error vararg callsite passed to apply2call " + functionNode.getName() + " " + actualCallSiteType;
277 
278         final TypeMap ptm = compiler.getTypeMap();
279         if (ptm.needsCallee()) {
280             start++;
281         }
282 
283         start++; // we always use this
284 
285         assert functionNode.getNumOfParams() == 0 : "apply2call on function with named paramaters!";
286         final List<IdentNode> newParams = new ArrayList<>();
287         final long to = actualCallSiteType.parameterCount() - start;
288         for (int i = 0; i < to; i++) {
289             newParams.add(new IdentNode(functionNode.getToken(), functionNode.getFinish(), EXPLODED_ARGUMENT_PREFIX.symbolName() + (i)));
290         }
291 
292         callSiteTypes.push(actualCallSiteType);
293         explodedArguments.push(newParams);
294     }
295 
296     @Override
enterFunctionNode(final FunctionNode functionNode)297     public boolean enterFunctionNode(final FunctionNode functionNode) {
298         // Cheap tests first
299         if (!(
300                 // is the transform globally enabled?
301                 USE_APPLY2CALL
302 
303                 // Are we compiling lazily? We can't known the number and types of the actual parameters at
304                 // the caller when compiling eagerly, so this only works with on-demand compilation.
305                 && compiler.isOnDemandCompilation()
306 
307                 // Does the function even reference the "arguments" identifier (without redefining it)? If not,
308                 // it trivially can't have an expression of form "f.apply(self, arguments)" that this transform
309                 // is targeting.
310                 && functionNode.needsArguments()
311 
312                 // Does the function have eval? If so, it can arbitrarily modify arguments so we can't touch it.
313                 && !functionNode.hasEval()
314 
315                 // Finally, does the function declare any parameters explicitly? We don't support that. It could
316                 // be done, but has some complications. Therefore only a function with no explicit parameters
317                 // is considered.
318                 && functionNode.getNumOfParams() == 0))
319         {
320             return false;
321         }
322 
323         if (!Global.isBuiltinFunctionPrototypeApply()) {
324             log.fine("Apply transform disabled: apply/call overridden");
325             assert !Global.isBuiltinFunctionPrototypeCall() : "call and apply should have the same SwitchPoint";
326             return false;
327         }
328 
329         if (!hasApplies(functionNode)) {
330             return false;
331         }
332 
333         if (log.isEnabled()) {
334             log.info("Trying to specialize apply to call in '",
335                     functionNode.getName(),
336                     "' params=",
337                     functionNode.getParameters(),
338                     " id=",
339                     functionNode.getId(),
340                     " source=",
341                     massageURL(functionNode.getSource().getURL()));
342         }
343 
344         try {
345             checkValidTransform(functionNode);
346             pushExplodedArgs(functionNode);
347         } catch (final TransformFailedException e) {
348             log.info("Failure: ", e.getMessage());
349             return false;
350         }
351 
352         return true;
353     }
354 
355     /**
356      * Try to do the apply to call transformation
357      * @return true if successful, false otherwise
358      */
359     @Override
leaveFunctionNode(final FunctionNode functionNode)360     public Node leaveFunctionNode(final FunctionNode functionNode) {
361         FunctionNode newFunctionNode = functionNode;
362         final String functionName = newFunctionNode.getName();
363 
364         if (changed.contains(newFunctionNode.getId())) {
365             newFunctionNode = newFunctionNode.clearFlag(lc, FunctionNode.USES_ARGUMENTS).
366                     setFlag(lc, FunctionNode.HAS_APPLY_TO_CALL_SPECIALIZATION).
367                     setParameters(lc, explodedArguments.peek());
368 
369             if (log.isEnabled()) {
370                 log.info("Success: ",
371                         massageURL(newFunctionNode.getSource().getURL()),
372                         '.',
373                         functionName,
374                         "' id=",
375                         newFunctionNode.getId(),
376                         " params=",
377                         callSiteTypes.peek());
378             }
379         }
380 
381         callSiteTypes.pop();
382         explodedArguments.pop();
383 
384         return newFunctionNode;
385     }
386 
isApply(final CallNode callNode)387     private static boolean isApply(final CallNode callNode) {
388         final Expression f = callNode.getFunction();
389         return f instanceof AccessNode && "apply".equals(((AccessNode)f).getProperty());
390     }
391 
massageURL(final URL url)392     private static String massageURL(final URL url) {
393         if (url == null) {
394             return "<null>";
395         }
396         final String str = url.toString();
397         final int slash = str.lastIndexOf('/');
398         if (slash == -1) {
399             return str;
400         }
401         return str.substring(slash + 1);
402     }
403 }
404