1 /*
   2  * Copyright (c) 2010, 2013, 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.runtime;
  27 
  28 import static jdk.nashorn.internal.lookup.Lookup.MH;
  29 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
  30 
  31 import java.lang.invoke.MethodHandle;
  32 import java.lang.invoke.MethodHandles;
  33 import java.lang.invoke.MethodType;
  34 import java.lang.invoke.SwitchPoint;
  35 import jdk.dynalink.CallSiteDescriptor;
  36 import jdk.dynalink.NamedOperation;
  37 import jdk.dynalink.Operation;
  38 import jdk.dynalink.StandardOperation;
  39 import jdk.dynalink.linker.GuardedInvocation;
  40 import jdk.dynalink.linker.LinkRequest;
  41 import jdk.nashorn.api.scripting.AbstractJSObject;
  42 import jdk.nashorn.api.scripting.ScriptObjectMirror;
  43 import jdk.nashorn.internal.runtime.linker.NashornCallSiteDescriptor;
  44 import jdk.nashorn.internal.runtime.linker.NashornGuards;
  45 
  46 /**
  47  * This class supports the handling of scope in a with body.
  48  *
  49  */
  50 public final class WithObject extends Scope {
  51     private static final MethodHandle WITHEXPRESSIONGUARD    = findOwnMH("withExpressionGuard",  boolean.class, Object.class, PropertyMap.class, SwitchPoint[].class);
  52     private static final MethodHandle WITHEXPRESSIONFILTER   = findOwnMH("withFilterExpression", Object.class, Object.class);
  53     private static final MethodHandle WITHSCOPEFILTER        = findOwnMH("withFilterScope",      Object.class, Object.class);
  54     private static final MethodHandle BIND_TO_EXPRESSION_OBJ = findOwnMH("bindToExpression",     Object.class, Object.class, Object.class);
  55     private static final MethodHandle BIND_TO_EXPRESSION_FN  = findOwnMH("bindToExpression",     Object.class, ScriptFunction.class, Object.class);
  56 
  57     /** With expression object. */
  58     private final ScriptObject expression;
  59 
  60     /**
  61      * Constructor
  62      *
  63      * @param scope scope object
  64      * @param expression with expression
  65      */
  66     WithObject(final ScriptObject scope, final ScriptObject expression) {
  67         super(scope, null);
  68         this.expression = expression;
  69         setIsInternal();
  70     }
  71 
  72     /**
  73      * Delete a property based on a key.
  74      * @param key Any valid JavaScript value.
  75      * @param strict strict mode execution.
  76      * @return True if deleted.
  77      */
  78     @Override
  79     public boolean delete(final Object key, final boolean strict) {
  80         final ScriptObject self = expression;
  81         final String propName = JSType.toString(key);
  82 
  83         final FindProperty find = self.findProperty(propName, true);
  84 
  85         if (find != null) {
  86             return self.delete(propName, strict);
  87         }
  88 
  89         return false;
  90     }
  91 
  92 
  93     @Override
  94     public GuardedInvocation lookup(final CallSiteDescriptor desc, final LinkRequest request) {
  95         if (request.isCallSiteUnstable()) {
  96             // Fall back to megamorphic invocation which performs a complete lookup each time without further relinking.
  97             return super.lookup(desc, request);
  98         }
  99 
 100         GuardedInvocation link = null;
 101         final Operation op = desc.getOperation();
 102 
 103         assert op instanceof NamedOperation; // WithObject is a scope object, access is always named
 104         final String name = ((NamedOperation)op).getName().toString();
 105 
 106         FindProperty find = expression.findProperty(name, true);
 107 
 108         if (find != null) {
 109             link = expression.lookup(desc, request);
 110             if (link != null) {
 111                 return fixExpressionCallSite(desc, link);
 112             }
 113         }
 114 
 115         final ScriptObject scope = getProto();
 116         find = scope.findProperty(name, true);
 117 
 118         if (find != null) {
 119             return fixScopeCallSite(scope.lookup(desc, request), name, find.getOwner());
 120         }
 121 
 122         // the property is not found - now check for
 123         // __noSuchProperty__ and __noSuchMethod__ in expression
 124         final String fallBack;
 125 
 126         final Operation firstOp = NashornCallSiteDescriptor.getBaseOperation(desc);
 127         if (firstOp == StandardOperation.GET) {
 128             if (NashornCallSiteDescriptor.isMethodFirstOperation(desc)) {
 129                 fallBack = NO_SUCH_METHOD_NAME;
 130             } else {
 131                 fallBack = NO_SUCH_PROPERTY_NAME;
 132             }
 133         } else {
 134             fallBack = null;
 135         }
 136 
 137         if (fallBack != null) {
 138             find = expression.findProperty(fallBack, true);
 139             if (find != null) {
 140                 if (NO_SUCH_METHOD_NAME.equals(fallBack)) {
 141                     link = expression.noSuchMethod(desc, request).addSwitchPoint(getProtoSwitchPoint(name));
 142                 } else if (NO_SUCH_PROPERTY_NAME.equals(fallBack)) {
 143                     link = expression.noSuchProperty(desc, request).addSwitchPoint(getProtoSwitchPoint(name));
 144                 }
 145             }
 146         }
 147 
 148         if (link != null) {
 149             return fixExpressionCallSite(desc, link);
 150         }
 151 
 152         // still not found, may be scope can handle with it's own
 153         // __noSuchProperty__, __noSuchMethod__ etc.
 154         link = scope.lookup(desc, request);
 155 
 156         if (link != null) {
 157             return fixScopeCallSite(link, name, null);
 158         }
 159 
 160         return null;
 161     }
 162 
 163     /**
 164      * Overridden to try to find the property first in the expression object (and its prototypes), and only then in this
 165      * object (and its prototypes).
 166      *
 167      * @param key  Property key.
 168      * @param deep Whether the search should look up proto chain.
 169      * @param isScope true if is this a scope access
 170      * @param start the object on which the lookup was originally initiated
 171      * @return FindPropertyData or null if not found.
 172      */
 173     @Override
 174     protected FindProperty findProperty(final Object key, final boolean deep, final boolean isScope, final ScriptObject start) {
 175         // We call findProperty on 'expression' with 'expression' itself as start parameter.
 176         // This way in ScriptObject.setObject we can tell the property is from a 'with' expression
 177         // (as opposed from another non-scope object in the proto chain such as Object.prototype).
 178         final FindProperty exprProperty = expression.findProperty(key, true, false, expression);
 179         if (exprProperty != null) {
 180             return exprProperty;
 181         }
 182         return super.findProperty(key, deep, isScope, start);
 183     }
 184 
 185     @Override
 186     protected Object invokeNoSuchProperty(final Object key, final boolean isScope, final int programPoint) {
 187         final FindProperty find = expression.findProperty(NO_SUCH_PROPERTY_NAME, true);
 188         if (find != null) {
 189             final Object func = find.getObjectValue();
 190             if (func instanceof ScriptFunction) {
 191                 final ScriptFunction sfunc = (ScriptFunction)func;
 192                 final Object self = isScope && sfunc.isStrict()? UNDEFINED : expression;
 193                 return ScriptRuntime.apply(sfunc, self, key);
 194             }
 195         }
 196 
 197         return getProto().invokeNoSuchProperty(key, isScope, programPoint);
 198     }
 199 
 200     @Override
 201     public void setSplitState(final int state) {
 202         ((Scope) getNonWithParent()).setSplitState(state);
 203     }
 204 
 205     @Override
 206     public int getSplitState() {
 207         return ((Scope) getNonWithParent()).getSplitState();
 208     }
 209 
 210     @Override
 211     public void addBoundProperties(final ScriptObject source, final Property[] properties) {
 212         // Declared variables in nested eval go to first normal (non-with) parent scope.
 213         getNonWithParent().addBoundProperties(source, properties);
 214     }
 215 
 216     /**
 217      * Get first parent scope that is not an instance of WithObject.
 218      */
 219     private ScriptObject getNonWithParent() {
 220         ScriptObject proto = getProto();
 221 
 222         while (proto != null && proto instanceof WithObject) {
 223             proto = proto.getProto();
 224         }
 225 
 226         return proto;
 227     }
 228 
 229     private static GuardedInvocation fixReceiverType(final GuardedInvocation link, final MethodHandle filter) {
 230         // The receiver may be an Object or a ScriptObject.
 231         final MethodType invType = link.getInvocation().type();
 232         final MethodType newInvType = invType.changeParameterType(0, filter.type().returnType());
 233         return link.asType(newInvType);
 234     }
 235 
 236     private static GuardedInvocation fixExpressionCallSite(final CallSiteDescriptor desc, final GuardedInvocation link) {
 237         // If it's not a getMethod, just add an expression filter that converts WithObject in "this" position to its
 238         // expression.
 239         if (NashornCallSiteDescriptor.getBaseOperation(desc) != StandardOperation.GET || !NashornCallSiteDescriptor.isMethodFirstOperation(desc)) {
 240             return fixReceiverType(link, WITHEXPRESSIONFILTER).filterArguments(0, WITHEXPRESSIONFILTER);
 241         }
 242 
 243         final MethodHandle linkInvocation      = link.getInvocation();
 244         final MethodType   linkType            = linkInvocation.type();
 245         final boolean      linkReturnsFunction = ScriptFunction.class.isAssignableFrom(linkType.returnType());
 246 
 247         return link.replaceMethods(
 248                 // Make sure getMethod will bind the script functions it receives to WithObject.expression
 249                 MH.foldArguments(
 250                         linkReturnsFunction ?
 251                                 BIND_TO_EXPRESSION_FN :
 252                                 BIND_TO_EXPRESSION_OBJ,
 253                         filterReceiver(
 254                                 linkInvocation.asType(
 255                                         linkType.changeReturnType(
 256                                                 linkReturnsFunction ?
 257                                                         ScriptFunction.class :
 258                                                         Object.class).
 259                                                             changeParameterType(
 260                                                                     0,
 261                                                                     Object.class)),
 262                                         WITHEXPRESSIONFILTER)),
 263                          filterGuardReceiver(link, WITHEXPRESSIONFILTER));
 264      // No clever things for the guard -- it is still identically filtered.
 265 
 266     }
 267 
 268     private GuardedInvocation fixScopeCallSite(final GuardedInvocation link, final String name, final ScriptObject owner) {
 269         final GuardedInvocation newLink             = fixReceiverType(link, WITHSCOPEFILTER);
 270         final MethodHandle      expressionGuard     = expressionGuard(name, owner);
 271         final MethodHandle      filteredGuard       = filterGuardReceiver(newLink, WITHSCOPEFILTER);
 272         return link.replaceMethods(
 273                 filterReceiver(
 274                         newLink.getInvocation(),
 275                         WITHSCOPEFILTER),
 276                 NashornGuards.combineGuards(
 277                         expressionGuard,
 278                         filteredGuard));
 279     }
 280 
 281     private static MethodHandle filterGuardReceiver(final GuardedInvocation link, final MethodHandle receiverFilter) {
 282         final MethodHandle test = link.getGuard();
 283         if (test == null) {
 284             return null;
 285         }
 286 
 287         final Class<?> receiverType = test.type().parameterType(0);
 288         final MethodHandle filter = MH.asType(receiverFilter,
 289                 receiverFilter.type().changeParameterType(0, receiverType).
 290                 changeReturnType(receiverType));
 291 
 292         return filterReceiver(test, filter);
 293     }
 294 
 295     private static MethodHandle filterReceiver(final MethodHandle mh, final MethodHandle receiverFilter) {
 296         //With expression filter == receiverFilter, i.e. receiver is cast to withobject and its expression returned
 297         return MH.filterArguments(mh, 0, receiverFilter.asType(receiverFilter.type().changeReturnType(mh.type().parameterType(0))));
 298     }
 299 
 300     /**
 301      * Drops the WithObject wrapper from the expression.
 302      * @param receiver WithObject wrapper.
 303      * @return The with expression.
 304      */
 305     public static Object withFilterExpression(final Object receiver) {
 306         return ((WithObject)receiver).expression;
 307     }
 308 
 309     @SuppressWarnings("unused")
 310     private static Object bindToExpression(final Object fn, final Object receiver) {
 311         if (fn instanceof ScriptFunction) {
 312             return bindToExpression((ScriptFunction) fn, receiver);
 313         } else if (fn instanceof ScriptObjectMirror) {
 314             final ScriptObjectMirror mirror = (ScriptObjectMirror)fn;
 315             if (mirror.isFunction()) {
 316                 // We need to make sure correct 'this' is used for calls with Ident call
 317                 // expressions. We do so here using an AbstractJSObject instance.
 318                 return new AbstractJSObject() {
 319                     @Override
 320                     public Object call(final Object thiz, final Object... args) {
 321                         return mirror.call(withFilterExpression(receiver), args);
 322                     }
 323                 };
 324             }
 325         }
 326 
 327         return fn;
 328     }
 329 
 330     private static Object bindToExpression(final ScriptFunction fn, final Object receiver) {
 331         return fn.createBound(withFilterExpression(receiver), ScriptRuntime.EMPTY_ARRAY);
 332     }
 333 
 334     private MethodHandle expressionGuard(final String name, final ScriptObject owner) {
 335         final PropertyMap map = expression.getMap();
 336         final SwitchPoint[] sp = expression.getProtoSwitchPoints(name, owner);
 337         return MH.insertArguments(WITHEXPRESSIONGUARD, 1, map, sp);
 338     }
 339 
 340     @SuppressWarnings("unused")
 341     private static boolean withExpressionGuard(final Object receiver, final PropertyMap map, final SwitchPoint[] sp) {
 342         return ((WithObject)receiver).expression.getMap() == map && !hasBeenInvalidated(sp);
 343     }
 344 
 345     private static boolean hasBeenInvalidated(final SwitchPoint[] switchPoints) {
 346         if (switchPoints != null) {
 347             for (final SwitchPoint switchPoint : switchPoints) {
 348                 if (switchPoint.hasBeenInvalidated()) {
 349                     return true;
 350                 }
 351             }
 352         }
 353         return false;
 354     }
 355 
 356     /**
 357      * Drops the WithObject wrapper from the scope.
 358      * @param receiver WithObject wrapper.
 359      * @return The with scope.
 360      */
 361     public static Object withFilterScope(final Object receiver) {
 362         return ((WithObject)receiver).getProto();
 363     }
 364 
 365     /**
 366      * Get the with expression for this {@code WithObject}
 367      * @return the with expression
 368      */
 369     public ScriptObject getExpression() {
 370         return expression;
 371     }
 372 
 373     private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
 374         return MH.findStatic(MethodHandles.lookup(), WithObject.class, name, MH.type(rtype, types));
 375     }
 376 }