From 20c7510a95d659cd865c3d5a7980897a6ac3a460 Mon Sep 17 00:00:00 2001 From: Jason Orendorff Date: Wed, 25 May 2011 15:21:53 -0500 Subject: [PATCH] Add Debug.Frame.prototype.evalWithBindings. --- .../tests/debug/Frame-evalWithBindings-01.js | 38 ++++++ .../tests/debug/Frame-evalWithBindings-02.js | 22 ++++ .../tests/debug/Frame-evalWithBindings-03.js | 20 ++++ .../tests/debug/Frame-evalWithBindings-04.js | 21 ++++ .../tests/debug/Frame-evalWithBindings-05.js | 15 +++ .../tests/debug/Frame-evalWithBindings-06.js | 13 ++ .../tests/debug/Frame-evalWithBindings-07.js | 20 ++++ .../tests/debug/Frame-evalWithBindings-08.js | 17 +++ .../tests/debug/Frame-evalWithBindings-09.js | 31 +++++ .../tests/debug/Frame-evalWithBindings-10.js | 19 +++ js/src/jsdbg.cpp | 113 ++++++++++++++++-- js/src/jsdbg.h | 4 + js/src/jsdbgapi.cpp | 20 +--- js/src/jsinterp.cpp | 3 +- js/src/vm/Stack-inl.h | 11 +- js/src/vm/Stack.h | 2 +- 16 files changed, 336 insertions(+), 33 deletions(-) create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js create mode 100644 js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js new file mode 100644 index 00000000000..ebaf01fc08e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-01.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js new file mode 100644 index 00000000000..bc23b258ab3 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-02.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js new file mode 100644 index 00000000000..e7f670addff --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-03.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js new file mode 100644 index 00000000000..270b67258c0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-04.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js new file mode 100644 index 00000000000..88fd99263a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-05.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js new file mode 100644 index 00000000000..d6c8a7d1745 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-06.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js new file mode 100644 index 00000000000..c2508ebece4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-07.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js new file mode 100644 index 00000000000..9168bb31932 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-08.js @@ -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); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js new file mode 100644 index 00000000000..d98574f5c5b --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-09.js @@ -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; })();"); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js new file mode 100644 index 00000000000..9dd30432ae5 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js @@ -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); diff --git a/js/src/jsdbg.cpp b/js/src/jsdbg.cpp index 42000ea1925..7118b69d5de 100644 --- a/js/src/jsdbg.cpp +++ b/js/src/jsdbg.cpp @@ -42,6 +42,7 @@ #include "jsdbg.h" #include "jsapi.h" #include "jscntxt.h" +#include "jsemit.h" #include "jsgcmark.h" #include "jsobj.h" #include "jswrapper.h" @@ -95,7 +96,7 @@ ReportMoreArgsNeeded(JSContext *cx, const char *name, uintN required) #define REQUIRE_ARGC(name, n) \ JS_BEGIN_MACRO \ - if (argc < n) \ + if (argc < (n)) \ return ReportMoreArgsNeeded(cx, name, n); \ JS_END_MACRO @@ -1095,7 +1096,8 @@ CheckThisFrame(JSContext *cx, Value *vp, const char *fnname, bool checkLive) JSObject *thisobj = CheckThisFrame(cx, vp, fnname, true); \ if (!thisobj) \ return false; \ - StackFrame *fp = (StackFrame *) thisobj->getPrivate() + StackFrame *fp = (StackFrame *) thisobj->getPrivate(); \ + JS_ASSERT((cx)->stack.contains(fp)) static JSBool DebugFrame_getType(JSContext *cx, uintN argc, Value *vp) @@ -1268,13 +1270,46 @@ DebugFrame_getLive(JSContext *cx, uintN argc, Value *vp) return true; } -static JSBool -DebugFrame_eval(JSContext *cx, uintN argc, Value *vp) +namespace js { + +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); - THIS_FRAME(cx, vp, "eval", thisobj, fp); + assertSameCompartment(cx, scobj, 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()); + // Check the first argument, the eval code string. if (!vp[2].isString()) { JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_EXPECTED_TYPE, "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); if (!linearStr) return false; - JS::Anchor 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()); if (!ac.enter()) 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(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; - bool ok = JS_EvaluateUCInStackFrame(cx, Jsvalify(fp), linearStr->chars(), linearStr->length(), - "debugger eval code", 1, Jsvalify(&rval)); + JS::Anchor anchor(linearStr); + bool ok = EvaluateInScope(cx, scobj, fp, linearStr->chars(), linearStr->length(), + "debugger eval code", 1, &rval); 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 DebugFrame_construct(JSContext *cx, uintN argc, Value *vp) { @@ -1315,6 +1409,7 @@ static JSPropertySpec DebugFrame_properties[] = { static JSFunctionSpec DebugFrame_methods[] = { JS_FN("eval", DebugFrame_eval, 1, 0), + JS_FN("evalWithBindings", DebugFrame_evalWithBindings, 1, 0), JS_FS_END }; diff --git a/js/src/jsdbg.h b/js/src/jsdbg.h index f4a2821d86c..85ad2d73a9a 100644 --- a/js/src/jsdbg.h +++ b/js/src/jsdbg.h @@ -270,6 +270,10 @@ Debug::onThrow(JSContext *cx, js::Value *vp) 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__ */ diff --git a/js/src/jsdbgapi.cpp b/js/src/jsdbgapi.cpp index 4c9b7ff9de5..e8075790c0d 100644 --- a/js/src/jsdbgapi.cpp +++ b/js/src/jsdbgapi.cpp @@ -1683,26 +1683,8 @@ JS_EvaluateUCInStackFrame(JSContext *cx, JSStackFrame *fpArg, if (!ac.enter()) 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); - 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; - - uintN evalFlags = StackFrame::DEBUGGER | StackFrame::EVAL; - bool ok = Execute(cx, *scobj, script, fp, evalFlags, Valueify(rval)); - - js_DestroyScript(cx, script); - return ok; + return EvaluateInScope(cx, scobj, fp, chars, length, filename, lineno, Valueify(rval)); } JS_PUBLIC_API(JSBool) diff --git a/js/src/jsinterp.cpp b/js/src/jsinterp.cpp index e06ca13f2c8..6e372753b4e 100644 --- a/js/src/jsinterp.cpp +++ b/js/src/jsinterp.cpp @@ -904,8 +904,7 @@ Execute(JSContext *cx, JSObject &chain, JSScript *script, StackFrame *prev, uint /* Initialize frame and locals. */ JSObject *initialVarObj; if (prev) { - JS_ASSERT(chain == prev->scopeChain()); - frame.fp()->initEvalFrame(cx, script, prev, flags); + frame.fp()->initEvalFrame(cx, script, prev, &chain, flags); /* NB: prev may not be in cx->currentSegment. */ initialVarObj = (prev == cx->maybefp()) diff --git a/js/src/vm/Stack-inl.h b/js/src/vm/Stack-inl.h index e28efcb7184..07338e27353 100644 --- a/js/src/vm/Stack-inl.h +++ b/js/src/vm/Stack-inl.h @@ -401,12 +401,19 @@ StackFrame::initCallFrameLatePrologue() } 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 | DEBUGGER)) == 0); 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). */ Value *dstvp = (Value *)this - 2; Value *srcvp = prev->hasArgs() @@ -427,7 +434,7 @@ StackFrame::initEvalFrame(JSContext *cx, JSScript *script, StackFrame *prev, uin exec.script = script; } - scopeChain_ = &prev->scopeChain(); + scopeChain_ = chain; prev_ = prev; prevpc_ = prev->pc(cx); JS_ASSERT(!hasImacropc()); diff --git a/js/src/vm/Stack.h b/js/src/vm/Stack.h index cb08b78a42f..1966d1ca8a5 100644 --- a/js/src/vm/Stack.h +++ b/js/src/vm/Stack.h @@ -308,7 +308,7 @@ class StackFrame inline void initCallFrameLatePrologue(); /* 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); inline void initGlobalFrame(JSScript *script, JSObject &chain, StackFrame *prev, uint32 flags);