Add Debug.Frame.prototype.evalWithBindings.

This commit is contained in:
Jason Orendorff 2011-05-25 15:21:53 -05:00
parent 0305e45450
commit 20c7510a95
16 changed files with 336 additions and 33 deletions

View File

@ -0,0 +1,38 @@
// |jit-test| debug
// evalWithBindings basics
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.evalWithBindings("x", {x: 2}).return, 2);
assertEq(frame.evalWithBindings("x + y", {x: 2}).return, 5);
hits++;
}
};
// in global code
g.y = 3;
g.eval("debugger;");
// in function code
g.y = "fail";
g.eval("function f(y) { debugger; }");
g.f(3);
// in direct eval code
g.eval("function f() { var y = 3; eval('debugger;'); }");
g.f();
// in strict eval code with var
g.eval("function f() { 'use strict'; eval('var y = 3; debugger;'); }");
g.f();
// in a with block
g.eval("with ({y: 3}) { debugger; }");
// shadowing
g.eval("let (x = 50, y = 3) { debugger; }");
assertEq(hits, 6);

View File

@ -0,0 +1,22 @@
// |jit-test| debug
// evalWithBindings to call a method of a debuggee object
var g = newGlobal('new-compartment');
var dbg = new Debug;
var global = dbg.addDebuggee(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
var obj = frame.arguments[0];
var expected = frame.arguments[1];
assertEq(frame.evalWithBindings("obj.toString()", {obj: obj}).return, expected);
hits++;
}
};
g.eval("function f(obj, expected) { debugger; }");
g.eval("f(new Number(-0), '0');");
g.eval("f(new String('ok'), 'ok');");
g.eval("f({toString: function () { return f; }}, f);");
assertEq(hits, 3);

View File

@ -0,0 +1,20 @@
// |jit-test| debug
// arguments works in evalWithBindings (it does not interpose a function scope)
var g = newGlobal('new-compartment');
var dbg = new Debug;
var global = dbg.addDebuggee(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
var argc = frame.arguments.length;
assertEq(argc, 7);
assertEq(frame.evalWithBindings("arguments[prop]", {prop: "length"}).return, argc);
for (var i = 0; i < argc; i++)
assertEq(frame.evalWithBindings("arguments[i]", {i: i}).return, frame.arguments[i]);
hits++;
}
};
g.eval("function f() { debugger; }");
g.eval("f(undefined, -0, NaN, '\uffff', Array.prototype, Math, f);");
assertEq(hits, 1);

View File

@ -0,0 +1,21 @@
// |jit-test| debug
// evalWithBindings works on non-top frames.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var f1;
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.older.evalWithBindings("q + r", {r: 3}).return, 5);
// frame.older.older is in the same function as frame, but a different activation of it
assertEq(frame.older.older.evalWithBindings("q + r", {r: 3}).return, 6);
hits++;
}
};
g.eval("function f1(q) { if (q == 1) debugger; else f2(2); }");
g.eval("function f2(arg) { var q = arg; f1(1); }");
g.f1(3);
assertEq(hits, 1);

View File

@ -0,0 +1,15 @@
// |jit-test| debug
// evalWithBindings code can assign to the bindings.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.evalWithBindings("for (i = 0; i < 5; i++) {} i;", {i: 10}).return, 5);
hits++;
}
};
g.eval("debugger;");
assertEq("i" in g, false);
assertEq(hits, 1);

View File

@ -0,0 +1,13 @@
// |jit-test| debug
// In evalWithBindings code, assignment to any name not in the bindings works just as in eval.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.evalWithBindings("y = z; x = w;", {z: 2, w: 3}).return, 3);
}
};
g.eval("function f(x) { debugger; assertEq(x, 3); }");
g.eval("var y = 0; f(0);");
assertEq(g.y, 2);

View File

@ -0,0 +1,20 @@
// |jit-test| debug
// var statements in strict evalWithBindings code behave like strict eval.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.evalWithBindings("var i = a*a + b*b; i === 25;", {a: 3, b: 4}).return, true);
hits++;
}
};
g.eval("'use strict'; debugger;");
assertEq(hits, 1);
assertEq("i" in g, false);
g.eval("function die() { throw fit; }");
g.eval("Object.defineProperty(this, 'i', {get: die, set: die});");
g.eval("'use strict'; debugger;");
assertEq(hits, 2);

View File

@ -0,0 +1,17 @@
// |jit-test| debug
// evalWithBindings ignores non-enumerable and non-own properties.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
assertEq(frame.evalWithBindings("toString + constructor + length", []).return, 112233);
var obj = Object.create({constructor: "FAIL"}, {length: {value: "fail"}});
assertEq(frame.evalWithBindings("toString + constructor + length", obj).return, 112233);
hits++;
}
};
g.eval("function f() { var toString = 111111, constructor = 1111, length = 11; debugger; }");
g.f();
assertEq(hits, 1);

View File

@ -0,0 +1,31 @@
// |jit-test| debug
// evalWithBindings code is debuggee code, so it can trip the debugger. It nests!
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var f1;
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
f1 = frame;
// This trips the throw hook.
var x = frame.evalWithBindings("wrongSpeling", {rightSpelling: 2}).throw;
assertEq(frame.evalWithBindings("exc.name", {exc: x}).return, "ReferenceError");
hits++;
},
throw: function (frame, exc) {
assertEq(frame !== f1, true);
// f1's environment does not contain the binding for the first evalWithBindings call.
assertEq(f1.eval("rightSpelling").return, "dependent");
assertEq(f1.evalWithBindings("n + rightSpelling", {n: "in"}).return, "independent");
// frame's environment does contain the binding.
assertEq(frame.eval("rightSpelling").return, 2);
assertEq(frame.evalWithBindings("rightSpelling + three", {three: 3}).return, 5);
hits++;
}
};
g.eval("(function () { var rightSpelling = 'dependent'; debugger; })();");

View File

@ -0,0 +1,19 @@
// |jit-test| debug
// Direct eval code under evalWithbindings sees both the bindings and the enclosing scope.
var g = newGlobal('new-compartment');
var dbg = new Debug(g);
var hits = 0;
dbg.hooks = {
debuggerHandler: function (frame) {
var code =
"assertEq(a, 1234);\n" +
"assertEq(b, null);\n" +
"assertEq(c, 'ok');\n";
assertEq(frame.evalWithBindings("eval(s)", {s: code, a: 1234}).return, undefined);
hits++;
}
};
g.eval("function f(b) { var c = 'ok'; debugger; }");
g.f(null);
assertEq(hits, 1);

View File

@ -42,6 +42,7 @@
#include "jsdbg.h" #include "jsdbg.h"
#include "jsapi.h" #include "jsapi.h"
#include "jscntxt.h" #include "jscntxt.h"
#include "jsemit.h"
#include "jsgcmark.h" #include "jsgcmark.h"
#include "jsobj.h" #include "jsobj.h"
#include "jswrapper.h" #include "jswrapper.h"
@ -95,7 +96,7 @@ ReportMoreArgsNeeded(JSContext *cx, const char *name, uintN required)
#define REQUIRE_ARGC(name, n) \ #define REQUIRE_ARGC(name, n) \
JS_BEGIN_MACRO \ JS_BEGIN_MACRO \
if (argc < n) \ if (argc < (n)) \
return ReportMoreArgsNeeded(cx, name, n); \ return ReportMoreArgsNeeded(cx, name, n); \
JS_END_MACRO JS_END_MACRO
@ -1095,7 +1096,8 @@ CheckThisFrame(JSContext *cx, Value *vp, const char *fnname, bool checkLive)
JSObject *thisobj = CheckThisFrame(cx, vp, fnname, true); \ JSObject *thisobj = CheckThisFrame(cx, vp, fnname, true); \
if (!thisobj) \ if (!thisobj) \
return false; \ return false; \
StackFrame *fp = (StackFrame *) thisobj->getPrivate() StackFrame *fp = (StackFrame *) thisobj->getPrivate(); \
JS_ASSERT((cx)->stack.contains(fp))
static JSBool static JSBool
DebugFrame_getType(JSContext *cx, uintN argc, Value *vp) DebugFrame_getType(JSContext *cx, uintN argc, Value *vp)
@ -1268,13 +1270,46 @@ DebugFrame_getLive(JSContext *cx, uintN argc, Value *vp)
return true; return true;
} }
static JSBool namespace js {
DebugFrame_eval(JSContext *cx, uintN argc, Value *vp)
JSBool
EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *chars,
uintN length, const char *filename, uintN lineno, Value *rval)
{ {
REQUIRE_ARGC("Debug.Frame.eval", 1); assertSameCompartment(cx, scobj, fp);
THIS_FRAME(cx, vp, "eval", thisobj, fp);
/*
* NB: This function breaks the assumption that the compiler can see all
* calls and properly compute a static level. In order to get around this,
* we use a static level that will cause us not to attempt to optimize
* variable references made by this frame.
*/
JSScript *script = Compiler::compileScript(cx, scobj, fp, fp->scopeChain().principals(cx),
TCF_COMPILE_N_GO, chars, length,
filename, lineno, cx->findVersion(),
NULL, UpvarCookie::UPVAR_LEVEL_LIMIT);
if (!script)
return false;
bool ok = Execute(cx, *scobj, script, fp, StackFrame::DEBUGGER | StackFrame::EVAL, rval);
js_DestroyScript(cx, script);
return ok;
}
}
enum EvalBindingsMode { WithoutBindings, WithBindings };
static JSBool
DebugFrameEval(JSContext *cx, uintN argc, Value *vp, EvalBindingsMode mode)
{
REQUIRE_ARGC(mode == WithBindings ? "Debug.Frame.evalWithBindings" : "Debug.Frame.eval",
mode == WithBindings ? 2 : 1);
THIS_FRAME(cx, vp, mode == WithBindings ? "evalWithBindings" : "eval", thisobj, fp);
Debug *dbg = Debug::fromChildJSObject(&vp[1].toObject()); Debug *dbg = Debug::fromChildJSObject(&vp[1].toObject());
// Check the first argument, the eval code string.
if (!vp[2].isString()) { if (!vp[2].isString()) {
JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_EXPECTED_TYPE, JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_EXPECTED_TYPE,
"Debug.Frame.eval", "string", InformalValueTypeName(vp[2])); "Debug.Frame.eval", "string", InformalValueTypeName(vp[2]));
@ -1283,17 +1318,76 @@ DebugFrame_eval(JSContext *cx, uintN argc, Value *vp)
JSLinearString *linearStr = vp[2].toString()->ensureLinear(cx); JSLinearString *linearStr = vp[2].toString()->ensureLinear(cx);
if (!linearStr) if (!linearStr)
return false; return false;
JS::Anchor<JSString *> anchor(linearStr);
// Gather keys and values of bindings, if any. This must be done in the
// debugger compartment, since that is where any exceptions must be
// thrown.
AutoIdVector keys(cx);
AutoValueVector values(cx);
if (mode == WithBindings) {
JSObject *bindingsobj = NonNullObject(cx, vp[3]);
if (!bindingsobj ||
!GetPropertyNames(cx, bindingsobj, JSITER_OWNONLY, &keys) ||
!values.growBy(keys.length()))
{
return false;
}
for (size_t i = 0; i < keys.length(); i++) {
Value *vp = &values[i];
if (!bindingsobj->getProperty(cx, bindingsobj, keys[i], vp) ||
!dbg->unwrapDebuggeeValue(cx, vp))
{
return false;
}
}
}
AutoCompartment ac(cx, &fp->scopeChain()); AutoCompartment ac(cx, &fp->scopeChain());
if (!ac.enter()) if (!ac.enter())
return false; return false;
// Get a scope object.
if (fp->isNonEvalFunctionFrame() && !fp->hasCallObj() && !CreateFunCallObject(cx, fp))
return false;
JSObject *scobj = GetScopeChain(cx, fp);
if (!scobj)
return false;
// If evalWithBindings, create the inner scope object.
if (mode == WithBindings) {
// TODO - Should probably create a With object here.
scobj = NewNonFunction<WithProto::Given>(cx, &js_ObjectClass, NULL, scobj);
if (!scobj)
return false;
for (size_t i = 0; i < keys.length(); i++) {
if (!cx->compartment->wrap(cx, &values[i]) ||
!DefineNativeProperty(cx, scobj, keys[i], values[i], NULL, NULL, 0, 0, 0))
{
return false;
}
}
}
// Run the code and produce the completion value.
Value rval; Value rval;
bool ok = JS_EvaluateUCInStackFrame(cx, Jsvalify(fp), linearStr->chars(), linearStr->length(), JS::Anchor<JSString *> anchor(linearStr);
"debugger eval code", 1, Jsvalify(&rval)); bool ok = EvaluateInScope(cx, scobj, fp, linearStr->chars(), linearStr->length(),
"debugger eval code", 1, &rval);
return dbg->newCompletionValue(ac, ok, rval, vp); return dbg->newCompletionValue(ac, ok, rval, vp);
} }
static JSBool
DebugFrame_eval(JSContext *cx, uintN argc, Value *vp)
{
return DebugFrameEval(cx, argc, vp, WithoutBindings);
}
static JSBool
DebugFrame_evalWithBindings(JSContext *cx, uintN argc, Value *vp)
{
return DebugFrameEval(cx, argc, vp, WithBindings);
}
static JSBool static JSBool
DebugFrame_construct(JSContext *cx, uintN argc, Value *vp) DebugFrame_construct(JSContext *cx, uintN argc, Value *vp)
{ {
@ -1315,6 +1409,7 @@ static JSPropertySpec DebugFrame_properties[] = {
static JSFunctionSpec DebugFrame_methods[] = { static JSFunctionSpec DebugFrame_methods[] = {
JS_FN("eval", DebugFrame_eval, 1, 0), JS_FN("eval", DebugFrame_eval, 1, 0),
JS_FN("evalWithBindings", DebugFrame_evalWithBindings, 1, 0),
JS_FS_END JS_FS_END
}; };

View File

@ -270,6 +270,10 @@ Debug::onThrow(JSContext *cx, js::Value *vp)
DebugHandleMethod(&Debug::handleThrow)); DebugHandleMethod(&Debug::handleThrow));
} }
extern JSBool
EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *chars,
uintN length, const char *filename, uintN lineno, Value *rval);
} }
#endif /* jsdbg_h__ */ #endif /* jsdbg_h__ */

View File

@ -1683,26 +1683,8 @@ JS_EvaluateUCInStackFrame(JSContext *cx, JSStackFrame *fpArg,
if (!ac.enter()) if (!ac.enter())
return false; return false;
/*
* NB: This function breaks the assumption that the compiler can see all
* calls and properly compute a static level. In order to get around this,
* we use a static level that will cause us not to attempt to optimize
* variable references made by this frame.
*/
StackFrame *fp = Valueify(fpArg); StackFrame *fp = Valueify(fpArg);
JSScript *script = Compiler::compileScript(cx, scobj, fp, fp->scopeChain().principals(cx), return EvaluateInScope(cx, scobj, fp, chars, length, filename, lineno, Valueify(rval));
TCF_COMPILE_N_GO, chars, length,
filename, lineno, cx->findVersion(),
NULL, UpvarCookie::UPVAR_LEVEL_LIMIT);
if (!script)
return false;
uintN evalFlags = StackFrame::DEBUGGER | StackFrame::EVAL;
bool ok = Execute(cx, *scobj, script, fp, evalFlags, Valueify(rval));
js_DestroyScript(cx, script);
return ok;
} }
JS_PUBLIC_API(JSBool) JS_PUBLIC_API(JSBool)

View File

@ -904,8 +904,7 @@ Execute(JSContext *cx, JSObject &chain, JSScript *script, StackFrame *prev, uint
/* Initialize frame and locals. */ /* Initialize frame and locals. */
JSObject *initialVarObj; JSObject *initialVarObj;
if (prev) { if (prev) {
JS_ASSERT(chain == prev->scopeChain()); frame.fp()->initEvalFrame(cx, script, prev, &chain, flags);
frame.fp()->initEvalFrame(cx, script, prev, flags);
/* NB: prev may not be in cx->currentSegment. */ /* NB: prev may not be in cx->currentSegment. */
initialVarObj = (prev == cx->maybefp()) initialVarObj = (prev == cx->maybefp())

View File

@ -401,12 +401,19 @@ StackFrame::initCallFrameLatePrologue()
} }
inline void inline void
StackFrame::initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, uint32 flagsArg) StackFrame::initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, JSObject *chain, uint32 flagsArg)
{ {
JS_ASSERT(flagsArg & EVAL); JS_ASSERT(flagsArg & EVAL);
JS_ASSERT((flagsArg & ~(EVAL | DEBUGGER)) == 0); JS_ASSERT((flagsArg & ~(EVAL | DEBUGGER)) == 0);
JS_ASSERT(prev->isScriptFrame()); JS_ASSERT(prev->isScriptFrame());
/*
* eval code always runs in prev's scope, except when executed via
* DebugFrame_evalWithBindings. Strict eval is another special case, dealt
* with specially in js::Execute after this method returns.
*/
JS_ASSERT_IF(!(flagsArg & DEBUGGER), chain == &prev->scopeChain());
/* Copy (callee, thisv). */ /* Copy (callee, thisv). */
Value *dstvp = (Value *)this - 2; Value *dstvp = (Value *)this - 2;
Value *srcvp = prev->hasArgs() Value *srcvp = prev->hasArgs()
@ -427,7 +434,7 @@ StackFrame::initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, uin
exec.script = script; exec.script = script;
} }
scopeChain_ = &prev->scopeChain(); scopeChain_ = chain;
prev_ = prev; prev_ = prev;
prevpc_ = prev->pc(cx); prevpc_ = prev->pc(cx);
JS_ASSERT(!hasImacropc()); JS_ASSERT(!hasImacropc());

View File

@ -308,7 +308,7 @@ class StackFrame
inline void initCallFrameLatePrologue(); inline void initCallFrameLatePrologue();
/* Used for eval. */ /* Used for eval. */
inline void initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, inline void initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, JSObject *chain,
uint32 flags); uint32 flags);
inline void initGlobalFrame(JSScript *script, JSObject &chain, StackFrame *prev, inline void initGlobalFrame(JSScript *script, JSObject &chain, StackFrame *prev,
uint32 flags); uint32 flags);