Bug 1054906 - Implement ES6 Symbol.hasInstance 1/2; r=evilpie,bz
authorMorgan Phillips <winter2718@gmail.com>
Mon, 06 Jun 2016 11:59:41 -0700
changeset 376150 35cc4a2451cc1aa71253c29c702db30e74c5f8ff
parent 376149 b27e5a33d24ccdeb54d582e56bdeb0429c26d3b7
child 376151 6394d8078dfe6a02451a5cc4fbf4ec7666ce5357
push id20510
push usercholler@mozilla.com
push dateTue, 07 Jun 2016 13:42:30 +0000
reviewersevilpie, bz
bugs1054906
milestone49.0a1
Bug 1054906 - Implement ES6 Symbol.hasInstance 1/2; r=evilpie,bz
js/src/jsapi.h
js/src/jsfun.cpp
js/src/jsfun.h
js/src/tests/ecma_6/Function/has-instance.js
js/src/tests/ecma_6/Symbol/well-known.js
js/src/vm/Interpreter.cpp
js/src/vm/Interpreter.h
js/xpconnect/tests/chrome/test_xrayToJS.xul
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -4981,16 +4981,17 @@ GetSymbolDescription(HandleSymbol symbol
 /* Well-known symbols. */
 #define JS_FOR_EACH_WELL_KNOWN_SYMBOL(macro) \
     macro(isConcatSpreadable) \
     macro(iterator) \
     macro(match) \
     macro(replace) \
     macro(search) \
     macro(species) \
+    macro(hasInstance) \
     macro(split) \
     macro(toPrimitive) \
     macro(unscopables)
 
 enum class SymbolCode : uint32_t {
     // There is one SymbolCode for each well-known symbol.
 #define JS_DEFINE_SYMBOL_ENUM(name) name,
     JS_FOR_EACH_WELL_KNOWN_SYMBOL(JS_DEFINE_SYMBOL_ENUM)  // SymbolCode::iterator, etc.
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -666,43 +666,90 @@ js::XDRInterpretedFunction(XDRState<mode
 }
 
 template bool
 js::XDRInterpretedFunction(XDRState<XDR_ENCODE>*, HandleObject, HandleScript, MutableHandleFunction);
 
 template bool
 js::XDRInterpretedFunction(XDRState<XDR_DECODE>*, HandleObject, HandleScript, MutableHandleFunction);
 
+/* ES6 (04-25-16) 19.2.3.6 Function.prototype [ @@hasInstance ] */
+bool
+js::fun_symbolHasInstance(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() < 1) {
+        args.rval().setBoolean(false);
+        return true;
+    }
+
+    /* Step 1. */
+    HandleValue func = args.thisv();
+    if (!func.isObject()) {
+        ReportIncompatible(cx, args);
+        return false;
+    }
+
+    RootedObject obj(cx, &func.toObject());
+    RootedValue v(cx, args[0]);
+
+    /* Step 2. */
+    bool result;
+    if (!OrdinaryHasInstance(cx, obj, &v, &result))
+        return false;
+
+    args.rval().setBoolean(result);
+    return true;
+}
+
 /*
- * [[HasInstance]] internal method for Function objects: fetch the .prototype
- * property of its 'this' parameter, and walks the prototype chain of v (only
- * if v is an object) returning true if .prototype is found.
+ * ES6 (4-25-16) 7.3.19 OrdinaryHasInstance
  */
-static bool
-fun_hasInstance(JSContext* cx, HandleObject objArg, MutableHandleValue v, bool* bp)
+bool
+js::OrdinaryHasInstance(JSContext* cx, HandleObject objArg, MutableHandleValue v, bool* bp)
 {
     RootedObject obj(cx, objArg);
 
-    while (obj->is<JSFunction>() && obj->isBoundFunction())
+    /* Step 1. */
+    if (!obj->isCallable()) {
+        *bp = false;
+        return true;
+    }
+
+    /* Step 2. */
+    if (obj->is<JSFunction>() && obj->isBoundFunction()) {
+        /* Steps 2a-b. */
         obj = obj->as<JSFunction>().getBoundFunctionTarget();
+        return InstanceOfOperator(cx, obj, v, bp);
+    }
 
+    /* Step 3. */
+    if (!v.isObject()) {
+        *bp = false;
+        return true;
+    }
+
+    /* Step 4. */
     RootedValue pval(cx);
     if (!GetProperty(cx, obj, obj, cx->names().prototype, &pval))
         return false;
 
+    /* Step 5. */
     if (pval.isPrimitive()) {
         /*
          * Throw a runtime error if instanceof is called on a function that
          * has a non-object as its .prototype value.
          */
         RootedValue val(cx, ObjectValue(*obj));
         ReportValueError(cx, JSMSG_BAD_PROTOTYPE, -1, val, nullptr);
         return false;
     }
 
+    /* Step 6. */
     RootedObject pobj(cx, &pval.toObject());
     bool isDelegate;
     if (!IsDelegate(cx, pobj, v, &isDelegate))
         return false;
     *bp = isDelegate;
     return true;
 }
 
@@ -852,17 +899,17 @@ static const ClassOps JSFunctionClassOps
     nullptr,                 /* delProperty */
     nullptr,                 /* getProperty */
     nullptr,                 /* setProperty */
     fun_enumerate,
     fun_resolve,
     fun_mayResolve,
     nullptr,                 /* finalize    */
     nullptr,                 /* call        */
-    fun_hasInstance,
+    nullptr,
     nullptr,                 /* construct   */
     fun_trace,
 };
 
 static const ClassSpec JSFunctionClassSpec = {
     CreateFunctionConstructor,
     CreateFunctionPrototype,
     nullptr,
@@ -1741,17 +1788,18 @@ OnBadFormal(JSContext* cx)
 const JSFunctionSpec js::function_methods[] = {
 #if JS_HAS_TOSOURCE
     JS_FN(js_toSource_str,   fun_toSource,   0,0),
 #endif
     JS_FN(js_toString_str,   fun_toString,   0,0),
     JS_FN(js_apply_str,      fun_apply,      2,0),
     JS_FN(js_call_str,       fun_call,       1,0),
     JS_FN("isGenerator",     fun_isGenerator,0,0),
-    JS_SELF_HOSTED_FN("bind", "FunctionBind", 2,JSFUN_HAS_REST),
+    JS_SELF_HOSTED_FN("bind", "FunctionBind", 2, JSFUN_HAS_REST),
+    JS_SYM_FN(hasInstance, fun_symbolHasInstance, 1, JSPROP_READONLY | JSPROP_PERMANENT),
     JS_FS_END
 };
 
 static bool
 FunctionConstructor(JSContext* cx, unsigned argc, Value* vp, GeneratorKind generatorKind)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
--- a/js/src/jsfun.h
+++ b/js/src/jsfun.h
@@ -672,16 +672,22 @@ DefineFunction(JSContext* cx, HandleObje
                gc::AllocKind allocKind = gc::AllocKind::FUNCTION);
 
 bool
 FunctionHasResolveHook(const JSAtomState& atomState, jsid id);
 
 extern bool
 fun_toString(JSContext* cx, unsigned argc, Value* vp);
 
+extern bool
+fun_symbolHasInstance(JSContext* cx, unsigned argc, Value* vp);
+
+extern bool
+OrdinaryHasInstance(JSContext* cx, HandleObject objArg, MutableHandleValue v, bool* bp);
+
 /*
  * Function extended with reserved slots for use by various kinds of functions.
  * Most functions do not have these extensions, but enough do that efficient
  * storage is required (no malloc'ed reserved slots).
  */
 class FunctionExtended : public JSFunction
 {
   public:
new file mode 100644
--- /dev/null
+++ b/js/src/tests/ecma_6/Function/has-instance.js
@@ -0,0 +1,88 @@
+// It is possible to override Function.prototype[@@hasInstance].
+let passed = false;
+let obj = { foo: true };
+let C = function(){};
+
+Object.defineProperty(C, Symbol.hasInstance, {
+  value: function(inst) { passed = inst.foo; return false; }
+});
+
+assertEq(obj instanceof C, false);
+assertEq(passed, true);
+
+{
+    let obj = {
+        [Symbol.hasInstance](v) { return true; },
+    };
+    let whatevs = {};
+    assertEq(whatevs instanceof obj, true);
+}
+
+{
+
+    function zzzz() {};
+    let xxxx = new zzzz();
+    assertEq(xxxx instanceof zzzz, true);
+    assertEq(zzzz[Symbol.hasInstance](xxxx), true);
+
+}
+
+// Non-callable objects should return false.
+const nonCallables = [
+    1,
+    undefined,
+    null,
+    "nope",
+]
+
+for (let nonCallable of nonCallables) {
+    assertEq(nonCallable instanceof Function, false);
+    assertEq(nonCallable instanceof Object, false);
+}
+
+// It should be possible to call the Symbol.hasInstance method directly.
+assertEq(Function.prototype[Symbol.hasInstance].call(Function, () => 1), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(Function, Object), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(Function, null), false);
+assertEq(Function.prototype[Symbol.hasInstance].call(Function, Array), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(Object, Array), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(Array, Function), false);
+assertEq(Function.prototype[Symbol.hasInstance].call(({}), Function), false);
+assertEq(Function.prototype[Symbol.hasInstance].call(), false)
+assertEq(Function.prototype[Symbol.hasInstance].call(({})), false)
+
+// Primitives should throw
+assertThrowsInstanceOf(() => {
+    Function.prototype[Symbol.hasInstance].call(1, Function);
+}, TypeError);
+
+// Non-callables should throw for the default method
+assertThrowsInstanceOf(() => {
+    function foo() {};
+    let obj = {};
+    foo instanceof obj;
+}, TypeError);
+
+// However non-callables do not throw for overridden methods
+for (let nonCallable of [1, undefined, null, {}]) {
+    let o = {[Symbol.hasInstance](v) { return true; }}
+    assertEq(nonCallable instanceof o, true);
+}
+
+// Ensure that bound functions are unwrapped properly
+let bindme = {x: function() {}};
+let instance = new bindme.x();
+let xOuter = bindme.x;
+let bound = xOuter.bind(bindme);
+let doubleBound = bound.bind(bindme);
+let tripleBound = bound.bind(doubleBound);
+assertEq(Function.prototype[Symbol.hasInstance].call(bound, instance), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(doubleBound, instance), true);
+assertEq(Function.prototype[Symbol.hasInstance].call(tripleBound, instance), true);
+
+// Function.prototype[Symbol.hasInstance] is not configurable
+let desc = Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance)
+assertEq(desc.configurable, false)
+
+if (typeof reportCompare === "function")
+    reportCompare(true, true);
--- a/js/src/tests/ecma_6/Symbol/well-known.js
+++ b/js/src/tests/ecma_6/Symbol/well-known.js
@@ -3,16 +3,17 @@
 
 var names = [
     "isConcatSpreadable",
     "iterator",
     "match",
     "replace",
     "search",
     "species",
+    "hasInstance",
     "split",
     "toPrimitive",
     "unscopables"
 ];
 
 for (var name of names) {
     // Well-known symbols exist.
     assertEq(typeof Symbol[name], "symbol");
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -704,28 +704,61 @@ js::Execute(JSContext* cx, HandleScript 
         MOZ_ASSERT_IF(!s->enclosingScope(), s->is<GlobalObject>());
     } while ((s = s->enclosingScope()));
 #endif
 
     return ExecuteKernel(cx, script, *scopeChain, NullValue(),
                          NullFramePtr() /* evalInFrame */, rval);
 }
 
+/*
+ * ES6 (4-25-16) 12.10.4 InstanceofOperator
+ */
+extern bool
+js::InstanceOfOperator(JSContext* cx, HandleObject obj, MutableHandleValue v, bool* bp)
+{
+    /* Step 1. is handled by caller. */
+
+    /* Step 2. */
+    RootedValue hasInstance(cx);
+    RootedId id(cx, SYMBOL_TO_JSID(cx->wellKnownSymbols().hasInstance));
+    if (!GetProperty(cx, obj, obj, id, &hasInstance))
+        return false;
+
+    if (!hasInstance.isNullOrUndefined()) {
+        if (!IsCallable(hasInstance))
+            ReportIsNotFunction(cx, hasInstance);
+
+        /* Step 3. */
+        RootedValue rval(cx);
+        if (!Call(cx, hasInstance, obj, v, &rval))
+            return false;
+        *bp = ToBoolean(rval);
+        return true;
+    }
+
+    /* Step 4. */
+    if (!obj->isCallable()) {
+        RootedValue val(cx, ObjectValue(*obj));
+        ReportIsNotFunction(cx, val);
+        return false;
+    }
+
+    /* Step 5. */
+    return js::OrdinaryHasInstance(cx, obj, v, bp);
+}
+
 bool
 js::HasInstance(JSContext* cx, HandleObject obj, HandleValue v, bool* bp)
 {
     const Class* clasp = obj->getClass();
     RootedValue local(cx, v);
     if (JSHasInstanceOp hasInstance = clasp->getHasInstance())
         return hasInstance(cx, obj, &local, bp);
-
-    RootedValue val(cx, ObjectValue(*obj));
-    ReportValueError(cx, JSMSG_BAD_INSTANCEOF_RHS,
-                        JSDVG_SEARCH_STACK, val, nullptr);
-    return false;
+    return js::InstanceOfOperator(cx, obj, &local, bp);
 }
 
 static inline bool
 EqualGivenSameType(JSContext* cx, HandleValue lval, HandleValue rval, bool* equal)
 {
     MOZ_ASSERT(SameType(lval, rval));
 
     if (lval.isString())
--- a/js/src/vm/Interpreter.h
+++ b/js/src/vm/Interpreter.h
@@ -318,16 +318,19 @@ SameValue(JSContext* cx, HandleValue v1,
 
 extern JSType
 TypeOfObject(JSObject* obj);
 
 extern JSType
 TypeOfValue(const Value& v);
 
 extern bool
+InstanceOfOperator(JSContext* cx, HandleObject obj, MutableHandleValue v, bool* bp);
+
+extern bool
 HasInstance(JSContext* cx, HandleObject obj, HandleValue v, bool* bp);
 
 // Unwind scope chain and iterator to match the static scope corresponding to
 // the given bytecode position.
 extern void
 UnwindScope(JSContext* cx, ScopeIter& si, jsbytecode* pc);
 
 // Unwind all scopes.
--- a/js/xpconnect/tests/chrome/test_xrayToJS.xul
+++ b/js/xpconnect/tests/chrome/test_xrayToJS.xul
@@ -209,17 +209,17 @@ https://bugzilla.mozilla.org/show_bug.cg
       gConstructorProperties[c] = constructorProps([]);
   }
   // toString and toSource only live on the parent proto (Error.prototype).
   gPrototypeProperties['Error'].push('toString');
   gPrototypeProperties['Error'].push('toSource');
 
   gPrototypeProperties['Function'] =
     ["constructor", "toSource", "toString", "apply", "call", "bind",
-     "isGenerator", "length", "name", "arguments", "caller"];
+     "isGenerator", "length", "name", "arguments", "caller", Symbol.hasInstance];
   gConstructorProperties['Function'] = constructorProps([])
 
   gPrototypeProperties['RegExp'] =
     ["constructor", "toSource", "toString", "compile", "exec", "test",
      Symbol.match, Symbol.replace, Symbol.search, Symbol.split,
      "flags", "global", "ignoreCase", "multiline", "source", "sticky", "unicode",
      "lastIndex"];
   gConstructorProperties['RegExp'] =
@@ -574,17 +574,17 @@ https://bugzilla.mozilla.org/show_bug.cg
     is(trickyArray.wrappedJSObject[11], "some other indexed property");
     trickyArray.length = 0;
     is(trickyArray.length, 0, "Setting length works over Xray");
     is(trickyArray[11], undefined, "Setting length truncates over Xray");
     Object.defineProperty(trickyArray, 'length', { configurable: false, enumerable: false, writable: false, value: 0 });
     trickyArray[1] = "hi";
     is(trickyArray.length, 0, "Length remains non-writable");
     is(trickyArray[1], undefined, "Frozen length forbids new properties");
-
+    is(trickyArray instanceof iwin.Array, true, "instanceof should work across xray wrappers.");
     testTrickyObject(trickyArray);
 
     testArrayIterators(new iwin.Array(1, 1, 2, 3, 5), [1, 1, 2, 3, 5]);
   }
 
   // Parts of this function are kind of specific to testing Object, but we factor
   // it out so that we can re-use the trickyObject stuff on Arrays.
   function testTrickyObject(trickyObject) {