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.FunctionNode.CompilationState;
  44 import jdk.nashorn.internal.ir.IdentNode;
  45 import jdk.nashorn.internal.ir.LexicalContext;
  46 import jdk.nashorn.internal.ir.Node;
  47 import jdk.nashorn.internal.ir.visitor.NodeVisitor;
  48 import jdk.nashorn.internal.objects.Global;
  49 import jdk.nashorn.internal.runtime.Context;
  50 import jdk.nashorn.internal.runtime.logging.DebugLogger;
  51 import jdk.nashorn.internal.runtime.logging.Loggable;
  52 import jdk.nashorn.internal.runtime.logging.Logger;
  53 import jdk.nashorn.internal.runtime.options.Options;
  54 
  55 /**
  56  * An optimization that attempts to turn applies into calls. This pattern
  57  * is very common for fake class instance creation, and apply
  58  * introduces expensive args collection and boxing
  59  *
  60  * <pre>
  61  * var Class = {
  62  *     create: function() {
  63  *         return function() { //vararg
  64  *             this.initialize.apply(this, arguments);
  65  *         }
  66  *     }
  67  * };
  68  *
  69  * Color = Class.create();
  70  *
  71  * Color.prototype = {
  72  *    red: 0, green: 0, blue: 0,
  73  *    initialize: function(r,g,b) {
  74  *        this.red = r;
  75  *        this.green = g;
  76  *        this.blue = b;
  77  *    }
  78  * }
  79  *
  80  * new Color(17, 47, 11);
  81  * </pre>
  82  */
  83 
  84 @Logger(name="apply2call")
  85 public final class ApplySpecialization extends NodeVisitor<LexicalContext> implements Loggable {
  86 
  87     private static final boolean USE_APPLY2CALL = Options.getBooleanProperty("nashorn.apply2call", true);
  88 
  89     private final DebugLogger log;
  90 
  91     private final Compiler compiler;
  92 
  93     private final Set<Integer> changed = new HashSet<>();
  94 
  95     private final Deque<List<IdentNode>> explodedArguments = new ArrayDeque<>();
  96 
  97     private final Deque<MethodType> callSiteTypes = new ArrayDeque<>();
  98 
  99     private static final String ARGUMENTS = ARGUMENTS_VAR.symbolName();
 100 
 101     /**
 102      * Apply specialization optimization. Try to explode arguments and call
 103      * applies as calls if they just pass on the "arguments" array and
 104      * "arguments" doesn't escape.
 105      *
 106      * @param compiler compiler
 107      */
 108     public ApplySpecialization(final Compiler compiler) {
 109         super(new LexicalContext());
 110         this.compiler = compiler;
 111         this.log = initLogger(compiler.getContext());
 112     }
 113 
 114     @Override
 115     public DebugLogger getLogger() {
 116         return log;
 117     }
 118 
 119     @Override
 120     public DebugLogger initLogger(final Context context) {
 121         return context.getLogger(this.getClass());
 122     }
 123 
 124     @SuppressWarnings("serial")
 125     private static class TransformFailedException extends RuntimeException {
 126         TransformFailedException(final FunctionNode fn, final String message) {
 127             super(massageURL(fn.getSource().getURL()) + '.' + fn.getName() + " => " + message, null, false, false);
 128         }
 129     }
 130 
 131     @SuppressWarnings("serial")
 132     private static class AppliesFoundException extends RuntimeException {
 133         AppliesFoundException() {
 134             super("applies_found", null, false, false);
 135         }
 136     }
 137 
 138     private static final AppliesFoundException HAS_APPLIES = new AppliesFoundException();
 139 
 140     private boolean hasApplies(final FunctionNode functionNode) {
 141         try {
 142             functionNode.accept(new NodeVisitor<LexicalContext>(new LexicalContext()) {
 143                 @Override
 144                 public boolean enterFunctionNode(final FunctionNode fn) {
 145                     return fn == functionNode;
 146                 }
 147 
 148                 @Override
 149                 public boolean enterCallNode(final CallNode callNode) {
 150                     if (isApply(callNode)) {
 151                         throw HAS_APPLIES;
 152                     }
 153                     return true;
 154                 }
 155             });
 156         } catch (final AppliesFoundException e) {
 157             return true;
 158         }
 159 
 160         log.fine("There are no applies in ", DebugLogger.quote(functionNode.getName()), " - nothing to do.");
 161         return false; // no applies
 162     }
 163 
 164     /**
 165      * Arguments may only be used as args to the apply. Everything else is disqualified
 166      * We cannot control arguments if they escape from the method and go into an unknown
 167      * scope, thus we are conservative and treat any access to arguments outside the
 168      * apply call as a case of "we cannot apply the optimization".
 169      */
 170     private static void checkValidTransform(final FunctionNode functionNode) {
 171 
 172         final Set<Expression> argumentsFound = new HashSet<>();
 173         final Deque<Set<Expression>> stack = new ArrayDeque<>();
 174 
 175         //ensure that arguments is only passed as arg to apply
 176         functionNode.accept(new NodeVisitor<LexicalContext>(new LexicalContext()) {
 177 
 178             private boolean isCurrentArg(final Expression expr) {
 179                 return !stack.isEmpty() && stack.peek().contains(expr); //args to current apply call
 180             }
 181 
 182             private boolean isArguments(final Expression expr) {
 183                 if (expr instanceof IdentNode && ARGUMENTS.equals(((IdentNode)expr).getName())) {
 184                     argumentsFound.add(expr);
 185                     return true;
 186                }
 187                 return false;
 188             }
 189 
 190             private boolean isParam(final String name) {
 191                 for (final IdentNode param : functionNode.getParameters()) {
 192                     if (param.getName().equals(name)) {
 193                         return true;
 194                     }
 195                 }
 196                 return false;
 197             }
 198 
 199             @Override
 200             public Node leaveIdentNode(final IdentNode identNode) {
 201                 if (isParam(identNode.getName())) {
 202                     throw new TransformFailedException(lc.getCurrentFunction(), "parameter: " + identNode.getName());
 203                 }
 204                 // it's OK if 'argument' occurs as the current argument of an apply
 205                 if (isArguments(identNode) && !isCurrentArg(identNode)) {
 206                     throw new TransformFailedException(lc.getCurrentFunction(), "is 'arguments': " + identNode.getName());
 207                 }
 208                 return identNode;
 209             }
 210 
 211             @Override
 212             public boolean enterCallNode(final CallNode callNode) {
 213                 final Set<Expression> callArgs = new HashSet<>();
 214                 if (isApply(callNode)) {
 215                     final List<Expression> argList = callNode.getArgs();
 216                     if (argList.size() != 2 || !isArguments(argList.get(argList.size() - 1))) {
 217                         throw new TransformFailedException(lc.getCurrentFunction(), "argument pattern not matched: " + argList);
 218                     }
 219                     callArgs.addAll(callNode.getArgs());
 220                 }
 221                 stack.push(callArgs);
 222                 return true;
 223             }
 224 
 225             @Override
 226             public Node leaveCallNode(final CallNode callNode) {
 227                 stack.pop();
 228                 return callNode;
 229             }
 230        });
 231     }
 232 
 233     @Override
 234     public boolean enterCallNode(final CallNode callNode) {
 235         return !explodedArguments.isEmpty();
 236     }
 237 
 238     @Override
 239     public Node leaveCallNode(final CallNode callNode) {
 240         //apply needs to be a global symbol or we don't allow it
 241 
 242         final List<IdentNode> newParams = explodedArguments.peek();
 243         if (isApply(callNode)) {
 244             final List<Expression> newArgs = new ArrayList<>();
 245             for (final Expression arg : callNode.getArgs()) {
 246                 if (arg instanceof IdentNode && ARGUMENTS.equals(((IdentNode)arg).getName())) {
 247                     newArgs.addAll(newParams);
 248                 } else {
 249                     newArgs.add(arg);
 250                 }
 251             }
 252 
 253             changed.add(lc.getCurrentFunction().getId());
 254 
 255             final CallNode newCallNode = callNode.setArgs(newArgs).setIsApplyToCall();
 256 
 257             if (log.isEnabled()) {
 258                 log.fine("Transformed ",
 259                         callNode,
 260                         " from apply to call => ",
 261                         newCallNode,
 262                         " in ",
 263                         DebugLogger.quote(lc.getCurrentFunction().getName()));
 264             }
 265 
 266             return newCallNode;
 267         }
 268 
 269         return callNode;
 270     }
 271 
 272     private void pushExplodedArgs(final FunctionNode functionNode) {
 273         int start = 0;
 274 
 275         final MethodType actualCallSiteType = compiler.getCallSiteType(functionNode);
 276         if (actualCallSiteType == null) {
 277             throw new TransformFailedException(lc.getCurrentFunction(), "No callsite type");
 278         }
 279         assert actualCallSiteType.parameterType(actualCallSiteType.parameterCount() - 1) != Object[].class : "error vararg callsite passed to apply2call " + functionNode.getName() + " " + actualCallSiteType;
 280 
 281         final TypeMap ptm = compiler.getTypeMap();
 282         if (ptm.needsCallee()) {
 283             start++;
 284         }
 285 
 286         start++; // we always use this
 287 
 288         assert functionNode.getNumOfParams() == 0 : "apply2call on function with named paramaters!";
 289         final List<IdentNode> newParams = new ArrayList<>();
 290         final long to = actualCallSiteType.parameterCount() - start;
 291         for (int i = 0; i < to; i++) {
 292             newParams.add(new IdentNode(functionNode.getToken(), functionNode.getFinish(), EXPLODED_ARGUMENT_PREFIX.symbolName() + (i)));
 293         }
 294 
 295         callSiteTypes.push(actualCallSiteType);
 296         explodedArguments.push(newParams);
 297     }
 298 
 299     @Override
 300     public boolean enterFunctionNode(final FunctionNode functionNode) {
 301         if (!USE_APPLY2CALL) {
 302             return false;
 303         }
 304 
 305         if (!Global.isBuiltinFunctionPrototypeApply()) {
 306             log.fine("Apply transform disabled: apply/call overridden");
 307             assert !Global.isBuiltinFunctionPrototypeCall() : "call and apply should have the same SwitchPoint";
 308             return false;
 309         }
 310 
 311         if (!compiler.isOnDemandCompilation()) {
 312             return false;
 313         }
 314 
 315         if (functionNode.getNumOfParams() != 0) {
 316             return false;
 317         }
 318 
 319         if (functionNode.hasEval()) {
 320             return false;
 321         }
 322 
 323         if (!hasApplies(functionNode)) {
 324             return false;
 325         }
 326 
 327         if (log.isEnabled()) {
 328             log.info("Trying to specialize apply to call in '",
 329                     functionNode.getName(),
 330                     "' params=",
 331                     functionNode.getParameters(),
 332                     " id=",
 333                     functionNode.getId(),
 334                     " source=",
 335                     massageURL(functionNode.getSource().getURL()));
 336         }
 337 
 338         try {
 339             checkValidTransform(functionNode);
 340             pushExplodedArgs(functionNode);
 341         } catch (final TransformFailedException e) {
 342             log.info("Failure: ", e.getMessage());
 343             return false;
 344         }
 345 
 346         return true;
 347     }
 348 
 349     /**
 350      * Try to do the apply to call transformation
 351      * @return true if successful, false otherwise
 352      */
 353     @Override
 354     public Node leaveFunctionNode(final FunctionNode functionNode) {
 355         FunctionNode newFunctionNode = functionNode;
 356         final String functionName = newFunctionNode.getName();
 357 
 358         if (changed.contains(newFunctionNode.getId())) {
 359             newFunctionNode = newFunctionNode.clearFlag(lc, FunctionNode.USES_ARGUMENTS).
 360                     setFlag(lc, FunctionNode.HAS_APPLY_TO_CALL_SPECIALIZATION).
 361                     setParameters(lc, explodedArguments.peek());
 362 
 363             if (log.isEnabled()) {
 364                 log.info("Success: ",
 365                         massageURL(newFunctionNode.getSource().getURL()),
 366                         '.',
 367                         functionName,
 368                         "' id=",
 369                         newFunctionNode.getId(),
 370                         " params=",
 371                         callSiteTypes.peek());
 372             }
 373         }
 374 
 375         callSiteTypes.pop();
 376         explodedArguments.pop();
 377 
 378         return newFunctionNode.setState(lc, CompilationState.BUILTINS_TRANSFORMED);
 379     }
 380 
 381     private static boolean isApply(final CallNode callNode) {
 382         final Expression f = callNode.getFunction();
 383         return f instanceof AccessNode && "apply".equals(((AccessNode)f).getProperty());
 384     }
 385 
 386     private static String massageURL(final URL url) {
 387         if (url == null) {
 388             return "<null>";
 389         }
 390         final String str = url.toString();
 391         final int slash = str.lastIndexOf('/');
 392         if (slash == -1) {
 393             return str;
 394         }
 395         return str.substring(slash + 1);
 396     }
 397 }