// Check that stepping doesn't make it look like unreachable code is running. // Because our script source notes record only those bytecode offsets // at which source positions change, the default behavior in the // absence of a source note is to attribute a bytecode instruction to // the same source location as the preceding instruction. When control // flows from the preceding bytecode to the one we're emitting, that's // usually plausible. But successors in the bytecode stream are not // necessarily successors in the control flow graph. If the preceding // bytecode was a back edge of a loop, or the jump at the end of a // 'then' clause, its source position can be completely unrelated to // that of its successor. // // We try to avoid showing such nonsense offsets to the user by // requiring breakpoints and single-stepping to stop only at a line's // entry points, as reported by Debugger.Script.prototype.getLineOffsets; // and then ensuring that those entry points are all offsets mentioned // explicitly in the source notes, and hence deliberately attributed // to the given bytecode. // // This bit of JavaScript compiles to bytecode ending in a branch // instruction whose source position is the body of an unreachable // loop. The first instruction of the bytecode we emit following it // will inherit this nonsense position, if we have not explicitly // emitted a source note for said instruction. // // This test steps across such code and verifies that control never // appears to enter the unreachable loop. var bitOfCode = `debugger; // +0 if(false) { // +1 for(var b=0; b<0; b++) { // +2 c = 2 // +3 } // +4 }`; // +5 var g = newGlobal({newCompartment: true}); var dbg = Debugger(g); g.eval("function nothing() { }\n"); var log = ''; dbg.onDebuggerStatement = function(frame) { let debugLine = frame.script.getOffsetLocation(frame.offset).lineNumber; frame.onStep = function() { let foundLine = this.script.getOffsetLocation(this.offset).lineNumber; if (this.script.getLineOffsets(foundLine).indexOf(this.offset) >= 0) { log += (foundLine - debugLine).toString(16); } }; }; function testOne(name, body, expected) { print(name); log = ''; g.eval(`function ${name} () { ${body} }`); g.eval(`${name}();`); assertEq(log, expected); } // Test the instructions at the end of a "try". testOne("testTryFinally", `try { ${bitOfCode} } finally { // +6 } // +7 nothing(); // +8 `, "1689"); // The same but without a finally clause. testOne("testTryCatch", `try { ${bitOfCode} } catch (e) { // +6 } // +7 nothing(); // +8 `, "189"); // Test the instructions at the end of a "catch". testOne("testCatchFinally", `try { throw new TypeError(); } catch (e) { ${bitOfCode} } finally { // +6 } // +7 nothing(); // +8 `, "1689"); // Test the instruction at the end of a "finally" clause. testOne("testFinally", `try { } finally { ${bitOfCode} } // +6 nothing(); // +7 `, "178"); // Test the instruction at the end of a "then" clause. testOne("testThen", `if (1 === 1) { ${bitOfCode} } else { // +6 } // +7 nothing(); // +8 `, "189"); // Test the instructions leaving a switch block. testOne("testSwitch", `var x = 5; switch (x) { case 5: ${bitOfCode} } // +6 nothing(); // +7 `, "178");