Bug 673125: Implement the Debugger.Frame.prototype.onStep accessor. r=jorendorff.
authorJim Blandy <jimb@mozilla.com>
Tue, 23 Aug 2011 14:45:36 -0500
changeset 77171 29527b07008dd2dfce9ecc355e9c46dd8df433c6
parent 77170 2853df3a5b570e4015c497c36adcdcaabf1d67c4
child 77172 6daedd1baec4267352979c3e42d4e1ea64206d19
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs673125
milestone9.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
Bug 673125: Implement the Debugger.Frame.prototype.onStep accessor. r=jorendorff.
js/src/jit-test/tests/debug/Frame-arguments-07.js
js/src/jit-test/tests/debug/Frame-onStep-01.js
js/src/jit-test/tests/debug/Frame-onStep-02.js
js/src/jit-test/tests/debug/Frame-onStep-03.js
js/src/jit-test/tests/debug/Frame-onStep-04.js
js/src/jit-test/tests/debug/Frame-onStep-05.js
js/src/jit-test/tests/debug/Frame-onStep-06.js
js/src/jit-test/tests/debug/Frame-onStep-07.js
js/src/jit-test/tests/debug/Frame-onStep-08.js
js/src/jit-test/tests/debug/Frame-onStep-09.js
js/src/jit-test/tests/debug/Frame-onStep-10.js
js/src/jit-test/tests/debug/Frame-onStep-lines-01.js
js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js
js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js
js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js
js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js
js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js
js/src/jit-test/tests/debug/Object-seal-01.js
js/src/jit-test/tests/debug/onExceptionUnwind-07.js
js/src/js.msg
js/src/jsinterp.cpp
js/src/methodjit/StubCalls.cpp
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
--- a/js/src/jit-test/tests/debug/Frame-arguments-07.js
+++ b/js/src/jit-test/tests/debug/Frame-arguments-07.js
@@ -9,15 +9,15 @@ g.eval("function f(a, b) {\n" +
 
 var dbg = Debugger(g);
 var hits = 0;
 dbg.onDebuggerStatement = function (frame) {
     var argc = frame.eval("arguments.length").return;
     var args = frame.arguments;
     assertEq(args.length, argc);
     for (var i = 0; i < argc; i++)
-	assertEq(args[i], i);
+        assertEq(args[i], i);
     hits++;
 }
 
 g.f(9);
 g.f(9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9);
 assertEq(hits, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-01.js
@@ -0,0 +1,24 @@
+// Simple Debugger.Frame.prototype.onStep test.
+// Test that onStep fires often enough to see all four values of a.
+
+var g = newGlobal('new-compartment');
+g.a = 0;
+g.eval("function f() {\n" +
+       "    a += 2;\n" +
+       "    a += 2;\n" +
+       "    a += 2;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0, 0, 0];
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () {
+        assertEq(arguments.length, 0);
+        assertEq(this, frame);
+        seen[g.a] = 1;
+    };
+}
+
+g.f();
+assertEq(seen.join(""), "1010101");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-02.js
@@ -0,0 +1,27 @@
+// Setting frame.onStep to undefined turns off single-stepping.
+
+var g = newGlobal('new-compartment');
+g.a = 0;
+g.eval("function f() {\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0];
+dbg.onEnterFrame = function (frame) {
+    seen[g.a] = 1;
+    frame.onStep = function () {
+        seen[g.a] = 1;
+        if (g.a === 2) {
+            frame.onStep = undefined;
+            assertEq(frame.onStep, undefined);
+        }
+    };
+}
+
+g.f();
+assertEq(seen.join(","), "1,1,1,0,0");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-03.js
@@ -0,0 +1,28 @@
+// Setting onStep does not affect later calls to the same function.
+// (onStep is per-frame, not per-function.)
+
+var g = newGlobal('new-compartment');
+g.a = 1;
+g.eval("function f(a) {\n" +
+       "    var x = 2 * a;\n" +
+       "    return x * x;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var log = '';
+dbg.onEnterFrame = function (frame) {
+    log += '+';
+    frame.onStep = function () {
+        if (log.charAt(log.length - 1) != 's')
+            log += 's';
+    };
+};
+
+g.f(1);
+log += '|';
+g.f(2);
+log += '|';
+dbg.onEnterFrame = undefined;
+g.f(3);
+
+assertEq(log, '+s|+s|');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-04.js
@@ -0,0 +1,34 @@
+// When a recursive function has many frames on the stack, onStep may be set or
+// not independently on each frame.
+
+var g = newGlobal('new-compartment');
+g.eval("function f(x) {\n" +
+       "    if (x > 0)\n" +
+       "        f(x - 1);\n" +
+       "    else\n" +
+       "        debugger;\n" +
+       "    return x;\n" +
+       "}");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0, 0, 0, 0];
+function step() {
+    seen[this.arguments[0]] = 1;
+}
+dbg.onEnterFrame = function (frame) {
+    // Turn on stepping for even-numbered frames.
+    var x = frame.arguments[0];
+    if (x % 2 === 0)
+        frame.onStep = step;
+};
+dbg.onDebuggerStatement = function (frame) {
+    // This is called with 8 call frames on the stack, 7 down to 0.
+    // At this point we should have seen all the even-numbered frames.
+    assertEq(seen.join(""), "10101010");
+
+    // Now reset seen to see which frames fire onStep on the way out.
+    seen = [0, 0, 0, 0, 0, 0, 0, 0];
+};
+
+g.f(7);
+assertEq(seen.join(""), "10101010");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-05.js
@@ -0,0 +1,14 @@
+// Upon returning to a frame with an onStep hook, the hook is called before the
+// next line.
+
+var g = newGlobal('new-compartment');
+g.log = '';
+g.eval("function f() { debugger; }");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () { g.log += 's'; };
+};
+g.eval("f();\n" +
+       "log += 'x';\n");
+assertEq(g.log.charAt(0), 's');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-06.js
@@ -0,0 +1,66 @@
+// After returning from an implicit toString call, the calling frame's onStep
+// hook fires.
+
+var g = newGlobal('new-compartment');
+g.eval("var originalX = {toString: function () { debugger; log += 'x'; return 1; }};\n");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    g.log += 'd';
+    frame.older.onStep = function () {
+        if (!g.log.match(/[sy]$/))
+            g.log += 's';
+    };
+};
+
+// expr is an expression that will trigger an implicit toString call.
+function check(expr) {
+    g.log = '';
+    g.x = g.originalX;
+    g.eval(expr + ";\n" +
+	   "log += 'y';\n");
+    assertEq(g.log, 'dxsy');
+}
+
+check("'' + x");
+check("0 + x");
+check("0 - x");
+check("0 * x");
+check("0 / x");
+check("0 % x");
+check("+x");
+check("x in {}");
+check("x++");
+check("++x");
+check("x--");
+check("--x");
+check("x < 0");
+check("x > 0");
+check("x >= 0");
+check("x <= 0");
+check("x == 0");
+check("x != 0");
+check("x & 1");
+check("x | 1");
+check("x ^ 1");
+check("~x");
+check("x << 1");
+check("x >> 1");
+check("x >>> 1");
+
+g.eval("function lastStep() { throw StopIteration; }");
+g.eval("function emptyIterator() { debugger; log += 'x'; return { next: lastStep }; }");
+g.eval("var customEmptyIterator = { __iterator__: emptyIterator };");
+g.log = '';
+g.eval("for (i in customEmptyIterator);\n" +
+       "log += 'y';\n");
+assertEq(g.log, 'dxsy');
+
+g.eval("var getter = { get x() { debugger; return log += 'x'; } }");
+check("getter.x");
+
+g.eval("var setter = { set x(v) { debugger; return log += 'x'; } }");
+check("setter.x = 1");
+
+g.eval("Object.defineProperty(this, 'thisgetter', { get: function() { debugger; log += 'x'; }});");
+check("thisgetter");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-07.js
@@ -0,0 +1,23 @@
+// The tracejit does not interfere with frame.onStep.
+//
+// The function f() writes 'L' to the log in a loop. If we enable stepping and
+// write an 's' each time frame.onStep is called, any two Ls should have at
+// least one 's' between them.
+
+var g = newGlobal('new-compartment');
+g.N = RUNLOOP + 2;
+g.log = '';
+g.eval("function f() {\n" +
+       "    for (var i = 0; i <= N; i++)\n" +
+       "        log += 'L';\n" +
+       "}\n");
+g.f();
+assertEq(/LL/.exec(g.log) !== null, true);
+
+var dbg = Debugger(g);
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () { g.log += 's'; };
+};
+g.log = '';
+g.f();
+assertEq(/LL/.exec(g.log), null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-08.js
@@ -0,0 +1,29 @@
+// frame.onStep can coexist with breakpoints.
+
+var g = newGlobal('new-compartment');
+var dbg = Debugger(g);
+var log = '';
+dbg.onEnterFrame = function (frame) {
+    var handler = {hit: function () { log += 'B'; }};
+    var lines = frame.script.getAllOffsets();
+    for (var line in lines) {
+        line = Number(line);
+        var offs = lines[line];
+        for (var i = 0; i < offs.length; i++)
+            frame.script.setBreakpoint(offs[i], handler);
+    }
+
+    frame.onStep = function () { log += 's'; };
+};
+
+g.eval("one = 1;\n" +
+       "two = 2;\n" +
+       "three = 3;\n" +
+       "four = 4;\n");
+assertEq(g.four, 4);
+
+// Breakpoints hit on all four lines.
+assertEq(log.replace(/[^B]/g, ''), 'BBBB');
+
+// onStep was called between each pair of breakpoints.
+assertEq(/BB/.exec(log), null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-09.js
@@ -0,0 +1,24 @@
+// After an implicit toString call throws an exception, the calling frame's
+// onStep hook fires.
+
+var g = newGlobal('new-compartment');
+g.eval("var x = {toString: function () { debugger; log += 'x'; throw 'mud'; }};");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    g.log += 'd';
+    frame.older.onStep = function () {
+        if (!g.log.match(/[sy]$/))
+            g.log += 's';
+    };
+};
+
+g.log = '';
+g.eval("try { x + ''; } catch (x) { }\n" +
+       "log += 'y';\n");
+assertEq(g.log, "dxsy");
+
+g.log = '';
+g.eval("try { '' + x; } catch (x) { }\n" +
+       "log += 'y';\n");
+assertEq(g.log, "dxsy");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-10.js
@@ -0,0 +1,28 @@
+// Throwing and catching an error in an onStep handler shouldn't interfere
+// with throwing and catching in the debuggee.
+
+var g = newGlobal('new-compartment');
+g.eval("function f() { debugger; throw 'mud'; }");
+
+var dbg = Debugger(g);
+var stepped = false;
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () {
+        stepped = true;
+        try {
+            throw 'snow';
+        } catch (x) {
+            assertEq(x, 'snow');
+        }
+    };
+};
+
+stepped = false;
+g.eval("var caught;\n" +
+       "try {\n" +
+       "    f();\n" +
+       "} catch (x) {\n" +
+       "    caught = x;\n" +
+       "}\n");
+assertEq(stepped, true);
+assertEq(g.caught, 'mud');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-lines-01.js
@@ -0,0 +1,78 @@
+// Test that a frame's onStep handler gets called at least once on each line of a function.
+
+var g = newGlobal('new-compartment');
+var dbg = new Debugger(g);
+
+// When we hit a 'debugger' statement, set offsets to the frame's script's
+// table of line offsets --- a sparse array indexed by line number. Begin
+// single-stepping the current frame; for each source line we hit, delete
+// the line's entry in offsets. Thus, at the end, offsets is an array with
+// an element for each line we did not reach.
+var doSingleStep = true;
+var offsets;
+dbg.onDebuggerStatement = function (frame) {
+    var script = frame.script;
+    offsets = script.getAllOffsets();
+    print("debugger line: " + script.getOffsetLine(frame.offset));
+    print("original lines: " + uneval(Object.keys(offsets)));
+    if (doSingleStep) {
+	frame.onStep = function onStepHandler() {
+	    var line = script.getOffsetLine(this.offset);
+	    delete offsets[line];
+	};
+    }
+};
+
+g.eval(
+       'function t(a, b, c) {                \n' +
+       '    debugger;                        \n' +
+       '    var x = a;                       \n' +
+       '    x += b;                          \n' +
+       '    if (x < 10)                      \n' +
+       '        x -= c;                      \n' +
+       '    return x;                        \n' +
+       '}                                    \n'
+       );
+
+// This should stop at every line but the first of the function.
+g.eval('t(1,2,3)');
+assertEq(Object.keys(offsets).length, 1);
+
+// This should stop at every line but the first of the function, and the
+// body of the 'if'.
+g.eval('t(10,20,30)');
+assertEq(Object.keys(offsets).length, 2);
+
+// This shouldn't stop at all. It's the frame that's in single-step mode,
+// not the script, so the prior execution of t in single-step mode should
+// have no effect on this one.
+doSingleStep = false;
+g.eval('t(0, 0, 0)');
+assertEq(Object.keys(offsets).length, 6);
+doSingleStep = true;
+
+// Single-step in an eval frame. This should reach every line but the
+// first.
+g.eval(
+       'debugger;                        \n' +
+       'var a=1, b=2, c=3;               \n' +
+       'var x = a;                       \n' +
+       'x += b;                          \n' +
+       'if (x < 10)                      \n' +
+       '    x -= c;                      \n'
+       );
+print("final lines: " + uneval(Object.keys(offsets)));
+assertEq(Object.keys(offsets).length, 1);
+
+// Single-step in a global code frame. This should reach every line but the
+// first.
+g.evaluate(
+           'debugger;                        \n' +
+           'var a=1, b=2, c=3;               \n' +
+           'var x = a;                       \n' +
+           'x += b;                          \n' +
+           'if (x < 10)                      \n' +
+           '    x -= c;                      \n'
+           );
+print("final lines: " + uneval(Object.keys(offsets)));
+assertEq(Object.keys(offsets).length, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js
@@ -0,0 +1,14 @@
+// If frame.onStep returns {return:val}, the frame returns.
+
+var g = newGlobal('new-compartment');
+g.eval("function f(x) {\n" +
+       "    var a = x * x;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () { return {return: "pass"}; };
+};
+
+assertEq(g.f(4), "pass");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js
@@ -0,0 +1,17 @@
+// If frame.onStep returns {throw:}, an exception is thrown in the debuggee.
+
+load(libdir + "asserts.js");
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }\n" +
+       "function f() {\n" +
+       "    h();\n" +
+       "    return 'fail';\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () { return {throw: "pass"}; };
+};
+
+assertThrowsValue(g.f, "pass");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js
@@ -0,0 +1,19 @@
+// If frame.onStep returns null, the debuggee terminates.
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }");
+
+var dbg = Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    hits++;
+    if (hits == 1) {
+        var rv = frame.eval("h();\n" +
+                            "throw 'fail';\n");
+        assertEq(rv, null);
+    } else {
+        frame.older.onStep = function () { return null; };
+    }
+};
+g.h();
+assertEq(hits, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js
@@ -0,0 +1,31 @@
+// If frame.onStep returns null, debuggee catch and finally blocks are skipped.
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }");
+
+var dbg = Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    hits++;
+    if (hits == 1) {
+        var rv = frame.eval("try {\n" +
+                            "    h();\n" +
+                            "    throw 'fail';\n" +
+                            "} catch (exc) {\n" +
+                            "    caught = exc;\n" +
+                            "} finally {\n" +
+                            "    finallyHit = true;\n" +
+                            "}\n");
+        assertEq(rv, null);
+    } else {
+        frame.older.onStep = function () {
+            this.onStep = undefined;
+            return null;
+        };
+    }
+};
+
+g.h();
+assertEq(hits, 2);
+assertEq("caught" in g, false);
+assertEq("finallyHit" in g, false);
--- a/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js
+++ b/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js
@@ -1,16 +1,16 @@
 // Don't crash when a scripted proxy handler throws Error.prototype.
 
 var g = newGlobal('new-compartment');
 var dbg = Debugger(g);
 dbg.onDebuggerStatement = function (frame) {
     try {
-	frame.arguments[0].deleteProperty("x");
+        frame.arguments[0].deleteProperty("x");
     } catch (exc) {
-	return;
+        return;
     }
     throw new Error("deleteProperty should throw");
 };
 
 g.eval("function h(x) { debugger; }");
 g.eval("h(Proxy.create({delete: function () { throw Error.prototype; }}));");
 
--- a/js/src/jit-test/tests/debug/Object-seal-01.js
+++ b/js/src/jit-test/tests/debug/Object-seal-01.js
@@ -22,24 +22,24 @@ g.eval("function compareObjects() {\n" +
 
 function test(code) {
     g.code = code;
     g.eval("x = (" + code + ");");
     g.eval("y = (" + code + ");");
     var xw = gw.getOwnPropertyDescriptor("x").value;
 
     function check() {
-	// The Debugger.Object seal/freeze/preventExtensions methods
-	// had the same effect as the corresponding ES5 Object methods.
-	g.compareObjects();
+        // The Debugger.Object seal/freeze/preventExtensions methods
+        // had the same effect as the corresponding ES5 Object methods.
+        g.compareObjects();
 
-	// The Debugger.Object introspection methods agree with the ES5 Object methods.
-	assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
-	assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
-	assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
+        // The Debugger.Object introspection methods agree with the ES5 Object methods.
+        assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
+        assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
+        assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
     }
 
     check();
 
     xw.preventExtensions();
     assertEq(g.Object.isExtensible(g.x), false, code + ' preventExtensions');
     g.Object.preventExtensions(g.y);
     check();
--- a/js/src/jit-test/tests/debug/onExceptionUnwind-07.js
+++ b/js/src/jit-test/tests/debug/onExceptionUnwind-07.js
@@ -1,15 +1,15 @@
 // Unwinding due to uncatchable errors does not trigger onExceptionUnwind.
 
 var g = newGlobal('new-compartment');
 var dbg = Debugger(g);
 var hits = 0;
 dbg.onExceptionUnwind = function (frame, value) { hits = 'BAD'; };
 dbg.onDebuggerStatement = function (frame) {
     if (hits++ === 0)
-	assertEq(frame.eval("debugger;"), null);
+        assertEq(frame.eval("debugger;"), null);
     else
-	return null;
+        return null;
 }
 
 assertEq(g.eval("debugger; 2"), 2);
 assertEq(hits, 2);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -365,8 +365,9 @@ MSG_DEF(JSMSG_DEBUG_OBJECT_WRONG_OWNER, 
 MSG_DEF(JSMSG_DEBUG_OBJECT_PROTO,     279, 0, JSEXN_TYPEERR, "Debugger.Object.prototype is not a valid Debugger.Object")
 MSG_DEF(JSMSG_DEBUG_LOOP,             280, 0, JSEXN_TYPEERR, "cannot debug an object in same compartment as debugger or a compartment that is already debugging the debugger")
 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_NOT_CALLABLE_OR_UNDEFINED, 286, 0, JSEXN_TYPEERR, "value is not a function or undefined")
+MSG_DEF(JSMSG_DEBUG_NOT_SCRIPT_FRAME, 287, 0, JSEXN_ERR, "stack frame is not running JavaScript code")
--- a/js/src/jsinterp.cpp
+++ b/js/src/jsinterp.cpp
@@ -1881,22 +1881,23 @@ Interpret(JSContext *cx, StackFrame *ent
     JS_END_MACRO
 #endif
 #else
 #define MONITOR_BRANCH_TRACEVIS
 #endif
 
 #define RESTORE_INTERP_VARS()                                                 \
     JS_BEGIN_MACRO                                                            \
-        script = regs.fp()->script();                                         \
+        SET_SCRIPT(regs.fp()->script());                                      \
         argv = regs.fp()->maybeFormalArgs();                                  \
         atoms = FrameAtomBase(cx, regs.fp());                                 \
         JS_ASSERT(&cx->regs() == &regs);                                      \
         if (cx->isExceptionPending())                                         \
             goto error;                                                       \
+        CHECK_INTERRUPT_HANDLER();                                            \
     JS_END_MACRO
 
 #define MONITOR_BRANCH()                                                      \
     JS_BEGIN_MACRO                                                            \
         if (TRACING_ENABLED(cx)) {                                            \
             if (!TRACE_RECORDER(cx) && !TRACE_PROFILER(cx) && useMethodJIT) { \
                 MONITOR_BRANCH_METHODJIT();                                   \
             } else {                                                          \
@@ -1969,16 +1970,23 @@ Interpret(JSContext *cx, StackFrame *ent
                 MONITOR_BRANCH();                                             \
                 op = (JSOp) *regs.pc;                                         \
             }                                                                 \
         }                                                                     \
         LEAVE_ON_SAFE_POINT();                                                \
         DO_OP();                                                              \
     JS_END_MACRO
 
+#define SET_SCRIPT(s)                                                         \
+    JS_BEGIN_MACRO                                                            \
+        script = (s);                                                         \
+        if (script->stepModeEnabled())                                        \
+            ENABLE_INTERRUPTS();                                              \
+    JS_END_MACRO
+
 #define CHECK_INTERRUPT_HANDLER()                                             \
     JS_BEGIN_MACRO                                                            \
         if (cx->debugHooks->interruptHook)                                    \
             ENABLE_INTERRUPTS();                                              \
     JS_END_MACRO
 
     FrameRegs regs = cx->regs();
 
@@ -2001,17 +2009,18 @@ Interpret(JSContext *cx, StackFrame *ent
             JS_ASSERT(&cx->regs() == &regs);
             *prevContextRegs = regs;
             cx->stack.repointRegs(prevContextRegs);
         }
     } interpGuard(cx, regs);
 
     /* Copy in hot values that change infrequently. */
     JSRuntime *const rt = cx->runtime;
-    JSScript *script = regs.fp()->script();
+    JSScript *script;
+    SET_SCRIPT(regs.fp()->script());
     int *pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
     Value *argv = regs.fp()->maybeFormalArgs();
     CHECK_INTERRUPT_HANDLER();
 
 #if defined(JS_TRACER) && defined(JS_METHODJIT)
     bool leaveOnSafePoint = (interpMode == JSINTERP_SAFEPOINT);
 # define CLEAR_LEAVE_ON_TRACE_POINT() ((void) (leaveOnSafePoint = false))
 #else
@@ -2146,28 +2155,34 @@ Interpret(JSContext *cx, StackFrame *ent
   interrupt:
 #else /* !JS_THREADED_INTERP */
   case -1:
     JS_ASSERT(switchMask == -1);
 #endif /* !JS_THREADED_INTERP */
     {
         bool moreInterrupts = false;
         JSInterruptHook hook = cx->debugHooks->interruptHook;
-        if (hook) {
+        if (hook || script->stepModeEnabled()) {
 #ifdef JS_TRACER
             if (TRACE_RECORDER(cx))
-                AbortRecording(cx, "interrupt hook");
+                AbortRecording(cx, "interrupt hook or singleStepMode");
 #ifdef JS_METHODJIT
             if (TRACE_PROFILER(cx))
                 AbortProfiling(cx);
 #endif
 #endif
             Value rval;
-            switch (hook(cx, script, regs.pc, Jsvalify(&rval),
-                         cx->debugHooks->interruptHookData)) {
+            JSTrapStatus status = JSTRAP_CONTINUE;
+            if (hook) {
+                status = hook(cx, script, regs.pc, Jsvalify(&rval),
+                              cx->debugHooks->interruptHookData);
+            }
+            if (status == JSTRAP_CONTINUE && script->stepModeEnabled())
+                status = Debugger::onSingleStep(cx, &rval);
+            switch (status) {
               case JSTRAP_ERROR:
                 goto error;
               case JSTRAP_CONTINUE:
                 break;
               case JSTRAP_RETURN:
                 regs.fp()->setReturnValue(rval);
                 interpReturnOK = JS_TRUE;
                 goto forced_return;
@@ -2376,29 +2391,29 @@ BEGIN_CASE(JSOP_STOP)
 
     interpReturnOK = true;
     if (entryFrame != regs.fp())
   inline_return:
     {
         JS_ASSERT(!regs.fp()->hasImacropc());
         JS_ASSERT(!js_IsActiveWithOrBlock(cx, &regs.fp()->scopeChain(), 0));
         interpReturnOK = ScriptEpilogue(cx, regs.fp(), interpReturnOK);
-        CHECK_INTERRUPT_HANDLER();
 
         /* The JIT inlines ScriptEpilogue. */
 #ifdef JS_METHODJIT
   jit_return:
 #endif
         cx->stack.popInlineFrame(regs);
 
         /* Sync interpreter locals. */
-        script = regs.fp()->script();
+        SET_SCRIPT(regs.fp()->script());
         pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
         argv = regs.fp()->maybeFormalArgs();
         atoms = FrameAtomBase(cx, regs.fp());
+        CHECK_INTERRUPT_HANDLER();
 
         JS_ASSERT(*regs.pc == JSOP_TRAP || *regs.pc == JSOP_NEW || *regs.pc == JSOP_CALL ||
                   *regs.pc == JSOP_FUNCALL || *regs.pc == JSOP_FUNAPPLY);
 
         /* Resume execution in the calling frame. */
         RESET_USE_METHODJIT();
         if (JS_LIKELY(interpReturnOK)) {
             TRACE_0(LeaveFrame);
@@ -4055,17 +4070,17 @@ BEGIN_CASE(JSOP_FUNAPPLY)
         DO_NEXT_OP(len);
     }
 
     JSScript *newScript = fun->script();
     if (!cx->stack.pushInlineFrame(cx, regs, args, *callee, fun, newScript, construct))
         goto error;
 
     /* Refresh local js::Interpret state. */
-    script = newScript;
+    SET_SCRIPT(newScript);
     pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
     argv = regs.fp()->formalArgsEnd() - fun->nargs;
     atoms = script->atomMap.vector;
 
     /* Only create call object after frame is rooted. */
     if (fun->isHeavyweight() && !CreateFunCallObject(cx, regs.fp()))
         goto error;
 
@@ -4078,17 +4093,16 @@ BEGIN_CASE(JSOP_FUNAPPLY)
         mjit::CompileRequest request = (interpMode == JSINTERP_NORMAL)
                                        ? mjit::CompileRequest_Interpreter
                                        : mjit::CompileRequest_JIT;
         mjit::CompileStatus status = mjit::CanMethodJIT(cx, script, regs.fp(), request);
         if (status == mjit::Compile_Error)
             goto error;
         if (!TRACE_RECORDER(cx) && !TRACE_PROFILER(cx) && status == mjit::Compile_Okay) {
             interpReturnOK = mjit::JaegerShot(cx);
-            CHECK_INTERRUPT_HANDLER();
             goto jit_return;
         }
     }
 #endif
 
     if (!ScriptPrologue(cx, regs.fp()))
         goto error;
 
--- a/js/src/methodjit/StubCalls.cpp
+++ b/js/src/methodjit/StubCalls.cpp
@@ -1188,16 +1188,19 @@ stubs::Trap(VMFrame &f, uint32 trapTypes
         /*
          * single step mode may be paused without recompiling by
          * setting the interruptHook to NULL.
          */
         JSInterruptHook hook = f.cx->debugHooks->interruptHook;
         if (hook)
             result = hook(f.cx, f.cx->fp()->script(), pc, Jsvalify(&rval),
                           f.cx->debugHooks->interruptHookData);
+
+        if (result == JSTRAP_CONTINUE)
+            result = Debugger::onSingleStep(f.cx, &rval);
     }
 
     if (result == JSTRAP_CONTINUE && (trapTypes & JSTRAP_TRAP))
         result = Debugger::onTrap(f.cx, &rval);
 
     switch (result) {
       case JSTRAP_THROW:
         f.cx->setPendingException(rval);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -58,16 +58,17 @@ using namespace js;
 
 /*** Forward declarations ************************************************************************/
 
 extern Class DebuggerFrame_class;
 
 enum {
     JSSLOT_DEBUGFRAME_OWNER,
     JSSLOT_DEBUGFRAME_ARGUMENTS,
+    JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
     JSSLOT_DEBUGFRAME_COUNT
 };
 
 extern Class DebuggerArguments_class;
 
 enum {
     JSSLOT_DEBUGARGUMENTS_FRAME,
     JSSLOT_DEBUGARGUMENTS_COUNT
@@ -383,21 +384,34 @@ Debugger::getHook(Hook hook) const
     JS_ASSERT(hook >= 0 && hook < HookCount);
     const Value &v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + hook);
     return v.isUndefined() ? NULL : &v.toObject();
 }
 
 bool
 Debugger::hasAnyLiveHooks() const
 {
-    return enabled && (getHook(OnDebuggerStatement) ||
-                       getHook(OnExceptionUnwind) ||
-                       getHook(OnNewScript) ||
-                       getHook(OnEnterFrame) ||
-                       !JS_CLIST_IS_EMPTY(&breakpoints));
+    if (!enabled)
+        return false;
+
+    if (getHook(OnDebuggerStatement) ||
+        getHook(OnExceptionUnwind) ||
+        getHook(OnNewScript) ||
+        getHook(OnEnterFrame))
+        return true;
+
+    if (!JS_CLIST_IS_EMPTY(&breakpoints))
+        return true;
+
+    for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
+        if (!r.front().value->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
+            return true;
+    }
+
+    return false;
 }
 
 void
 Debugger::slowPathOnEnterFrame(JSContext *cx)
 {
     /* Build the list of recipients. */
     AutoValueVector triggered(cx);
     GlobalObject *global = cx->fp()->scopeChain().getGlobal();
@@ -428,18 +442,26 @@ Debugger::slowPathOnLeaveFrame(JSContext
      * FIXME This notifies only current debuggers, so it relies on a hack in
      * Debugger::removeDebuggeeGlobal to make sure only current debuggers have
      * Frame objects with .live === true.
      */
     if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
         for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
             Debugger *dbg = *p;
             if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
+                StackFrame *frame = p->key;
                 JSObject *frameobj = p->value;
                 frameobj->setPrivate(NULL);
+
+                /* If this frame had an onStep handler, adjust the script's count. */
+                if (!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
+                    frame->isScriptFrame()) {
+                    frame->script()->changeStepModeCount(cx, -1);
+                }
+
                 dbg->frames.remove(p);
             }
         }
     }
 
     /*
      * If this is an eval frame, then from the debugger's perspective the
      * script is about to be destroyed. Remove any breakpoints in it.
@@ -873,16 +895,111 @@ Debugger::onTrap(JSContext *cx, Value *v
             return st;
     }
 
     /* By convention, return the true op to the interpreter in vp. */
     vp->setInt32(op);
     return JSTRAP_CONTINUE;
 }
 
+JSTrapStatus
+Debugger::onSingleStep(JSContext *cx, Value *vp)
+{
+    StackFrame *fp = cx->fp();
+
+    /*
+     * We may be stepping over a JSOP_EXCEPTION, that pushes the context's
+     * pending exception for a 'catch' clause to handle. Don't let the
+     * onStep handlers mess with that (other than by returning a resumption
+     * value).
+     */
+    Value exception = UndefinedValue();
+    bool exceptionPending = cx->isExceptionPending();
+    if (exceptionPending) {
+        exception = cx->getPendingException();
+        cx->clearPendingException();
+    }
+
+    /* We should only receive single-step traps for scripted frames. */
+    JS_ASSERT(fp->isScriptFrame());
+
+    /*
+     * Build list of Debugger.Frame instances referring to this frame with
+     * onStep handlers.
+     */
+    AutoObjectVector frames(cx);
+    GlobalObject *global = fp->scopeChain().getGlobal();
+    if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
+        for (Debugger **d = debuggers->begin(); d != debuggers->end(); d++) {
+            Debugger *dbg = *d;
+            if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
+                JSObject *frame = p->value;
+                if (!frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
+                    !frames.append(frame))
+                    return JSTRAP_ERROR;
+            }
+        }
+    }
+
+#ifdef DEBUG
+    /*
+     * Validate the single-step count on this frame's script, to ensure that
+     * we're not receiving traps we didn't ask for. Even when frames is
+     * non-empty (and thus we know this trap was requested), do the check
+     * anyway, to make sure the count has the correct non-zero value.
+     *
+     * The converse --- ensuring that we do receive traps when we should --- can
+     * be done with unit tests.
+     */
+    {
+        uint32 stepperCount = 0;
+        JSScript *trappingScript = fp->script();
+        if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
+            for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
+                Debugger *dbg = *p;
+                for (FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) {
+                    StackFrame *frame = r.front().key;
+                    JSObject *frameobj = r.front().value;
+                    if (frame->isScriptFrame() &&
+                        frame->script() == trappingScript &&
+                        !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
+                    {
+                        stepperCount++;
+                    }
+                }
+            }
+        }
+        if (trappingScript->compileAndGo)
+            JS_ASSERT(stepperCount == trappingScript->stepModeCount());
+        else
+            JS_ASSERT(stepperCount <= trappingScript->stepModeCount());
+    }
+#endif
+
+    /* Call all the onStep handlers we found. */
+    for (JSObject **p = frames.begin(); p != frames.end(); p++) {
+        JSObject *frame = *p;
+        Debugger *dbg = Debugger::fromChildJSObject(frame);
+        AutoCompartment ac(cx, dbg->object);
+        if (!ac.enter())
+            return JSTRAP_ERROR;
+        const Value &handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+        Value rval;
+        bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, NULL, &rval);
+        JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rval, vp);
+        if (st != JSTRAP_CONTINUE)
+            return st;
+    }
+
+    vp->setUndefined();
+    if (exceptionPending)
+        cx->setPendingException(exception);
+    return JSTRAP_CONTINUE;
+}
+
 
 /*** Debugger JSObjects **************************************************************************/
 
 void
 Debugger::markKeysInCompartment(JSTracer *tracer, const ObjectWeakMap &map)
 {
     JSCompartment *comp = tracer->context->runtime->gcCurrentCompartment;
     JS_ASSERT(comp);
@@ -1022,18 +1139,22 @@ Debugger::traceObject(JSTracer *trc, JSO
 
 void
 Debugger::trace(JSTracer *trc)
 {
     if (uncaughtExceptionHook)
         MarkObject(trc, *uncaughtExceptionHook, "hooks");
 
     /*
-     * Mark Debugger.Frame objects that are reachable from JS if we look them up
-     * again (because the corresponding StackFrame is still on the stack).
+     * Mark Debugger.Frame objects. These are all reachable from JS, because the
+     * corresponding StackFrames are still on the stack.
+     *
+     * (Once we support generator frames properly, we will need
+     * weakly-referenced Debugger.Frame objects as well, for suspended generator
+     * frames.)
      */
     for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
         JSObject *frameobj = r.front().value;
         JS_ASSERT(frameobj->getPrivate());
         MarkObject(trc, *frameobj, "live Debugger.Frame");
     }
 
     /* Trace the referent -> Debugger.Object weak map. */
@@ -2645,16 +2766,64 @@ DebuggerFrame_getLive(JSContext *cx, uin
     JSObject *thisobj = CheckThisFrame(cx, args, "get live", false);
     if (!thisobj)
         return false;
     StackFrame *fp = (StackFrame *) thisobj->getPrivate();
     args.rval().setBoolean(!!fp);
     return true;
 }
 
+static bool
+IsValidHook(const Value &v)
+{
+    return v.isUndefined() || (v.isObject() && v.toObject().isCallable());
+}
+
+static JSBool
+DebuggerFrame_getOnStep(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_FRAME(cx, argc, vp, "get onStep", args, thisobj, fp);
+    (void) fp;  // Silence GCC warning
+    Value handler = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+    JS_ASSERT(IsValidHook(handler));
+    args.rval() = handler;
+    return true;
+}
+
+static JSBool
+DebuggerFrame_setOnStep(JSContext *cx, uintN argc, Value *vp)
+{
+    REQUIRE_ARGC("Debugger.Frame.set onStep", 1);
+    THIS_FRAME(cx, argc, vp, "set onStep", args, thisobj, fp);
+    if (!fp->isScriptFrame()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_SCRIPT_FRAME);
+        return false;
+    }
+    if (!IsValidHook(args[0])) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_CALLABLE_OR_UNDEFINED);
+        return false;
+    }
+
+    Value prior = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+    int delta = !args[0].isUndefined() - !prior.isUndefined();
+    if (delta != 0) {
+        /* Try to adjust this frame's script single-step mode count. */
+        AutoCompartment ac(cx, &fp->scopeChain());
+        if (!ac.enter())
+            return false;
+        if (!fp->script()->changeStepModeCount(cx, delta))
+            return false;
+    }
+
+    /* Now that the step mode switch has succeeded, we can install the handler. */
+    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)
 {
     assertSameCompartment(cx, scobj, fp);
 
@@ -2790,16 +2959,17 @@ static JSPropertySpec DebuggerFrame_prop
     JS_PSG("constructing", DebuggerFrame_getConstructing, 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),
     JS_PS_END
 };
 
 static JSFunctionSpec DebuggerFrame_methods[] = {
     JS_FN("eval", DebuggerFrame_eval, 1, 0),
     JS_FN("evalWithBindings", DebuggerFrame_evalWithBindings, 1, 0),
     JS_FS_END
 };
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -301,16 +301,17 @@ class Debugger {
     static inline void onEnterFrame(JSContext *cx);
     static inline void onLeaveFrame(JSContext *cx);
     static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp);
     static inline JSTrapStatus onExceptionUnwind(JSContext *cx, js::Value *vp);
     static inline void onNewScript(JSContext *cx, JSScript *script, JSObject *obj,
                                    NewScriptKind kind);
     static inline void onDestroyScript(JSScript *script);
     static JSTrapStatus onTrap(JSContext *cx, Value *vp);
+    static JSTrapStatus onSingleStep(JSContext *cx, Value *vp);
 
     /************************************* Functions for use by Debugger.cpp. */
 
     inline bool observesEnterFrame() const;
     inline bool observesNewScript() const;
     inline bool observesScope(JSObject *obj) const;
     inline bool observesFrame(StackFrame *fp) const;