Bug 673125: Implement the Debugger.Frame.prototype.onStep accessor. r=jorendorff.
☠☠ backed out by 7d5ebc4ed8b3 ☠ ☠
authorJim Blandy <jimb@mozilla.com>
Tue, 23 Aug 2011 14:45:36 -0500
changeset 75767 4a86c9387193773d6ce52a10946ca94dc14b7149
parent 75766 331146c93ebedb27babefef5321f33bef6806830
child 75768 5bfb2f2d1464fa30517b87872f505c06a6657696
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
reviewersjorendorff
bugs673125
milestone9.0a1
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/onEnterFrame-04.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,72 @@
+// 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("var fgetter = { get f() { debugger; log += 'x'; return function() { log += 'z'; }; } }");
+g.log = '';
+g.eval("fgetter.f();\n" +
+       "log += 'y';\n");
+assertEq(g.log, 'dxszsy');
+
+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("dis(); 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;
+    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,63 @@ 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);
+    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 +2958,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;