Add Debug.Function.prototype.apply. Also, correctly handle bad resumption values by creating a TypeError and passing it to the uncaughtExceptionHook.
authorJason Orendorff <jorendorff@mozilla.com>
Thu, 05 May 2011 14:34:11 -0500
changeset 74411 9914b62860dc96fc75448f78abd9ecd460beae57
parent 74410 ee8bde5b1b7729ee7894d13eaf9bc1b5d155b93f
child 74412 012541acc62a95c78dcbd6549f91f52b3ea29db0
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
milestone6.0a1
Add Debug.Function.prototype.apply. Also, correctly handle bad resumption values by creating a TypeError and passing it to the uncaughtExceptionHook.
js/src/jit-test/tests/debug/Function-apply-01.js
js/src/jit-test/tests/debug/Function-apply-02.js
js/src/jit-test/tests/debug/Function-apply-03.js
js/src/jit-test/tests/debug/uncaughtExceptionHook-02.js
js/src/jsdbg.cpp
js/src/jsdbg.h
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Function-apply-01.js
@@ -0,0 +1,47 @@
+// |jit-test| debug
+// tests calling script functions via Debug.Function.prototype.apply
+
+load(libdir + "asserts.js");
+
+var g = newGlobal("new-compartment");
+g.eval("function f() { debugger; }");
+var dbg = new Debug(g);
+dbg.hooks = {debuggerHandler: function () {}};
+
+dbg.hooks.debuggerHandler = function (frame) {
+    var fn = frame.arguments[0];
+    var cv = fn.apply(null, [9, 16]);
+    assertEq(Object.keys(cv).join(","), "return");
+    assertEq(Object.getPrototypeOf(cv), Object.prototype);
+    assertEq(cv.return, 25);
+
+    cv = fn.apply(null, ["hello ", "world"]);
+    assertEq(Object.keys(cv).join(","), "return");
+    assertEq(cv.return, "hello world");
+
+    // Handle more or less arguments.
+    assertEq(fn.apply(null, [1, 5, 100]).return, 6);
+    assertEq(fn.apply(null, []).return, NaN);
+    assertEq(fn.apply().return, NaN);
+
+    // Throw if a this-value or argument is an object but not a Debug.Object.
+    assertThrowsInstanceOf(function () { fn.apply({}, []); }, TypeError);
+    assertThrowsInstanceOf(function () { fn.apply(null, [{}]); }, TypeError);
+};
+g.eval("f(function (a, b) { return a + b; });");
+
+// The callee receives the right arguments even if more arguments are provided
+// than the callee's .length.
+dbg.hooks.debuggerHandler = function (frame) {
+    assertEq(frame.arguments[0].apply(null, ['one', 'two']).return, 2);
+};
+g.eval("f(function () { return arguments.length; });");
+
+// Exceptions are reported as {throw:} completion values.
+dbg.hooks.debuggerHandler = function (frame) {
+    var lose = frame.arguments[0];
+    var cv = lose.apply(null, []);
+    assertEq(Object.keys(cv).join(","), "throw");
+    assertEq(cv.throw, frame.callee);
+};
+g.eval("f(function lose() { throw f; });");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Function-apply-02.js
@@ -0,0 +1,48 @@
+// |jit-test| debug
+// tests calling native functions via Debug.Function.prototype.apply
+
+load(libdir + "asserts.js");
+
+var g = newGlobal("new-compartment");
+g.eval("function f() { debugger; }");
+var dbg = new Debug(g);
+dbg.hooks = {debuggerHandler: function () {}};
+
+dbg.hooks.debuggerHandler = function (frame) {
+    var max = frame.arguments[0];
+    var cv = max.apply(null, [9, 16]);
+    assertEq(cv.return, 16);
+
+    cv = max.apply();
+    assertEq(cv.return, -1/0);
+
+    cv = max.apply(null, [2, 5, 3, 8, 1, 9, 4, 6, 7]);
+    assertEq(cv.return, 9);
+
+    // second argument to apply must be an array
+    assertThrowsInstanceOf(function () { max.apply(null, 12); }, TypeError);
+};
+g.eval("f(Math.max);");
+
+dbg.hooks.debuggerHandler = function (frame) {
+    var push = frame.arguments[0];
+    var arr = frame.arguments[1];
+    var cv;
+
+    cv = push.apply(arr, [0, 1, 2]);
+    assertEq(cv.return, 3);
+
+    cv = push.apply(arr, [arr]);
+    assertEq(cv.return, 4);
+
+    cv = push.apply(arr);
+    assertEq(cv.return, 4);
+
+    // you can apply Array.prototype.push to a string; it does ToObject on it.
+    cv = push.apply("hello", ["world"]);
+    assertEq(cv.return, 6);
+};
+g.eval("var a = []; f(Array.prototype.push, a);");
+assertEq(g.a.length, 4);
+assertEq(g.a.slice(0, 3).join(","), "0,1,2");
+assertEq(g.a[3], g.a);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Function-apply-03.js
@@ -0,0 +1,17 @@
+// |jit-test| debug
+// reentering the debugger several times via debuggerHandler and apply() on a single stack
+
+var g = newGlobal("new-compartment");
+var dbg = Debug(g);
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var n = frame.arguments[0];
+        if (n > 1) {
+            var result = frame.callee.apply(null, [n - 1]);
+            result.return *= n;
+            return result;
+        }
+    }
+};
+g.eval("function fac(n) { debugger; return 1; }");
+assertEq(g.fac(5), 5 * 4 * 3 * 2 * 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/uncaughtExceptionHook-02.js
@@ -0,0 +1,13 @@
+// |jit-test| debug
+// Returning a bad resumption value causes an exception that is reported to the
+// uncaughtExceptionHook.
+
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+dbg.hooks = {debuggerHandler: function () { return {oops: "bad resumption value"}; }};
+dbg.uncaughtExceptionHook = function (exc) {
+    assertEq(exc instanceof TypeError, true);
+    return {return: "pass"};
+};
+
+assertEq(g.eval("debugger"), "pass");
--- a/js/src/jsdbg.cpp
+++ b/js/src/jsdbg.cpp
@@ -162,17 +162,17 @@ Debug::fromChildJSObject(JSObject *obj)
 }
 
 bool
 Debug::getScriptFrame(JSContext *cx, StackFrame *fp, Value *vp)
 {
     JS_ASSERT(fp->isScriptFrame());
     FrameMap::AddPtr p = frames.lookupForAdd(fp);
     if (!p) {
-        // Create script frame. First copy the arguments.
+        // Create script Debug.Frame. First copy the arguments.
         JSObject *argsobj;
         if (fp->hasArgs()) {
             uintN argc = fp->numActualArgs();
             JS_ASSERT(uint(argc) == argc);
             argsobj = NewDenseAllocatedArray(cx, uint(argc), NULL);
             Value *argv = fp->actualArgs();
             for (uintN i = 0; i < argc; i++) {
                 Value v = argv[i];
@@ -267,30 +267,26 @@ Debug::wrapDebuggeeValue(JSContext *cx, 
         }
     }
     return true;
 }
 
 bool
 Debug::unwrapDebuggeeValue(JSContext *cx, Value *vp)
 {
+    assertSameCompartment(cx, object, *vp);
     if (vp->isObject()) {
         JSObject *dobj = &vp->toObject();
         if (dobj->clasp != &DebugObject_class && dobj->clasp != &DebugFunction_class) {
             JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_EXPECTED_TYPE,
                                  "Debug", "Debug.Object", dobj->clasp->name);
             return false;
         }
         *vp = dobj->getReservedSlot(JSSLOT_DEBUGOBJECT_CCW);
     }
-
-    if (!cx->compartment->wrap(cx, vp)) {
-        vp->setUndefined();
-        return false;
-    }
     return true;
 }
 
 JSTrapStatus
 Debug::handleUncaughtException(AutoCompartment &ac, Value *vp, bool callHook)
 {
     JSContext *cx = ac.context;
     if (cx->isExceptionPending()) {
@@ -306,16 +302,49 @@ Debug::handleUncaughtException(AutoCompa
         if (cx->isExceptionPending()) {
             JS_ReportPendingException(cx);
             cx->clearPendingException();
         }
     }
     return JSTRAP_ERROR;
 }
 
+bool
+Debug::newCompletionValue(AutoCompartment &ac, bool ok, Value val, Value *vp)
+{
+    JS_ASSERT_IF(ok, !ac.context->isExceptionPending());
+
+    JSContext *cx = ac.context;
+    jsid key;
+    if (ok) {
+        ac.leave();
+        key = ATOM_TO_JSID(cx->runtime->atomState.returnAtom);
+    } else if (cx->isExceptionPending()) {
+        key = ATOM_TO_JSID(cx->runtime->atomState.throwAtom);
+        val = cx->getPendingException();
+        cx->clearPendingException();
+        ac.leave();
+    } else {
+        ac.leave();
+        vp->setNull();
+        return true;
+    }
+
+    JSObject *obj = NewBuiltinClassInstance(cx, &js_ObjectClass);
+    if (!obj ||
+        !wrapDebuggeeValue(cx, &val) ||
+        !js_DefineNativeProperty(cx, obj, key, val, PropertyStub, StrictPropertyStub,
+                                 JSPROP_ENUMERATE, 0, 0, NULL))
+    {
+        return false;
+    }
+    vp->setObject(*obj);
+    return true;
+}
+
 JSTrapStatus
 Debug::parseResumptionValue(AutoCompartment &ac, bool ok, const Value &rv, Value *vp,
                             bool callHook)
 {
     vp->setUndefined();
     if (!ok)
         return handleUncaughtException(ac, vp, callHook);
     if (rv.isUndefined())
@@ -335,24 +364,22 @@ Debug::parseResumptionValue(AutoCompartm
         shape->previous()->previous() ||
         (shape->id != returnId && shape->id != throwId) ||
         !shape->isDataDescriptor())
     {
         JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_BAD_RESUMPTION);
         return handleUncaughtException(ac, vp, callHook);
     }
 
-    if (!js_NativeGet(cx, obj, obj, shape, 0, vp))
+    if (!js_NativeGet(cx, obj, obj, shape, 0, vp) || !unwrapDebuggeeValue(cx, vp))
         return handleUncaughtException(ac, vp, callHook);
 
     ac.leave();
-    if (!unwrapDebuggeeValue(cx, vp)) {
-        // Swallow this exception rather than report it in the debuggee's
-        // compartment.  But return JSTRAP_ERROR to terminate the debuggee.
-        cx->clearPendingException();
+    if (!cx->compartment->wrap(cx, vp)) {
+        vp->setUndefined();
         return JSTRAP_ERROR;
     }
     return shape->id == returnId ? JSTRAP_RETURN : JSTRAP_THROW;
 }
 
 bool
 CallMethodIfPresent(JSContext *cx, JSObject *obj, const char *name, int argc, Value *argv,
                     Value *rval)
@@ -769,17 +796,16 @@ DebugFrame_getThis(JSContext *cx, uintN 
     THIS_FRAME(cx, vp, "get this", thisobj, fp);
     {
         AutoCompartment ac(cx, &fp->scopeChain());
         if (!ac.enter())
             return false;
         if (!ComputeThis(cx, fp))
             return false;
         *vp = fp->thisValue();
-        ac.leave();
     }
     return Debug::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, vp);
 }
 
 static JSBool
 DebugFrame_getOlder(JSContext *cx, uintN argc, Value *vp)
 {
     THIS_FRAME(cx, vp, "get this", thisobj, thisfp);
@@ -933,21 +959,84 @@ DebugFunction_getName(JSContext *cx, uin
     if (JSString *name = funobj->getFunctionPrivate()->atom) {
         vp->setString(name);
         return Debug::fromChildJSObject(&vp[1].toObject())->wrapDebuggeeValue(cx, vp);
     }
     vp->setNull();
     return true;
 }
 
+static JSBool
+DebugFunction_apply(JSContext *cx, uintN argc, Value *vp)
+{
+    // Don't require a Debug.Function. Any Debug.Object might be callable.
+    // Check callability using JSObject::isCallable below.
+    THIS_DEBUGOBJECT_REFERENT(cx, vp, "apply", obj);
+    Debug *dbg = Debug::fromChildJSObject(&vp[1].toObject());
+
+    // Any JS exceptions thrown must be in the debugger compartment, so do
+    // sanity checks and fallible conversions before entering the debuggee.
+    if (!obj->isCallable()) {
+        ReportIncompatibleMethod(cx, vp, &DebugFunction_class);
+        return false;
+    }
+
+    // Unwrap Debug.Objects. This happens in the debugger's compartment since
+    // that is where any exceptions must be reported.
+    Value calleev = vp[1];
+    Value thisv = argc > 0 ? vp[2] : UndefinedValue();
+    AutoValueVector argv(cx);
+    if (!dbg->unwrapDebuggeeValue(cx, &calleev) || !dbg->unwrapDebuggeeValue(cx, &thisv))
+        return false;
+    if (argc >= 2 && !vp[3].isNullOrUndefined()) {
+        if (!vp[3].isObject()) {
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BAD_APPLY_ARGS, js_apply_str);
+            return false;
+        }
+        JSObject *argsobj = &vp[3].toObject();
+        uintN length;
+        if (!js_GetLengthProperty(cx, argsobj, &length))
+            return false;
+        length = uintN(JS_MIN(length, JS_ARGS_LENGTH_MAX));
+
+        if (!argv.growBy(length) || !GetElements(cx, argsobj, length, argv.begin()))
+            return false;
+        for (uintN i = 0; i < length; i++) {
+            if (!dbg->unwrapDebuggeeValue(cx, &argv[i]))
+                return false;
+        }
+    }
+
+    // Enter the debuggee compartment and rewrap all input value for that compartment.
+    // (Rewrapping always takes place in the destination compartment.)
+    AutoCompartment ac(cx, obj);
+    if (!ac.enter() || !cx->compartment->wrap(cx, &calleev) || !cx->compartment->wrap(cx, &thisv))
+        return false;
+    for (Value *p = argv.begin(); p != argv.end(); ++p) {
+        if (!cx->compartment->wrap(cx, p))
+            return false;
+    }
+
+    // Call the function. Use newCompletionValue to return to the debugger
+    // compartment and populate *vp.
+    Value rval;
+    bool ok = ExternalInvoke(cx, thisv, calleev, argv.length(), argv.begin(), &rval);
+    return dbg->newCompletionValue(ac, ok, rval, vp);
+}
+
 static JSPropertySpec DebugFunction_properties[] = {
     JS_PSG("name", DebugFunction_getName, 0),
     JS_PS_END
 };
 
+static JSFunctionSpec DebugFunction_methods[] = {
+    JS_FN("apply", DebugFunction_apply, 0, 0),
+    JS_FS_END
+};
+
 // === Glue
 
 extern JS_PUBLIC_API(JSBool)
 JS_DefineDebugObject(JSContext *cx, JSObject *obj)
 {
     JSObject *objProto;
     if (!js_GetClassPrototype(cx, obj, JSProto_Object, &objProto))
         return false;
@@ -968,17 +1057,18 @@ JS_DefineDebugObject(JSContext *cx, JSOb
     JSObject *objectProto = js_InitClass(cx, debugCtor, objProto, &DebugObject_class,
                                          DebugObject_construct, 0,
                                          NULL, DebugObject_methods, NULL, NULL);
     if (!objectProto)
         return false;
 
     JSObject *functionProto = js_InitClass(cx, debugCtor, objectProto, &DebugFunction_class,
                                            DebugFunction_construct, 0,
-                                           DebugFunction_properties, NULL, NULL, NULL);
+                                           DebugFunction_properties, DebugFunction_methods,
+                                           NULL, NULL);
     if (!functionProto)
         return false;
 
     debugProto->setReservedSlot(JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto));
     debugProto->setReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto));
     debugProto->setReservedSlot(JSSLOT_DEBUG_FUNCTION_PROTO, ObjectValue(*functionProto));
     return true;
 }
--- a/js/src/jsdbg.h
+++ b/js/src/jsdbg.h
@@ -137,30 +137,45 @@ class Debug {
     // Precondition: *vp is a value from a debuggee compartment and cx is in
     // the debugger's compartment.
     //
     // Wrap *vp for the debugger compartment, wrap it in a Debug.Object if it's
     // an object, store the result in *vp, and return true.
     //
     bool wrapDebuggeeValue(JSContext *cx, Value *vp);
 
-    // Inverse of wrapDebuggeeValue.
+    // NOT the inverse of wrapDebuggeeValue.
     //
-    // Precondition: cx is in a debuggee compartment.
+    // Precondition: cx is in the debugger compartment. *vp is a value in that
+    // compartment. (*vp is a "debuggee value", meaning it is the debugger's
+    // reflection of a value in the debuggee.)
     //
-    // If *vp is a Debug.Object, store the referent in *vp, appropriately
-    // rewrapped for the debuggee's compartment, regardless of what compartment
-    // the actual referent inhabits. Otherwise, if *vp is an object, throw a
-    // TypeError, because it is not a debuggee value. Otherwise *vp is a
-    // primitive, so copy it to the debuggee's compartment.
+    // If *vp is a Debug.Object, store the referent in *vp. Otherwise, if *vp
+    // is an object, throw a TypeError, because it is not a debuggee
+    // value. Otherwise *vp is a primitive, so leave it alone.
+    //
+    // The value is not rewrapped for any debuggee compartment.
     //
     bool unwrapDebuggeeValue(JSContext *cx, Value *vp);
 
     // Store the Debug.Frame object for the frame fp in *vp.
     bool getScriptFrame(JSContext *cx, StackFrame *fp, Value *vp);
+
+    // Precondition: we are in the debuggee compartment (ac is entered) and ok
+    // is true if the operation in the debuggee compartment succeeded, false on
+    // error or exception.
+    //
+    // Postcondition: we are in the debugger compartment (ac is not entered)
+    // whether creating the new completion value succeeded or not.
+    //
+    // On success, a completion value is in vp and ac.context does not have a
+    // pending exception. (This ordinarily returns true even if the ok argument
+    // is false.)
+    //
+    bool newCompletionValue(AutoCompartment &ac, bool ok, Value val, Value *vp);
 };
 
 bool
 Debug::hasAnyLiveHooks() const
 {
     return observesDebuggerStatement();
 }