Add Debug.Function.prototype.apply. Also, correctly handle bad resumption values by creating a TypeError and passing it to the uncaughtExceptionHook.
Add Debug.Function.prototype.apply. Also, correctly handle bad resumption values by creating a TypeError and passing it to the uncaughtExceptionHook.
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();
}