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