A first cut at Debugger.Environment. Bug 690558, r=jimb.
authorJason Orendorff <jorendorff@mozilla.com>
Thu, 08 Dec 2011 14:54:26 -0600
changeset 82305 3c620a2f8919a2862bcc00a2cae4ea2d12be69e1
parent 82304 5eb308b7f2b217e5e66503ca6d676b283283b87c
child 82306 c7d9b329a45da7d34efca2d35d4209e214f7da0d
push id3959
push userjorendorff@mozilla.com
push dateThu, 08 Dec 2011 21:10:45 +0000
treeherdermozilla-inbound@3c620a2f8919 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs690558
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
A first cut at Debugger.Environment. Bug 690558, r=jimb.
js/src/jit-test/tests/debug/Environment-01.js
js/src/jit-test/tests/debug/Environment-02.js
js/src/jit-test/tests/debug/Environment-find-01.js
js/src/jit-test/tests/debug/Environment-find-02.js
js/src/jit-test/tests/debug/Environment-find-03.js
js/src/jit-test/tests/debug/Environment-find-04.js
js/src/jit-test/tests/debug/Environment-find-05.js
js/src/jit-test/tests/debug/Environment-find-06.js
js/src/jit-test/tests/debug/Environment-gc-01.js
js/src/jit-test/tests/debug/Environment-gc-02.js
js/src/jit-test/tests/debug/Environment-identity-01.js
js/src/jit-test/tests/debug/Environment-identity-02.js
js/src/jit-test/tests/debug/Environment-identity-03.js
js/src/jit-test/tests/debug/Environment-identity-04.js
js/src/jit-test/tests/debug/Environment-names-01.js
js/src/jit-test/tests/debug/Environment-names-02.js
js/src/jit-test/tests/debug/Environment-parent-01.js
js/src/jit-test/tests/debug/Environment-type-01.js
js/src/jit-test/tests/debug/Frame-environment-01.js
js/src/jit-test/tests/debug/Frame-environment-02.js
js/src/jit-test/tests/debug/Frame-environment-03.js
js/src/jit-test/tests/debug/Frame-environment-04.js
js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
js/src/jit-test/tests/debug/Object-environment-01.js
js/src/jit-test/tests/debug/Object-environment-02.js
js/src/js.msg
js/src/jscntxtinlines.h
js/src/jsdbgapi.cpp
js/src/jsprvtd.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
new file mode 100644
--- /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);');
new file mode 100644
--- /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");
new file mode 100644
--- /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();
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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 = <><y/><z/></>.(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));
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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);
new file mode 100644
--- /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');
new file mode 100644
--- /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();
new file mode 100644
--- /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()");
new file mode 100644
--- /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);
new file mode 100644
--- /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);
--- a/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
+++ b/js/src/jit-test/tests/debug/Frame-evalWithBindings-10.js
@@ -1,9 +1,10 @@
-// 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;
 dbg.onDebuggerStatement = function (frame) {
     var code =
         "assertEq(a, 1234);\n" +
         "assertEq(b, null);\n" +
         "assertEq(c, 'ok');\n";
new file mode 100644
--- /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);
+
new file mode 100644
--- /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);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -367,8 +367,9 @@ MSG_DEF(JSMSG_DEBUG_LOOP,             28
 MSG_DEF(JSMSG_DEBUG_NOT_IDLE,         281, 0, JSEXN_ERR, "can't start debugging: a debuggee script is on the stack")
 MSG_DEF(JSMSG_DEBUG_BAD_OFFSET,       282, 0, JSEXN_TYPEERR, "invalid script offset")
 MSG_DEF(JSMSG_DEBUG_BAD_LINE,         283, 0, JSEXN_TYPEERR, "invalid line number")
 MSG_DEF(JSMSG_DEBUG_NOT_DEBUGGING,    284, 0, JSEXN_ERR, "can't set breakpoint: script global is not a debuggee")
 MSG_DEF(JSMSG_DEBUG_COMPARTMENT_MISMATCH, 285, 2, JSEXN_TYPEERR, "{0}: descriptor .{1} property is an object in a different compartment than the target object")
 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")
--- a/js/src/jscntxtinlines.h
+++ b/js/src/jscntxtinlines.h
@@ -222,17 +222,18 @@ class CompartmentChecker
         if (script) {
             check(script->compartment());
             if (!script->isCachedEval && script->globalObject)
                 check(script->globalObject);
         }
     }
 
     void check(StackFrame *fp) {
-        check(&fp->scopeChain());
+        if (fp)
+            check(&fp->scopeChain());
     }
 };
 
 #endif
 
 /*
  * Don't perform these checks when called from a finalizer. The checking
  * depends on other objects not having been swept yet.
--- a/js/src/jsdbgapi.cpp
+++ b/js/src/jsdbgapi.cpp
@@ -752,26 +752,26 @@ JS_PUBLIC_API(JSBool)
 JS_EvaluateUCInStackFrame(JSContext *cx, JSStackFrame *fpArg,
                           const jschar *chars, uintN length,
                           const char *filename, uintN lineno,
                           jsval *rval)
 {
     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)
 JS_EvaluateInStackFrame(JSContext *cx, JSStackFrame *fp,
                         const char *bytes, uintN length,
                         const char *filename, uintN lineno,
                         jsval *rval)
 {
--- a/js/src/jsprvtd.h
+++ b/js/src/jsprvtd.h
@@ -238,16 +238,24 @@ class Debugger;
 class WatchpointMap;
 
 typedef HashMap<JSAtom *,
                 detail::RegExpPrivateCacheValue,
                 DefaultHasher<JSAtom *>,
                 RuntimeAllocPolicy>
     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;
 typedef JSPropertyDescriptor PropertyDescriptor;
 
 namespace analyze {
 
 struct LifetimeVariable;
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -73,16 +73,23 @@ enum {
 
 extern Class DebuggerArguments_class;
 
 enum {
     JSSLOT_DEBUGARGUMENTS_FRAME,
     JSSLOT_DEBUGARGUMENTS_COUNT
 };
 
+extern Class DebuggerEnv_class;
+
+enum {
+    JSSLOT_DEBUGENV_OWNER,
+    JSSLOT_DEBUGENV_COUNT
+};
+
 extern Class DebuggerObject_class;
 
 enum {
     JSSLOT_DEBUGOBJECT_OWNER,
     JSSLOT_DEBUGOBJECT_COUNT
 };
 
 extern Class DebuggerScript_class;
@@ -116,16 +123,31 @@ ReportMoreArgsNeeded(JSContext *cx, cons
 
 bool
 ReportObjectRequired(JSContext *cx)
 {
     JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_NONNULL_OBJECT);
     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 *********************************************************************************/
 
 BreakpointSite::BreakpointSite(JSScript *script, jsbytecode *pc)
   : script(script), pc(pc), scriptGlobal(NULL), enabledCount(0),
     trapHandler(NULL), trapClosure(UndefinedValue())
 {
     JS_ASSERT(!script->hasBreakpointsAt(pc));
@@ -292,17 +314,17 @@ Breakpoint::nextInSite()
     return (link == &site->breakpoints) ? NULL : fromSiteLinks(link);
 }
 
 
 /*** Debugger hook dispatch **********************************************************************/
 
 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);
 
     JSRuntime *rt = cx->runtime;
     AutoLockGC lock(rt);
     JS_APPEND_LINK(&link, &rt->debuggerList);
     JS_INIT_CLIST(&breakpoints);
 }
@@ -314,34 +336,37 @@ Debugger::~Debugger()
     /* This always happens in the GC thread, so no locking is required. */
     JS_ASSERT(object->compartment()->rt->gcRunning);
     JS_REMOVE_LINK(&link);
 }
 
 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_DEBUGSCRIPT_OWNER));
 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_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);
 }
 
 bool
 Debugger::getScriptFrame(JSContext *cx, StackFrame *fp, Value *vp)
 {
     JS_ASSERT(fp->isScriptFrame());
@@ -465,16 +490,45 @@ Debugger::slowPathOnLeaveFrame(JSContext
      */
     if (fp->isEvalFrame()) {
         JSScript *script = fp->script();
         script->clearBreakpointsIn(cx, NULL, NULL);
     }
 }
 
 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)
 {
     assertSameCompartment(cx, object.get());
 
     if (vp->isObject()) {
         JSObject *obj = &vp->toObject();
 
         ObjectWeakMap::AddPtr p = objects.lookupForAdd(obj);
@@ -1008,16 +1062,23 @@ Debugger::markKeysInCompartment(JSTracer
         ObjectMap;
     const ObjectMap &objStorage = objects;
     for (ObjectMap::Range r = objStorage.all(); !r.empty(); r.popFront()) {
         const HeapPtrObject &key = r.front().key;
         if (key->compartment() == comp && IsAboutToBeFinalized(tracer->context, key))
             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<HeapPtrScript, HeapPtrObject, DefaultHasher<HeapPtrScript>, RuntimeAllocPolicy>
         ScriptMap;
     const ScriptMap &scriptStorage = scripts;
     for (ScriptMap::Range r = scriptStorage.all(); !r.empty(); r.popFront()) {
         const HeapPtrScript &key = r.front().key;
         if (key->compartment() == comp && IsAboutToBeFinalized(tracer->context, key))
             gc::MarkScript(tracer, key, "cross-compartment WeakMap key");
     }
@@ -1173,21 +1234,24 @@ Debugger::trace(JSTracer *trc)
      * frames.)
      */
     for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
         const HeapPtrObject &frameobj = r.front().value;
         JS_ASSERT(frameobj->getPrivate());
         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
 Debugger::sweepAll(JSContext *cx)
 {
     JSRuntime *rt = cx->runtime;
     JS_ASSERT(!rt->gcCurrentCompartment);
 
@@ -2397,16 +2461,20 @@ StackContains(JSContext *cx, StackFrame 
 #define THIS_FRAME(cx, argc, vp, fnname, args, thisobj, fp)                  \
     CallArgs args = CallArgsFromVp(argc, vp);                                \
     JSObject *thisobj = CheckThisFrame(cx, args, fnname, true);              \
     if (!thisobj)                                                            \
         return false;                                                        \
     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)
 {
     THIS_FRAME(cx, argc, vp, "get type", args, thisobj, fp);
 
     /*
      * Indirect eval frames are both isGlobalFrame() and isEvalFrame(), so the
      * order of checks here is significant.
@@ -2414,16 +2482,43 @@ DebuggerFrame_getType(JSContext *cx, uin
     args.rval().setString(fp->isEvalFrame()
                           ? cx->runtime->atomState.evalAtom
                           : fp->isGlobalFrame()
                           ? cx->runtime->atomState.globalAtom
                           : cx->runtime->atomState.callAtom);
     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)
 {
     THIS_FRAME(cx, argc, vp, "get callee", args, thisobj, fp);
     Value calleev = (fp->isFunctionFrame() && !fp->isEvalFrame()) ? fp->calleev() : NullValue();
     if (!Debugger::fromChildJSObject(thisobj)->wrapDebuggeeValue(cx, &calleev))
         return false;
     args.rval() = calleev;
@@ -2685,42 +2780,44 @@ DebuggerFrame_setOnStep(JSContext *cx, u
     thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, args[0]);
     args.rval().setUndefined();
     return true;
 }
 
 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);
-
-    /* Execute assumes an already-computed 'this" value. */
-    if (!ComputeThis(cx, fp))
-        return false;
+    assertSameCompartment(cx, env, fp);
+
+    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
      * 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 = 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,
                                                cx->findVersion(), NULL,
                                                UpvarCookie::UPVAR_LEVEL_LIMIT);
 
     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);
 }
 
 }
 
 enum EvalBindingsMode { WithoutBindings, WithBindings };
 
 static JSBool
 DebuggerFrameEval(JSContext *cx, uintN argc, Value *vp, EvalBindingsMode mode)
@@ -2767,40 +2864,40 @@ DebuggerFrameEval(JSContext *cx, uintN a
             }
         }
     }
 
     AutoCompartment ac(cx, &fp->scopeChain());
     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;
             }
         }
     }
 
     /* Run the code and produce the completion value. */
     Value rval;
     JS::Anchor<JSString *> 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);
 }
 
 static JSBool
 DebuggerFrame_eval(JSContext *cx, uintN argc, Value *vp)
 {
     return DebuggerFrameEval(cx, argc, vp, WithoutBindings);
 }
@@ -2817,16 +2914,17 @@ DebuggerFrame_construct(JSContext *cx, u
     JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debugger.Frame");
     return false;
 }
 
 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),
     JS_PSG("older", DebuggerFrame_getOlder, 0),
     JS_PSG("script", DebuggerFrame_getScript, 0),
     JS_PSG("this", DebuggerFrame_getThis, 0),
     JS_PSG("type", DebuggerFrame_getType, 0),
     JS_PSGS("onStep", DebuggerFrame_getOnStep, DebuggerFrame_setOnStep, 0),
@@ -3009,34 +3107,51 @@ DebuggerObject_getParameterNames(JSConte
     return true;
 }
 
 static JSBool
 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)
         return false;
 
     args.rval().setObject(*scriptObject);
     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)
 {
     THIS_DEBUGOBJECT_OWNER_REFERENT(cx, argc, vp, "getOwnPropertyDescriptor", args, dbg, obj);
 
     jsid id;
     if (!ValueToId(cx, argc >= 1 ? args[0] : UndefinedValue(), &id))
         return false;
 
@@ -3441,16 +3556,17 @@ DebuggerObject_call(JSContext *cx, uintN
 
 static JSPropertySpec DebuggerObject_properties[] = {
     JS_PSG("proto", DebuggerObject_getProto, 0),
     JS_PSG("class", DebuggerObject_getClass, 0),
     JS_PSG("callable", DebuggerObject_getCallable, 0),
     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
 };
 
 static JSFunctionSpec DebuggerObject_methods[] = {
     JS_FN("getOwnPropertyDescriptor", DebuggerObject_getOwnPropertyDescriptor, 1, 0),
     JS_FN("getOwnPropertyNames", DebuggerObject_getOwnPropertyNames, 0, 0),
     JS_FN("defineProperty", DebuggerObject_defineProperty, 2, 0),
     JS_FN("defineProperties", DebuggerObject_defineProperties, 1, 0),
@@ -3462,16 +3578,218 @@ static JSFunctionSpec DebuggerObject_met
     JS_FN("isFrozen", DebuggerObject_isFrozen, 0, 0),
     JS_FN("isExtensible", DebuggerObject_isExtensible, 0, 0),
     JS_FN("apply", DebuggerObject_apply, 0, 0),
     JS_FN("call", DebuggerObject_call, 0, 0),
     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<Env *>(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 ****************************************************************************************/
 
 extern JS_PUBLIC_API(JSBool)
 JS_DefineDebuggerObject(JSContext *cx, JSObject *obj)
 {
     JSObject *objProto;
     if (!js_GetClassPrototype(cx, obj, JSProto_Object, &objProto))
         return false;
@@ -3499,13 +3817,21 @@ JS_DefineDebuggerObject(JSContext *cx, J
 
     JSObject *objectProto = js_InitClass(cx, debugCtor, objProto, &DebuggerObject_class,
                                          DebuggerObject_construct, 0,
                                          DebuggerObject_properties, DebuggerObject_methods,
                                          NULL, NULL);
     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;
 }
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -67,16 +67,17 @@ class Debugger {
         OnNewScript,
         OnEnterFrame,
         HookCount
     };
 
     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,
         JSSLOT_DEBUG_HOOK_START = JSSLOT_DEBUG_PROTO_STOP,
         JSSLOT_DEBUG_HOOK_STOP = JSSLOT_DEBUG_HOOK_START + HookCount,
         JSSLOT_DEBUG_COUNT = JSSLOT_DEBUG_HOOK_STOP
     };
 
@@ -100,23 +101,26 @@ class Debugger {
      * thus necessarily live), but we do trace the values. It's like a WeakMap
      * that way, but since stack frames are not gc-things, the implementation
      * has to be different.
      */
     typedef HashMap<StackFrame *, HeapPtrObject, DefaultHasher<StackFrame *>, RuntimeAllocPolicy>
         FrameMap;
     FrameMap frames;
 
+    /* An ephemeral map from JSScript* to Debugger.Script instances. */
+    typedef WeakMap<HeapPtrScript, HeapPtrObject> ScriptWeakMap;
+    ScriptWeakMap scripts;
+
     /* The map from debuggee objects to their Debugger.Object instances. */
     typedef WeakMap<HeapPtrObject, HeapPtrObject> ObjectWeakMap;
     ObjectWeakMap objects;
 
-    /* An ephemeral map from JSScript* to Debugger.Script instances. */
-    typedef WeakMap<HeapPtrScript, HeapPtrObject> 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,
                               GlobalObjectSet::Enum *compartmentEnum,
                               GlobalObjectSet::Enum *debugEnum);
 
     /*
      * Cope with an error or exception in a debugger hook.
@@ -268,16 +272,23 @@ class Debugger {
     /************************************* Functions for use by Debugger.cpp. */
 
     inline bool observesEnterFrame() const;
     inline bool observesNewScript() const;
     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.
      *
      * Preconditions: *vp is a value from a debuggee compartment; cx is in the
      * debugger's compartment.
      *
      * If *vp is an object, this produces a (new or existing) Debugger.Object
      * wrapper for it. Otherwise this is the same as JSCompartment::wrap.
      */
@@ -513,14 +524,14 @@ Debugger::onNewScript(JSContext *cx, JSS
 {
     JS_ASSERT_IF(script->compileAndGo, compileAndGoGlobal);
     JS_ASSERT_IF(!script->compileAndGo, !compileAndGoGlobal);
     if (!script->compartment()->getDebuggees().empty())
         slowPathOnNewScript(cx, script, compileAndGoGlobal);
 }
 
 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);
 
 }
 
 #endif /* Debugger_h__ */