diff --git a/js/src/jit-test/tests/debug/Environment-01.js b/js/src/jit-test/tests/debug/Environment-01.js new file mode 100644 index 00000000000..08f18aea7d2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-01.js @@ -0,0 +1,23 @@ +// A live Environment can observe the new variables introduced by ES5 non-strict direct eval. + +var g = newGlobal('new-compartment'); +g.eval("var x = 'global'; function f(s) { h(); eval(s); h(); }"); +g.eval("function h() { debugger; }"); +var dbg = Debugger(g); +var env = undefined; +var hits = 0; +dbg.onDebuggerStatement = function (hframe) { + if (env === undefined) { + // First debugger statement. + env = hframe.older.environment; + assertEq(env.find("x") !== env, true); + assertEq(env.names().indexOf("x"), -1); + } else { + // Second debugger statement, post-eval. + assertEq(env.find("x"), env); + assertEq(env.names().indexOf("x") >= 0, true); + } + hits++; +}; +g.f("var x = 'local';"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-02.js b/js/src/jit-test/tests/debug/Environment-02.js new file mode 100644 index 00000000000..d05c56f5940 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-02.js @@ -0,0 +1,20 @@ +// The last Environment on the environment chain always has .type == "object" and .object === the global object. + +var g = newGlobal('new-compartment'); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.eval("function h() { debugger; }"); +var hits = 0; +dbg.onDebuggerStatement = function (hframe) { + var env = hframe.older.environment; + while (env.parent) + env = env.parent; + assertEq(env.type, "object"); + assertEq(env.object, gw); + hits++; +}; + +g.eval("h();"); +g.eval("(function () { h(); return []; })();"); +g.eval("with (Math) { h(-2 * PI); }"); +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Environment-find-01.js b/js/src/jit-test/tests/debug/Environment-find-01.js new file mode 100644 index 00000000000..684ffe9e5bb --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-01.js @@ -0,0 +1,19 @@ +// find sees that vars are hoisted out of with statements. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.environment.find("x").type, "object"); + hits++; +}; + +assertEq(g.eval("(function () {\n" + + " function g() { x = 1; }\n" + + " with ({x: 2}) {\n" + + " var x;\n" + + " debugger;\n" + + " return x;\n" + + " }\n" + + "})();"), 2); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-find-02.js b/js/src/jit-test/tests/debug/Environment-find-02.js new file mode 100644 index 00000000000..ef4e058d6a0 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-02.js @@ -0,0 +1,18 @@ +// env.find() finds nonenumerable names in the global environment. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + var last = env; + while (last.parent) + last = last.parent; + + assertEq(env.find("Array"), last); + hits++; +}; + +g.eval("h();"); +g.eval("(function () { let (x = 1, y = 2) h(); })();"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-find-03.js b/js/src/jit-test/tests/debug/Environment-find-03.js new file mode 100644 index 00000000000..04bfb616749 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-03.js @@ -0,0 +1,20 @@ +// env.find() finds noneumerable properties in with statements. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var frame = dbg.getNewestFrame(); + var target = frame.eval("obj").return; + var env = frame.environment.find("PI"); + assertEq(env.object, target); + hits++; +}; + +g.obj = g.Math; +g.eval("with (obj) h();"); +g.eval("with (Math) { let x = 12; h(); }"); +g.eval("obj = {};\n" + + "Object.defineProperty(obj, 'PI', {enumerable: false, value: 'Marlowe'});\n" + + "with (obj) h();\n"); +assertEq(hits, 3); diff --git a/js/src/jit-test/tests/debug/Environment-find-04.js b/js/src/jit-test/tests/debug/Environment-find-04.js new file mode 100644 index 00000000000..39f986c818f --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-04.js @@ -0,0 +1,21 @@ +// env.find throws a TypeError if the argument is not an identifier. + +load(libdir + "asserts.js"); + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + assertThrowsInstanceOf(function () { env.find(); }, TypeError); + assertThrowsInstanceOf(function () { env.find(""); }, TypeError); + assertThrowsInstanceOf(function () { env.find(" "); }, TypeError); + assertThrowsInstanceOf(function () { env.find(0); }, TypeError); + assertThrowsInstanceOf(function () { env.find("0"); }, TypeError); + assertThrowsInstanceOf(function () { env.find("0xc"); }, TypeError); + assertThrowsInstanceOf(function () { env.find("Anna Karenina"); }, TypeError); + hits++; +}; +g.eval("h();"); +g.eval("with ([1]) h();"); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-find-05.js b/js/src/jit-test/tests/debug/Environment-find-05.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/js/src/jit-test/tests/debug/Environment-find-06.js b/js/src/jit-test/tests/debug/Environment-find-06.js new file mode 100644 index 00000000000..f49dc31cf2e --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-find-06.js @@ -0,0 +1,49 @@ +// Environment.prototype.find finds bindings that are function arguments, 'let' +// bindings, or FunctionExpression names. + +var g = newGlobal('new-compartment'); +g.eval("function h() { debugger; }"); + +var dbg = new Debugger(g); + +function test1(code) { + var hits = 0; + dbg.onDebuggerStatement = function (frame) { + var env = frame.older.environment.find('X'); + assertEq(env.names().indexOf('X') !== -1, true); + assertEq(env.type, 'declarative'); + assertEq(env.parent !== null, true); + hits++; + }; + g.eval(code); + assertEq(hits, 1); +} + +var manyNames = ''; +for (var i = 0; i < 4096; i++) + manyNames += 'x' + i + ', '; +manyNames += 'X'; + +function test2(code) { + print(code + " : one"); + test1(code.replace('@@', 'X')); + print(code + " : many"); + test1(code.replace('@@', manyNames)); +} + +test2('function f(@@) { h(); } f(1);'); +test2('function f(@@) { h(); } f();'); +test2('function f(@@) { return function g() { h(X); }; } f(1)();'); +test2('function f(@@) { return function g() { h(X); }; } f()();'); + +test2(' { let @@ = 0; h(); }'); +test2('function f(a, b, c) { let @@ = 0; h(); } f(1, 2, 3);'); +test2(' { let @@ = 0; { let y = 0; h(); } }'); +test2('function f() { let @@ = 0; { let y = 0; h(); } } f();'); +test2(' { for (let @@ = 0; X < 1; X++) h(); }'); +test2('function f() { for (let @@ = 0; X < 1; X++) h(); } f();'); +test2(' { (let (@@ = 0) let (y = 2, z = 3) h()); }'); +test2('function f() { return (let (@@ = 0) let (y = 2, z = 3) h()); } f();'); + +test1('(function X() { h(); })();'); +test1('(function X(a, b, c) { h(); })(1, 2, 3);'); diff --git a/js/src/jit-test/tests/debug/Environment-gc-01.js b/js/src/jit-test/tests/debug/Environment-gc-01.js new file mode 100644 index 00000000000..b9318f646c4 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-gc-01.js @@ -0,0 +1,19 @@ +// An Environment keeps its referent alive. + +var g = newGlobal('new-compartment'); +g.eval("function f(x) { return 2 * x; }"); +var dbg = Debugger(g); +var env; +dbg.onEnterFrame = function (frame) { env = frame.environment; }; +assertEq(g.f(22), 44); +dbg.onEnterFrame = undefined; + +assertEq(env.find("x"), env); +assertEq(env.names().join(","), "x"); + +gc(); +g.gc(g); +gc(env); + +assertEq(env.find("x"), env); +assertEq(env.names().join(","), "x"); diff --git a/js/src/jit-test/tests/debug/Environment-gc-02.js b/js/src/jit-test/tests/debug/Environment-gc-02.js new file mode 100644 index 00000000000..098a04c1d87 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-gc-02.js @@ -0,0 +1,28 @@ +// A closure's .environment keeps the lexical environment alive even if the closure is destroyed. + +var N = 4; +var g = newGlobal('new-compartment'); +g.eval("function add(a) { return function (b) { return eval('a + b'); }; }"); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +var aw = gw.getOwnPropertyDescriptor("add").value; + +// Create N closures and collect environments. +var arr = []; +for (var i = 0; i < N; i++) + arr[i] = aw.call(null, i).return.environment; + +// Test that they work now. +function check() { + for (var i = 0; i < N; i++) { + assertEq(arr[i].find("b"), null); + assertEq(arr[i].find("a"), arr[i]); + } +} +check(); + +// Test that they work after gc. +gc(); +gc({}); +g.gc(g); +check(); diff --git a/js/src/jit-test/tests/debug/Environment-identity-01.js b/js/src/jit-test/tests/debug/Environment-identity-01.js new file mode 100644 index 00000000000..f7e76d1d87e --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-01.js @@ -0,0 +1,40 @@ +// The value of frame.environment is the same Environment object at different +// times within a single visit to a scope. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +g.eval("function h() { debugger; }"); +var hits, env; +dbg.onDebuggerStatement = function (hframe) { + var frame = hframe.older; + var e = frame.environment; + + // frame.environment is at least cached from one moment to the next. + assertEq(e, frame.environment); + + // frame.environment is cached from statement to statement within a call frame. + if (env === undefined) + env = e; + else + assertEq(e, env); + + hits++; +}; + +hits = 0; +env = undefined; +g.eval("function f() { (function () { var i = 0; h(); var j = 2; h(); })(); }"); +g.f(); +assertEq(hits, 2); + +hits = 0; +env = undefined; +g.eval("function f2() { { let i = 0; h(); let j = 2; h(); } }"); +g.f2(); +assertEq(hits, 2); + +hits = 0; +env = undefined; +g.eval("function f3() { { let i; for (i = 0; i < 2; i++) h(); } }"); +g.f3(); +assertEq(hits, 2); diff --git a/js/src/jit-test/tests/debug/Environment-identity-02.js b/js/src/jit-test/tests/debug/Environment-identity-02.js new file mode 100644 index 00000000000..0ab4b7d216c --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-02.js @@ -0,0 +1,29 @@ +// frame.environment is different for different activations of a scope. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +g.eval("function h() { debugger; }"); +var arr; +dbg.onDebuggerStatement = function (hframe) { + var e = hframe.older.environment; + assertEq(arr.indexOf(e), -1); + arr.push(e); +}; + +function test(code, expectedHits) { + arr = []; + g.eval(code); + assertEq(arr.length, expectedHits); +} + +// two separate calls to a function +test("(function () { var f = function (a) { h(); return a; }; f(1); f(2); })();", 2); + +// recursive calls to a function +test("(function f(n) { h(); return n < 2 ? 1 : n * f(n - 1); })(3);", 3); + +// separate visits to a block in the same call frame +test("(function () { for (var i = 0; i < 3; i++) { let j = i * 4; h(); }})();", 3); + +// two strict direct eval calls in the same function scope +test("(function () { 'use strict'; for (var i = 0; i < 3; i++) eval('h();'); })();", 3); diff --git a/js/src/jit-test/tests/debug/Environment-identity-03.js b/js/src/jit-test/tests/debug/Environment-identity-03.js new file mode 100644 index 00000000000..9a020182006 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-03.js @@ -0,0 +1,109 @@ +// Two Environments nested in the same runtime scope share the correct tail of their parent chains. + +// The compiler must be allowed to elide empty scopes and so forth, so this +// test does not check the number of unshared Environments. Instead, each test +// case identifies the expected innermost shared scope by the name of a +// variable in it. + +var g = newGlobal('new-compartment'); +g.eval("function h() { debugger; }"); +var dbg = Debugger(g); +var hits, name, shared, unshared; +dbg.onDebuggerStatement = function (hframe) { + var frame = hframe.older; + + // Find name in frame.environment. + var env, child = null; + for (env = frame.environment; env !== null; env = env.parent) { + if (env.names().indexOf(name) != -1) + break; + child = env; + } + assertEq(env !== null, true, "expected '" + name + "' to be in scope"); + assertEq(env, frame.environment.find(name), + "env.find should find the same frame as the written out search"); + + if (hits === 0) { + // First hit. + shared = env; + unshared = child; + } else { + // Subsequent hit. + assertEq(env, shared, "the environment containing '" + name + "' should be shared"); + assertEq(child === null || unshared === null || unshared !== child, true, + "environments nested within the one containing '" + name + "' should not be shared"); + } + hits++; +}; + +function test(sharedName, expectedHits, code) { + hits = 0; + name = sharedName; + shared = unshared = undefined; + g.eval(code); + assertEq(hits, expectedHits); +} + +// Basic test cases. +// +// (The stray "a = b" assignments in these tests are to inhibit the flat closure +// optimization, which Environments expose. There's nothing really wrong with +// the optimization or with the debugger exposing it, but that's not what we +// want to test here.) + +test("q", 2, "var q = function (a) { h(); }; q(1); q(2);"); +test("a", 2, "q = function (a) { (function (b) { h(); a = b; })(2); h(); }; q(1);"); +test("a", 2, "q = function (a) { h(); return function (b) { h(); a = b; }; }; q(1)(2);"); +test("n", 3, "q = function (n) { for (var i = 0; i < n; i++) { let (j = i) { h(); } } }; q(3);"); + +// Don't crash in E4X filter scopes. +test("x", 2, "q = function () { var x = <>.(function (e) { h(); }(this)); }; q();"); + +// A function with long dynamic and static chains. +var N = 80; + +var code = "function f" + N + "(a" + N + ") {\neval('a0 + a1'); h();\n}\n"; +for (var i = N; --i >= 0;) { + var call = "f" + (i + 1) + "(a" + i + " - 1);\n"; + code = ("function f" + i + "(a" + i + ") {\n" + + code + + call + + "if (a" + i + " === 0) " + call + + "}\n"); +} + +g.eval(code); +test("a0", 2, "f0(0);"); +test("a17", 2, "f0(17);"); +test("a" + (N-2), 2, "f0(" + (N-2) + ");"); +test("a" + (N-1), 2, "f0(" + (N-1) + ");"); + +// A function with a short dynamic chain and a long static chain. +N = 60; + +function DeepStaticShallowDynamic(i, n) { + var code = "function f" + i + "(a" + i + ") {\n"; + if (i >= n) + code += "eval('a1 + a2'); h();\n"; + else + code += "return " + DeepStaticShallowDynamic(i+1, n) + ";\n"; + code += "}"; + return code; +} +g.eval(DeepStaticShallowDynamic(1, N)); + +function range(start, stop) { + for (var i = start; i < stop; i++) + yield i; +} + +function DSSDsplit(s) { + return ("var mid = f1" + ["(" + i + ")" for (i in range(0, s))].join("") + ";\n" + + "mid" + ["(" + i + ")" for (i in range(s, N))].join("") + ";\n" + + "mid" + ["(" + i + ")" for (i in range(s, N))].join("") + ";\n"); +} + +test("a1", 2, DSSDsplit(1)); +test("a17", 2, DSSDsplit(17)); +test("a" + (N-2), 2, DSSDsplit(N-2)); +test("a" + (N-1), 2, DSSDsplit(N-1)); diff --git a/js/src/jit-test/tests/debug/Environment-identity-04.js b/js/src/jit-test/tests/debug/Environment-identity-04.js new file mode 100644 index 00000000000..a9d2b21dcd1 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-identity-04.js @@ -0,0 +1,19 @@ +// Observably different visits to the same with-statement produce distinct Environments. + +var g = newGlobal('new-compartment'); +g.eval("function f(a, obj) { with (obj) return function () { return a; }; }"); +var dbg = Debugger(g); +var hits = 0; +dbg.onDebuggerStatement = function (frame) { + // Even though the two visits to the with-statement have the same target + // object, Math, the environments are observably different. + var f1 = frame.eval("f(1, Math);").return; + var f2 = frame.eval("f(2, Math);").return; + assertEq(f1.environment !== f2.environment, true); + assertEq(f1.object, f2.object); + assertEq(f1.call().return, 1); + assertEq(f2.call().return, 2); + hits++; +}; +g.eval("debugger;"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-names-01.js b/js/src/jit-test/tests/debug/Environment-names-01.js new file mode 100644 index 00000000000..557e4f976ec --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-names-01.js @@ -0,0 +1,17 @@ +// env.names() lists nonenumerable names in with-statement environments. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var env = dbg.getNewestFrame().environment; + var names = env.names(); + assertEq(names.indexOf("a") !== -1, true); + assertEq(names.indexOf("b") !== -1, true); + assertEq(names.indexOf("isPrototypeOf") !== -1, true); + hits++; +}; +g.eval("var obj = {a: 1};\n" + + "Object.defineProperty(obj, 'b', {value: 2});\n" + + "with (obj) h();"); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-names-02.js b/js/src/jit-test/tests/debug/Environment-names-02.js new file mode 100644 index 00000000000..0073cadcf85 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-names-02.js @@ -0,0 +1,15 @@ +// env.names() on object environments ignores property names that are not identifiers. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var names; +g.h = function () { + names = dbg.getNewestFrame().environment.names(); +}; +g.eval("var obj = {a: 1};\n" + + "with ({a: 1, '0xcafe': 2, ' ': 3, '': 4, '0': 5}) h();"); +assertEq(names.indexOf("a") !== -1, true); +assertEq(names.indexOf("0xcafe"), -1); +assertEq(names.indexOf(" "), -1); +assertEq(names.indexOf(""), -1); +assertEq(names.indexOf("0"), -1); diff --git a/js/src/jit-test/tests/debug/Environment-parent-01.js b/js/src/jit-test/tests/debug/Environment-parent-01.js new file mode 100644 index 00000000000..77e0ddb2380 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-parent-01.js @@ -0,0 +1,18 @@ +// The objects on the environment chain are all Debugger.Environment objects. +// The environment chain ends in null. + +var g = newGlobal('new-compartment') +g.eval("function f(a) { return function (b) { return function (c) { h(); return a + b + c; }; }; }"); +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var n = 0; + for (var env = dbg.getNewestFrame().environment; env !== null; env = env.parent) { + n++; + assertEq(env instanceof Debugger.Environment, true); + } + assertEq(n >= 4, true); + hits++; +}; +assertEq(g.f(5)(7)(9), 21); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Environment-type-01.js b/js/src/jit-test/tests/debug/Environment-type-01.js new file mode 100644 index 00000000000..695f6c3e1a2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Environment-type-01.js @@ -0,0 +1,38 @@ +// env.type is 'object' in global environments and with-blocks, and 'declarative' otherwise. + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +function test(code, expected) { + var actual = ''; + g.h = function () { actual += dbg.getNewestFrame().environment.type; } + g.eval(code); + assertEq(actual, expected); +} + +test("h();", 'object'); +test("(function (s) { eval(s); })('var v = h();')", 'declarative'); +test("(function (s) { h(); })();", 'declarative'); +test("{let x = 1, y = 2; h();}", 'declarative'); +test("with({x: 1, y: 2}) h();", 'object'); +test("(function (s) { with ({x: 1, y: 2}) h(); })();", 'object'); +test("let (x = 1) { h(); }", 'declarative'); +test("(let (x = 1) h());", 'declarative'); +test("for (let x = 0; x < 1; x++) h();", 'declarative'); +test("for (let x in h()) ;", 'object'); +test("for (let x in {a:1}) h();", 'declarative'); +test("try { throw new Error; } catch (x) { h(x) }", 'declarative'); +test("'use strict'; eval('var z = 1; h();');", 'declarative'); +test("for (var x in [h(m) for (m in [1])]) ;", 'declarative'); +test("for (var x in (h(m) for (m in [1]))) ;", 'declarative'); + +// Since a generator-expression is effectively a function, the innermost scope +// is a function scope, and thus declarative. Thanks to an odd design decision, +// m is already in scope at the point of the call to h(). The answer here is +// not all that important, but we shouldn't crash. +test("for (var x in (0 for (m in h()))) ;", 'declarative'); + +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval("h(), 2 + 2;").return, 4); +} +test("debugger;", 'object'); +test("(function f() { debugger; })();", 'declarative'); diff --git a/js/src/jit-test/tests/debug/Frame-environment-01.js b/js/src/jit-test/tests/debug/Frame-environment-01.js new file mode 100644 index 00000000000..73b5582dc5e --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-01.js @@ -0,0 +1,13 @@ +// frame.environment is a Debugger.Environment object + +var g = newGlobal('new-compartment') +var dbg = Debugger(g); +g.h = function () { + assertEq(dbg.getNewestFrame().environment instanceof Debugger.Environment, true); +}; + +g.eval("h()"); +g.evaluate("h()"); +g.eval("eval('h()')"); +g.eval("function f() { h(); }"); +g.f(); diff --git a/js/src/jit-test/tests/debug/Frame-environment-02.js b/js/src/jit-test/tests/debug/Frame-environment-02.js new file mode 100644 index 00000000000..c712c919ec2 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-02.js @@ -0,0 +1,12 @@ +// dbg.getNewestFrame().environment works. + +var g = newGlobal('new-compartment'); +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +g.h = function () { + var env = dbg.getNewestFrame().environment; + assertEq(env instanceof Debugger.Environment, true); + assertEq(env.object, gw); + assertEq(env.parent, null); +}; +g.eval("h()"); diff --git a/js/src/jit-test/tests/debug/Frame-environment-03.js b/js/src/jit-test/tests/debug/Frame-environment-03.js new file mode 100644 index 00000000000..c7ed7eaa4ac --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-03.js @@ -0,0 +1,11 @@ +// If !frame.live, frame.environment throws. + +load(libdir + "asserts.js"); + +var g = newGlobal('new-compartment'); +var dbg = Debugger(g); +var frame; +g.h = function () { frame = dbg.getNewestFrame(); }; +g.eval("h();"); +assertEq(frame.live, false); +assertThrowsInstanceOf(function () { frame.environment; }, Error); diff --git a/js/src/jit-test/tests/debug/Frame-environment-04.js b/js/src/jit-test/tests/debug/Frame-environment-04.js new file mode 100644 index 00000000000..b41a7888f53 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-environment-04.js @@ -0,0 +1,12 @@ +// frame.environment can be called from the onEnterFrame hook. + +var g = newGlobal('new-compartment'); +g.eval("function f(x) { return 2 * x; }"); +var dbg = Debugger(g); +var hits = 0; +dbg.onEnterFrame = function (frame) { + assertEq(frame.environment.names().join(","), "x"); + hits++; +}; +assertEq(g.f(22), 44); +assertEq(hits, 1); diff --git a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js index 20b2fc700f5..99e3cf3d48a 100644 --- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js +++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js @@ -1,4 +1,5 @@ -// Direct eval code under evalWithbindings sees both the bindings and the enclosing scope. +// Direct eval code under evalWithBindings sees both the bindings and the enclosing scope. + var g = newGlobal('new-compartment'); var dbg = new Debugger(g); var hits = 0; diff --git a/js/src/jit-test/tests/debug/Object-environment-01.js b/js/src/jit-test/tests/debug/Object-environment-01.js new file mode 100644 index 00000000000..168bc780bb6 --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-environment-01.js @@ -0,0 +1,17 @@ +// obj.environment is undefined when the referent is not a JS function. + +var g = newGlobal('new-compartment') +var dbg = new Debugger; +var gw = dbg.addDebuggee(g); +assertEq(gw.environment, undefined); + +g.eval("var r = /x/;"); +var rw = gw.getOwnPropertyDescriptor("r").value; +assertEq(rw.class, "RegExp"); +assertEq(rw.environment, undefined); + +// Native function. +var fw = gw.getOwnPropertyDescriptor("parseInt").value; +assertEq(fw.class, "Function"); +assertEq(fw.environment, undefined); + diff --git a/js/src/jit-test/tests/debug/Object-environment-02.js b/js/src/jit-test/tests/debug/Object-environment-02.js new file mode 100644 index 00000000000..5f1a849feef --- /dev/null +++ b/js/src/jit-test/tests/debug/Object-environment-02.js @@ -0,0 +1,20 @@ +// The .environment of a function Debugger.Object is an Environment object. + +var g = newGlobal('new-compartment') +var dbg = Debugger(g); +var hits = 0; +g.h = function () { + var frame = dbg.getNewestFrame(); + var fn = frame.eval("j").return; + assertEq(fn.environment instanceof Debugger.Environment, true); + var closure = frame.eval("f").return; + assertEq(closure.environment instanceof Debugger.Environment, true); + hits++; +}; +g.eval("function j(a) {\n" + + " var f = function () { return a; };\n" + + " h();\n" + + " return f;\n" + + "}\n" + + "j(0);\n"); +assertEq(hits, 1); diff --git a/js/src/js.msg b/js/src/js.msg index 028555c226b..53f815d582a 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -372,3 +372,4 @@ MSG_DEF(JSMSG_DEBUG_COMPARTMENT_MISMATCH, 285, 2, JSEXN_TYPEERR, "{0}: descripto MSG_DEF(JSMSG_DEBUG_NOT_SCRIPT_FRAME, 286, 0, JSEXN_ERR, "stack frame is not running JavaScript code") MSG_DEF(JSMSG_CANT_WATCH_PROP, 287, 0, JSEXN_TYPEERR, "properties whose names are objects can't be watched") MSG_DEF(JSMSG_CSP_BLOCKED_EVAL, 288, 0, JSEXN_ERR, "call to eval() blocked by CSP") +MSG_DEF(JSMSG_DEBUG_NO_SCOPE_OBJECT, 289, 0, JSEXN_TYPEERR, "declarative Environments don't have binding objects") diff --git a/js/src/jscntxtinlines.h b/js/src/jscntxtinlines.h index ce21f4c862d..c7e99e5592d 100644 --- a/js/src/jscntxtinlines.h +++ b/js/src/jscntxtinlines.h @@ -227,7 +227,8 @@ class CompartmentChecker } void check(StackFrame *fp) { - check(&fp->scopeChain()); + if (fp) + check(&fp->scopeChain()); } }; diff --git a/js/src/jsdbgapi.cpp b/js/src/jsdbgapi.cpp index 9894805fc40..1763f6ee179 100644 --- a/js/src/jsdbgapi.cpp +++ b/js/src/jsdbgapi.cpp @@ -757,16 +757,16 @@ JS_EvaluateUCInStackFrame(JSContext *cx, JSStackFrame *fpArg, if (!CheckDebugMode(cx)) return false; - JSObject *scobj = JS_GetFrameScopeChain(cx, fpArg); - if (!scobj) + Env *env = JS_GetFrameScopeChain(cx, fpArg); + if (!env) return false; - js::AutoCompartment ac(cx, scobj); + js::AutoCompartment ac(cx, env); if (!ac.enter()) return false; StackFrame *fp = Valueify(fpArg); - return EvaluateInScope(cx, scobj, fp, chars, length, filename, lineno, rval); + return EvaluateInEnv(cx, env, fp, chars, length, filename, lineno, rval); } JS_PUBLIC_API(JSBool) diff --git a/js/src/jsprvtd.h b/js/src/jsprvtd.h index 1e400489dc0..0680106eb19 100644 --- a/js/src/jsprvtd.h +++ b/js/src/jsprvtd.h @@ -243,6 +243,14 @@ typedef HashMap RegExpPrivateCache; +/* + * Env is the type of what ES5 calls "lexical environments" (runtime + * activations of lexical scopes). This is currently just JSObject, and is + * implemented by Call, Block, With, and DeclEnv objects, among others--but + * environments and objects are really two different concepts. + */ +typedef JSObject Env; + typedef JSNative Native; typedef JSPropertyOp PropertyOp; typedef JSStrictPropertyOp StrictPropertyOp; diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp index e1f6afc25d7..8316bfdba28 100644 --- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -78,6 +78,13 @@ enum { JSSLOT_DEBUGARGUMENTS_COUNT }; +extern Class DebuggerEnv_class; + +enum { + JSSLOT_DEBUGENV_OWNER, + JSSLOT_DEBUGENV_COUNT +}; + extern Class DebuggerObject_class; enum { @@ -121,6 +128,21 @@ ReportObjectRequired(JSContext *cx) return false; } +bool +ValueToIdentifier(JSContext *cx, const Value &v, jsid *idp) +{ + jsid id; + if (!ValueToId(cx, v, &id)) + return false; + if (!JSID_IS_ATOM(id) || !IsIdentifier(JSID_TO_ATOM(id))) { + js_ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE, + JSDVG_SEARCH_STACK, v, NULL, "not an identifier", NULL); + return false; + } + *idp = id; + return true; +} + /*** Breakpoints *********************************************************************************/ @@ -297,7 +319,7 @@ Breakpoint::nextInSite() Debugger::Debugger(JSContext *cx, JSObject *dbg) : object(dbg), uncaughtExceptionHook(NULL), enabled(true), - frames(cx), objects(cx), scripts(cx) + frames(cx), scripts(cx), objects(cx), environments(cx) { assertSameCompartment(cx, dbg); @@ -319,24 +341,27 @@ Debugger::~Debugger() bool Debugger::init(JSContext *cx) { - bool ok = frames.init() && + bool ok = debuggees.init() && + frames.init() && + scripts.init() && objects.init() && - debuggees.init() && - scripts.init(); + environments.init(); if (!ok) js_ReportOutOfMemory(cx); return ok; } -JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGOBJECT_OWNER)); JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGSCRIPT_OWNER)); +JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGOBJECT_OWNER)); +JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGENV_OWNER)); Debugger * Debugger::fromChildJSObject(JSObject *obj) { JS_ASSERT(obj->getClass() == &DebuggerFrame_class || + obj->getClass() == &DebuggerScript_class || obj->getClass() == &DebuggerObject_class || - obj->getClass() == &DebuggerScript_class); + obj->getClass() == &DebuggerEnv_class); JSObject *dbgobj = &obj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject(); return fromJSObject(dbgobj); } @@ -469,6 +494,35 @@ Debugger::slowPathOnLeaveFrame(JSContext *cx) } } +bool +Debugger::wrapEnvironment(JSContext *cx, Env *env, Value *rval) +{ + if (!env) { + rval->setNull(); + return true; + } + + JSObject *envobj; + ObjectWeakMap::AddPtr p = environments.lookupForAdd(env); + if (p) { + envobj = p->value; + } else { + /* Create a new Debugger.Environment for env. */ + JSObject *proto = &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject(); + envobj = NewObjectWithGivenProto(cx, &DebuggerEnv_class, proto, NULL); + if (!envobj) + return false; + envobj->setPrivate(env); + envobj->setReservedSlot(JSSLOT_DEBUGENV_OWNER, ObjectValue(*object)); + if (!environments.relookupOrAdd(p, env, envobj)) { + js_ReportOutOfMemory(cx); + return false; + } + } + rval->setObject(*envobj); + return true; +} + bool Debugger::wrapDebuggeeValue(JSContext *cx, Value *vp) { @@ -1013,6 +1067,13 @@ Debugger::markKeysInCompartment(JSTracer *tracer) gc::MarkObject(tracer, key, "cross-compartment WeakMap key"); } + const ObjectMap &envStorage = environments; + for (ObjectMap::Range r = envStorage.all(); !r.empty(); r.popFront()) { + const HeapPtrObject &key = r.front().key; + if (key->compartment() == comp && IsAboutToBeFinalized(tracer->context, key)) + js::gc::MarkObject(tracer, key, "cross-compartment WeakMap key"); + } + typedef HashMap, RuntimeAllocPolicy> ScriptMap; const ScriptMap &scriptStorage = scripts; @@ -1178,11 +1239,14 @@ Debugger::trace(JSTracer *trc) MarkObject(trc, frameobj, "live Debugger.Frame"); } + /* Trace the weak map from JSScript instances to Debugger.Script objects. */ + scripts.trace(trc); + /* Trace the referent -> Debugger.Object weak map. */ objects.trace(trc); - /* Trace the weak map from JSScript instances to Debugger.Script objects. */ - scripts.trace(trc); + /* Trace the referent -> Debugger.Environment weak map. */ + environments.trace(trc); } void @@ -2402,6 +2466,10 @@ StackContains(JSContext *cx, StackFrame *fp) StackFrame *fp = (StackFrame *) thisobj->getPrivate(); \ JS_ASSERT(StackContains(cx, fp)) +#define THIS_FRAME_OWNER(cx, argc, vp, fnname, args, thisobj, fp, dbg) \ + THIS_FRAME(cx, argc, vp, fnname, args, thisobj, fp); \ + Debugger *dbg = Debugger::fromChildJSObject(thisobj) + static JSBool DebuggerFrame_getType(JSContext *cx, uintN argc, Value *vp) { @@ -2419,6 +2487,33 @@ DebuggerFrame_getType(JSContext *cx, uintN argc, Value *vp) return true; } +static Env * +Frame_GetEnv(JSContext *cx, StackFrame *fp) +{ + assertSameCompartment(cx, fp); + if (fp->isNonEvalFunctionFrame() && !fp->hasCallObj() && !CreateFunCallObject(cx, fp)) + return NULL; + return GetScopeChain(cx, fp); +} + +static JSBool +DebuggerFrame_getEnvironment(JSContext *cx, uintN argc, Value *vp) +{ + THIS_FRAME_OWNER(cx, argc, vp, "get environment", args, thisobj, fp, dbg); + + Env *env; + { + AutoCompartment ac(cx, &fp->scopeChain()); + if (!ac.enter()) + return false; + env = Frame_GetEnv(cx, fp); + if (!env) + return false; + } + + return dbg->wrapEnvironment(cx, env, &args.rval()); +} + static JSBool DebuggerFrame_getCallee(JSContext *cx, uintN argc, Value *vp) { @@ -2690,14 +2785,16 @@ DebuggerFrame_setOnStep(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) +EvaluateInEnv(JSContext *cx, Env *env, StackFrame *fp, const jschar *chars, + uintN length, const char *filename, uintN lineno, Value *rval) { - assertSameCompartment(cx, scobj, fp); + assertSameCompartment(cx, env, fp); - /* Execute assumes an already-computed 'this" value. */ - if (!ComputeThis(cx, fp)) - return false; + if (fp) { + /* Execute assumes an already-computed 'this" value. */ + if (!ComputeThis(cx, fp)) + return false; + } /* * NB: This function breaks the assumption that the compiler can see all @@ -2705,7 +2802,7 @@ EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *ch * we use a static level that will cause us not to attempt to optimize * variable references made by this frame. */ - JSScript *script = frontend::CompileScript(cx, scobj, fp, + JSScript *script = frontend::CompileScript(cx, env, fp, fp->scopeChain().principals(cx), TCF_COMPILE_N_GO | TCF_NEED_SCRIPT_GLOBAL, chars, length, filename, lineno, @@ -2715,7 +2812,7 @@ EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *ch if (!script) return false; - return ExecuteKernel(cx, script, *scobj, fp->thisValue(), EXECUTE_DEBUG, fp, rval); + return ExecuteKernel(cx, script, *env, fp->thisValue(), EXECUTE_DEBUG, fp, rval); } } @@ -2772,19 +2869,19 @@ DebuggerFrameEval(JSContext *cx, uintN argc, Value *vp, EvalBindingsMode mode) if (!ac.enter()) return false; - JSObject *scobj = JS_GetFrameScopeChain(cx, Jsvalify(fp)); - if (!scobj) + Env *env = JS_GetFrameScopeChain(cx, Jsvalify(fp)); + if (!env) return false; - /* If evalWithBindings, create the inner scope object. */ + /* If evalWithBindings, create the inner environment. */ if (mode == WithBindings) { - /* TODO - Should probably create a With object here. */ - scobj = NewObjectWithGivenProto(cx, &ObjectClass, NULL, scobj); - if (!scobj) + /* TODO - This should probably be a Call object, like ES5 strict eval. */ + env = NewObjectWithGivenProto(cx, &ObjectClass, NULL, env); + if (!env) 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)) + !DefineNativeProperty(cx, env, keys[i], values[i], NULL, NULL, 0, 0, 0)) { return false; } @@ -2794,8 +2891,8 @@ DebuggerFrameEval(JSContext *cx, uintN argc, Value *vp, EvalBindingsMode mode) /* Run the code and produce the completion value. */ Value rval; JS::Anchor anchor(linearStr); - bool ok = EvaluateInScope(cx, scobj, fp, linearStr->chars(), linearStr->length(), - "debugger eval code", 1, &rval); + bool ok = EvaluateInEnv(cx, env, fp, linearStr->chars(), linearStr->length(), + "debugger eval code", 1, &rval); return dbg->newCompletionValue(ac, ok, rval, vp); } @@ -2822,6 +2919,7 @@ static JSPropertySpec DebuggerFrame_properties[] = { JS_PSG("arguments", DebuggerFrame_getArguments, 0), JS_PSG("callee", DebuggerFrame_getCallee, 0), JS_PSG("constructing", DebuggerFrame_getConstructing, 0), + JS_PSG("environment", DebuggerFrame_getEnvironment, 0), JS_PSG("generator", DebuggerFrame_getGenerator, 0), JS_PSG("live", DebuggerFrame_getLive, 0), JS_PSG("offset", DebuggerFrame_getOffset, 0), @@ -3014,14 +3112,16 @@ DebuggerObject_getScript(JSContext *cx, uintN argc, Value *vp) { THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get script", args, dbg, obj); - args.rval().setUndefined(); - - if (!obj->isFunction()) + if (!obj->isFunction()) { + args.rval().setUndefined(); return true; + } JSFunction *fun = obj->toFunction(); - if (!fun->isInterpreted()) + if (!fun->isInterpreted()) { + args.rval().setUndefined(); return true; + } JSObject *scriptObject = dbg->wrapScript(cx, fun->script()); if (!scriptObject) @@ -3031,6 +3131,21 @@ DebuggerObject_getScript(JSContext *cx, uintN argc, Value *vp) return true; } +static JSBool +DebuggerObject_getEnvironment(JSContext *cx, uintN argc, Value *vp) +{ + THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "get environment", args, dbg, obj); + + /* Don't bother switching compartments just to check obj's type and get its env. */ + if (!obj->isFunction() || !obj->toFunction()->isInterpreted()) { + args.rval().setUndefined(); + return true; + } + + Env *env = obj->toFunction()->environment(); + return dbg->wrapEnvironment(cx, env, &args.rval()); +} + static JSBool DebuggerObject_getOwnPropertyDescriptor(JSContext *cx, uintN argc, Value *vp) { @@ -3446,6 +3561,7 @@ static JSPropertySpec DebuggerObject_properties[] = { JS_PSG("name", DebuggerObject_getName, 0), JS_PSG("parameterNames", DebuggerObject_getParameterNames, 0), JS_PSG("script", DebuggerObject_getScript, 0), + JS_PSG("environment", DebuggerObject_getEnvironment, 0), JS_PS_END }; @@ -3466,6 +3582,208 @@ static JSFunctionSpec DebuggerObject_methods[] = { JS_FS_END }; + +/*** Debugger.Environment ************************************************************************/ + +static void +DebuggerEnv_trace(JSTracer *trc, JSObject *obj) +{ + if (!trc->runtime->gcCurrentCompartment) { + /* + * There is a barrier on private pointers, so the Unbarriered marking + * is okay. + */ + if (Env *referent = (JSObject *) obj->getPrivate()) + MarkObjectUnbarriered(trc, referent, "Debugger.Environment referent"); + } +} + +Class DebuggerEnv_class = { + "Environment", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGENV_COUNT), + JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_StrictPropertyStub, + JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, NULL, + NULL, /* reserved0 */ + NULL, /* checkAccess */ + NULL, /* call */ + NULL, /* construct */ + NULL, /* xdrObject */ + NULL, /* hasInstance */ + DebuggerEnv_trace +}; + +static JSObject * +DebuggerEnv_checkThis(JSContext *cx, const CallArgs &args, const char *fnname) +{ + if (!args.thisv().isObject()) { + ReportObjectRequired(cx); + return NULL; + } + JSObject *thisobj = &args.thisv().toObject(); + if (thisobj->getClass() != &DebuggerEnv_class) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Environment", fnname, thisobj->getClass()->name); + return NULL; + } + + /* + * Forbid Debugger.Environment.prototype, which is of class DebuggerEnv_class + * but isn't a real working Debugger.Environment. The prototype object is + * distinguished by having no referent. + */ + if (!thisobj->getPrivate()) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO, + "Debugger.Environment", fnname, "prototype object"); + return NULL; + } + return thisobj; +} + +#define THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env) \ + CallArgs args = CallArgsFromVp(argc, vp); \ + JSObject *envobj = DebuggerEnv_checkThis(cx, args, fnname); \ + if (!envobj) \ + return false; \ + Env *env = static_cast(envobj->getPrivate()); \ + JS_ASSERT(env) + +#define THIS_DEBUGENV_OWNER(cx, argc, vp, fnname, args, envobj, env, dbg) \ + THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env); \ + Debugger *dbg = Debugger::fromChildJSObject(envobj) + +static JSBool +DebuggerEnv_construct(JSContext *cx, uintN argc, Value *vp) +{ + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debugger.Environment"); + return false; +} + +static JSBool +DebuggerEnv_getType(JSContext *cx, uintN argc, Value *vp) +{ + THIS_DEBUGENV(cx, argc, vp, "get type", args, envobj, env); + + /* Don't bother switching compartments just to check env's class. */ + const char *s; + if (env->isCall() || env->isBlock() || env->isDeclEnv()) + s = "declarative"; + else + s = "object"; + + JSAtom *str = js_Atomize(cx, s, strlen(s), InternAtom, NormalEncoding); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +static JSBool +DebuggerEnv_getParent(JSContext *cx, uintN argc, Value *vp) +{ + THIS_DEBUGENV_OWNER(cx, argc, vp, "get parent", args, envobj, env, dbg); + + /* Don't bother switching compartments just to get env's parent. */ + Env *parent = env->scopeChain(); + return dbg->wrapEnvironment(cx, parent, &args.rval()); +} + +static JSBool +DebuggerEnv_getObject(JSContext *cx, uintN argc, Value *vp) +{ + THIS_DEBUGENV_OWNER(cx, argc, vp, "get type", args, envobj, env, dbg); + + /* + * Don't bother switching compartments just to check env's class and + * possibly get its proto. + */ + if (env->isCall() || env->isBlock() || env->isDeclEnv()) { + JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NO_SCOPE_OBJECT); + return false; + } + JSObject *obj = env->isWith() ? env->getProto() : env; + + Value rval = ObjectValue(*obj); + if (!dbg->wrapDebuggeeValue(cx, &rval)) + return false; + args.rval() = rval; + return true; +} + +static JSBool +DebuggerEnv_names(JSContext *cx, uintN argc, Value *vp) +{ + THIS_DEBUGENV_OWNER(cx, argc, vp, "get type", args, envobj, env, dbg); + + AutoIdVector keys(cx); + { + AutoCompartment ac(cx, env); + if (!ac.enter()) + return false; + + ErrorCopier ec(ac, dbg->toJSObject()); + if (!GetPropertyNames(cx, env, JSITER_HIDDEN, &keys)) + return false; + } + + JSObject *arr = NewDenseEmptyArray(cx); + if (!arr) + return false; + for (size_t i = 0, len = keys.length(); i < len; i++) { + jsid id = keys[i]; + if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) { + if (!cx->compartment->wrapId(cx, &id)) + return false; + if (!js_NewbornArrayPush(cx, arr, StringValue(JSID_TO_STRING(id)))) + return false; + } + } + args.rval().setObject(*arr); + return true; +} + +static JSBool +DebuggerEnv_find(JSContext *cx, uintN argc, Value *vp) +{ + REQUIRE_ARGC("Debugger.Environment.find", 1); + THIS_DEBUGENV_OWNER(cx, argc, vp, "get type", args, envobj, env, dbg); + + jsid id; + if (!ValueToIdentifier(cx, args[0], &id)) + return false; + + { + AutoCompartment ac(cx, env); + if (!ac.enter() || !cx->compartment->wrapId(cx, &id)) + return false; + + /* This can trigger resolve hooks. */ + ErrorCopier ec(ac, dbg->toJSObject()); + JSProperty *prop = NULL; + JSObject *pobj; + for (; env && !prop; env = env->scopeChain()) { + if (!env->lookupGeneric(cx, id, &pobj, &prop)) + return false; + if (prop) + break; + } + } + + return dbg->wrapEnvironment(cx, env, &args.rval()); +} + +static JSPropertySpec DebuggerEnv_properties[] = { + JS_PSG("type", DebuggerEnv_getType, 0), + JS_PSG("object", DebuggerEnv_getObject, 0), + JS_PSG("parent", DebuggerEnv_getParent, 0), + JS_PS_END +}; + +static JSFunctionSpec DebuggerEnv_methods[] = { + JS_FN("names", DebuggerEnv_names, 0, 0), + JS_FN("find", DebuggerEnv_find, 1, 0), + JS_FS_END +}; + + /*** Glue ****************************************************************************************/ @@ -3504,8 +3822,16 @@ JS_DefineDebuggerObject(JSContext *cx, JSObject *obj) if (!objectProto) return false; + JSObject *envProto = js_InitClass(cx, debugCtor, objProto, &DebuggerEnv_class, + DebuggerEnv_construct, 0, + DebuggerEnv_properties, DebuggerEnv_methods, + NULL, NULL); + if (!envProto) + return false; + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto)); debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_SCRIPT_PROTO, ObjectValue(*scriptProto)); + debugProto->setReservedSlot(Debugger::JSSLOT_DEBUG_ENV_PROTO, ObjectValue(*envProto)); return true; } diff --git a/js/src/vm/Debugger.h b/js/src/vm/Debugger.h index 5daf9855889..fcbece278eb 100644 --- a/js/src/vm/Debugger.h +++ b/js/src/vm/Debugger.h @@ -72,6 +72,7 @@ class Debugger { enum { JSSLOT_DEBUG_PROTO_START, JSSLOT_DEBUG_FRAME_PROTO = JSSLOT_DEBUG_PROTO_START, + JSSLOT_DEBUG_ENV_PROTO, JSSLOT_DEBUG_OBJECT_PROTO, JSSLOT_DEBUG_SCRIPT_PROTO, JSSLOT_DEBUG_PROTO_STOP, @@ -105,13 +106,16 @@ class Debugger { FrameMap; FrameMap frames; + /* An ephemeral map from JSScript* to Debugger.Script instances. */ + typedef WeakMap ScriptWeakMap; + ScriptWeakMap scripts; + /* The map from debuggee objects to their Debugger.Object instances. */ typedef WeakMap ObjectWeakMap; ObjectWeakMap objects; - /* An ephemeral map from JSScript* to Debugger.Script instances. */ - typedef WeakMap ScriptWeakMap; - ScriptWeakMap scripts; + /* The map from debuggee Envs to Debugger.Environment instances. */ + ObjectWeakMap environments; bool addDebuggeeGlobal(JSContext *cx, GlobalObject *obj); void removeDebuggeeGlobal(JSContext *cx, GlobalObject *global, @@ -272,6 +276,13 @@ class Debugger { inline bool observesGlobal(GlobalObject *global) const; inline bool observesFrame(StackFrame *fp) const; + /* + * If env is NULL, call vp->setNull() and return true. Otherwise, find or + * create a Debugger.Environment object for the given Env. On success, + * store the Environment object in *vp and return true. + */ + bool wrapEnvironment(JSContext *cx, Env *env, Value *vp); + /* * Like cx->compartment->wrap(cx, vp), but for the debugger compartment. * @@ -518,8 +529,8 @@ Debugger::onNewScript(JSContext *cx, JSScript *script, GlobalObject *compileAndG } extern JSBool -EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *chars, - uintN length, const char *filename, uintN lineno, Value *rval); +EvaluateInEnv(JSContext *cx, Env *env, StackFrame *fp, const jschar *chars, + uintN length, const char *filename, uintN lineno, Value *rval); }