Bug 1475417 - Part 2: Fire onEnterFrame when resuming a generator or async function. r=jandem, r=jimb

This commit is contained in:
Jason Orendorff 2018-08-15 15:09:30 -05:00
Родитель 5cd444cdaa
Коммит 071656e082
20 изменённых файлов: 570 добавлений и 14 удалений

Просмотреть файл

@ -0,0 +1,53 @@
// frame.live is false for generator frames popped due to exception or termination.
load(libdir + "/asserts.js");
function test(when, what) {
let g = newGlobal();
g.eval("function* f(x) { yield x; }");
let dbg = new Debugger;
let gw = dbg.addDebuggee(g);
let fw = gw.getOwnPropertyDescriptor("f").value;
let t = 0;
let poppedFrame;
function tick(frame) {
if (frame.callee == fw) {
if (t == when) {
poppedFrame = frame;
dbg.onEnterFrame = undefined;
frame.onPop = undefined;
return what;
}
t++;
}
return undefined;
}
dbg.onDebuggerStatement = frame => {
dbg.onEnterFrame = frame => {
frame.onPop = function() {
return tick(this);
};
return tick(frame);
};
let result = frame.eval("for (let _ of f(0)) {}");
assertDeepEq(result, what);
};
g.eval("debugger;");
assertEq(t, when);
assertEq(poppedFrame.live, false);
assertErrorMessage(() => poppedFrame.older,
Error,
"Debugger.Frame is not live");
}
for (let when = 0; when < 6; when++) {
for (let what of [null, {throw: "fit"}]) {
test(when, what);
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Stepping into the `.next()` method of a generator works as expected.
let g = newGlobal();
g.eval(`\
function* nums() { // line 1
yield 1; // 2
yield 2; // 3
} // 4
function f() { // 5
let gen = nums(); // 6
gen.next(); // 7
gen.next(); // 8
gen.next(); // 9
} // 10
`);
let log = [];
let previousLine = -1;
let dbg = new Debugger(g);
dbg.onEnterFrame = frame => {
frame.onStep = () => {
let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
if (previousLine != line) { // We stepped to a new line.
log.push(line);
previousLine = line;
}
};
};
g.f();
assertEq(log.join(" "), "5 6 1 6 7 1 2 7 8 2 3 8 9 3 9 10");

Просмотреть файл

@ -0,0 +1,44 @@
// Stepping into the `.throw()` method of a generator with no relevant catch block.
//
// The debugger fires onEnterFrame and then frame.onPop for the generator frame when
// `gen.throw()` is called.
load(libdir + "asserts.js");
let g = newGlobal();
g.eval(`\
function* z() { // line 1
yield 1; // 2
yield 2; // 3
} // 4
function f() { // 5
let gen = z(); // 6
gen.next(); // 7
gen.throw("fit"); // 8
} // 9
`);
let log = "";
let previousLine = -1;
let dbg = new Debugger(g);
dbg.onEnterFrame = frame => {
log += frame.callee.name + "{";
frame.onStep = () => {
let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
if (previousLine != line) { // We stepped to a new line.
log += line;
previousLine = line;
}
};
frame.onPop = completion => {
if ("throw" in completion)
log += "!";
log += "}";
}
};
assertThrowsValue(() => g.f(), "fit");
// z{1} is the initial generator setup.
// z{12} is the first .next() call, running to `yield 1` on line 2
// The final `z{!}` is for the .throw() call.
assertEq(log, "f{56z{1}67z{12}78z{!}!}");

Просмотреть файл

@ -0,0 +1,45 @@
// Stepping into the `.throw()` method of a generator with a relevant catch block.
load(libdir + "asserts.js");
let g = newGlobal();
g.eval(`\
function* z() { // line 1
try { // 2
yield 1; // 3
} catch (exc) { // 4
yield 2; // 5
} // 6
} // 7
function f() { // 8
let gen = z(); // 9
gen.next(); // 10
gen.throw("fit"); // 11
} // 12
`);
let log = [];
let previousLine = -1;
let dbg = new Debugger(g);
dbg.onEnterFrame = frame => {
log.push(frame.callee.name + " in");
frame.onStep = () => {
let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
if (previousLine != line) { // We stepped to a new line.
log.push(line);
previousLine = line;
}
};
frame.onPop = completion => {
log.push(frame.callee.name + " out");
};
};
g.f();
assertEq(
log.join(", "),
"f in, 8, 9, z in, 1, z out, " +
"9, 10, z in, 1, 2, 3, z out, " +
"10, 11, z in, 2, 4, 5, z out, " + // not sure why we hit line 2 here, source notes bug maybe
"11, 12, f out"
);

Просмотреть файл

@ -0,0 +1,36 @@
// A Debugger can {throw:} from onEnterFrame in an async function.
// The resulting promise (if any) is rejected with the thrown error value.
load(libdir + "asserts.js");
let g = newGlobal();
g.eval(`
async function f() { await 1; }
var err = new TypeError("object too hairy");
`);
let dbg = new Debugger;
let gw = dbg.addDebuggee(g);
let errw = gw.makeDebuggeeValue(g.err);
// Repeat the test for each onEnterFrame event.
// It fires up to three times:
// - when the async function g.f is called;
// - when we enter it to run to `await 1`;
// - when we resume after the await to run to the end.
for (let when = 0; when < 3; when++) {
let hits = 0;
dbg.onEnterFrame = frame => {
return hits++ < when ? undefined : {throw: errw};
};
let result = undefined;
g.f()
.then(value => { result = {returned: value}; })
.catch(err => { result = {threw: err}; });
drainJobQueue();
assertEq(hits, when + 1);
assertDeepEq(result, {threw: g.err});
}

Просмотреть файл

@ -0,0 +1,30 @@
// A Debugger can {return:} from onEnterFrame at any resume point in an async function.
// The async function's promise is resolved with the returned value.
let g = newGlobal();
g.eval(`async function f(x) { await x; }`);
let dbg = new Debugger(g);
function test(when) {
let hits = 0;
dbg.onEnterFrame = frame => {
if (frame.type == "call" && frame.callee.name === "f") {
if (hits++ == when) {
return {return: "exit"};
}
}
};
let result = undefined;
let finished = false;
g.f("hello").then(value => { result = value; finished = true; });
drainJobQueue();
assertEq(finished, true);
assertEq(hits, when + 1);
assertEq(result, "exit");
}
// onEnterFrame with hits==0 is not a resume point; {return:} behaves differently there
// (see onEnterFrame-async-resumption-02.js).
test(1); // force return from first resume point, immediately after the initial suspend
test(2); // force return from second resume point, immediately after the await instruction

Просмотреть файл

@ -0,0 +1,81 @@
// Frame properties and methods work in generator-resuming onEnterFrame events.
// Also tests onPop events, for good measure.
let g = newGlobal();
g.eval(`\
function* gen(lo, hi) {
var a = 1/2;
yield a;
yield a * a;
}
`);
let dbg = new Debugger;
let gw = dbg.addDebuggee(g);
let hits = 0;
let savedScript = null;
let savedEnv = null;
let savedOffsets = new Set;
function check(frame) {
assertEq(frame.type, "call");
assertEq(frame.constructing, false);
assertEq(frame.callee, gw.makeDebuggeeValue(g.gen));
// `arguments` elements don't work in resumed generator frames,
// because generators don't keep the arguments around.
// The first onEnterFrame and onPop events can see them.
assertEq(frame.arguments.length, hits < 2 ? args.length : 0);
for (var i = 0; i < frame.arguments.length; i++) {
assertEq(frame.arguments.hasOwnProperty(i), true);
if (hits < 2)
assertEq(frame.arguments[i], gw.makeDebuggeeValue(args[i]), `arguments[${i}]`);
else
assertEq(frame.arguments[i], undefined);
}
if (savedEnv === null) {
savedEnv = frame.environment;
assertEq(savedScript, null);
savedScript = frame.script;
} else {
assertEq(frame.environment, savedEnv);
assertEq(frame.script, savedScript);
}
let a_expected = hits < 3 ? undefined : 1/2;
assertEq(savedEnv.getVariable("a"), a_expected);
assertEq(frame.generator, true);
assertEq(frame.live, true);
let pc = frame.offset;
assertEq(savedOffsets.has(pc), false);
savedOffsets.add(pc);
assertEq(frame.older, null);
assertEq(frame.this, gw);
assertEq(typeof frame.implementation, "string");
// And the moment of truth:
assertEq(frame.eval("2 + 2").return, 4);
assertEq(frame.eval("a").return, a_expected);
assertEq(frame.eval("if (a !== undefined) { assertEq(a < (lo + hi) / 2, true); } 7;").return, 7);
}
dbg.onEnterFrame = frame => {
if (frame.type === "eval")
return;
check(frame);
hits++;
frame.onPop = completion => {
check(frame);
hits++;
};
};
// g.gen ignores the arguments passed to it, but we use them to test
// frame.arguments.
let args = [0, 10, g, dbg];
for (let v of g.gen(...args)) {}
assertEq(hits, 8);

Просмотреть файл

@ -0,0 +1,27 @@
// onEnterFrame fires after the [[GeneratorState]] is set to "executing".
//
// This test checks that Debugger doesn't accidentally make it possible to
// reenter a generator frame that's already on the stack. (Also tests a fun
// corner case in baseline debug-mode OSR.)
load(libdir + "asserts.js");
let g = newGlobal();
g.eval('function* f() { yield 1; yield 2; }');
let dbg = Debugger(g);
let genObj = null;
let hits = 0;
dbg.onEnterFrame = frame => {
// The first time onEnterFrame fires, there is no generator object, so
// there's nothing to test. The generator object doesn't exist until
// JSOP_GENERATOR is reached, right before the initial yield.
if (genObj !== null) {
dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception
assertThrowsInstanceOf(() => genObj.next(), g.TypeError);
dbg.addDebuggee(g);
hits++;
}
};
genObj = g.f();
for (let x of genObj) {}
assertEq(hits, 3);

Просмотреть файл

@ -0,0 +1,25 @@
// If onEnterFrame terminates a generator, the Frame is left in a sane but inactive state.
load(libdir + "asserts.js");
let g = newGlobal();
g.eval("function* f(x) { yield x; }");
let dbg = new Debugger;
let gw = dbg.addDebuggee(g);
let genFrame = null;
dbg.onDebuggerStatement = frame => {
dbg.onEnterFrame = frame => {
if (frame.callee == gw.getOwnPropertyDescriptor("f").value) {
genFrame = frame;
return null;
}
};
assertEq(frame.eval("f(0);"), null);
};
g.eval("debugger;");
assertEq(genFrame instanceof Debugger.Frame, true);
assertEq(genFrame.live, false);
assertThrowsInstanceOf(() => genFrame.callee, Error);

Просмотреть файл

@ -0,0 +1,36 @@
// A debugger can {throw:} from onEnterFrame at any resume point in a generator.
// It closes the generator.
load(libdir + "asserts.js");
let g = newGlobal();
g.eval(`
function* f() { yield 1; }
var exn = new TypeError("object too hairy");
`);
let dbg = new Debugger;
let gw = dbg.addDebuggee(g);
// Repeat the test for each onEnterFrame event.
// It fires up to three times:
// - when the generator g.f is called;
// - when we enter it to run to `yield 1`;
// - when we resume after the yield to run to the end.
for (let i = 0; i < 3; i++) {
let hits = 0;
dbg.onEnterFrame = frame => {
return hits++ < i ? undefined : {throw: gw.makeDebuggeeValue(g.exn)};
};
let genObj;
assertThrowsValue(
() => {
genObj = g.f();
for (let x of genObj) {}
},
g.exn
);
assertEq(hits, i + 1);
if (hits > 1)
assertEq(genObj.next().done, true);
}

Просмотреть файл

@ -0,0 +1,39 @@
// A Debugger can {return:} from onEnterFrame at any resume point in a generator.
// Force-returning closes the generator.
load(libdir + "asserts.js");
let g = newGlobal();
g.values = [1, 2, 3];
g.eval(`function* f() { yield* values; }`);
let dbg = Debugger(g);
// onEnterFrame will fire up to 5 times.
// - once for the initial call to g.f();
// - four times at resume points:
// - initial resume at the top of the generator body
// - resume after yielding 1
// - resume after yielding 2
// - resume after yielding 3 (this resumption will run to the end).
// This test ignores the initial call and focuses on resume points.
for (let i = 1; i < 5; i++) {
let hits = 0;
dbg.onEnterFrame = frame => {
return hits++ < i ? undefined : {return: "we're done here"};
};
let genObj = g.f();
let actual = [];
while (true) {
let r = genObj.next();
if (r.done) {
assertDeepEq(r, {value: "we're done here", done: true});
break;
}
actual.push(r.value);
}
assertEq(hits, i + 1);
assertDeepEq(actual, g.values.slice(0, i - 1));
assertDeepEq(genObj.next(), {value: undefined, done: true});
}

Просмотреть файл

@ -0,0 +1,35 @@
// Returning {throw:} from onEnterFrame when resuming inside a try block in a
// generator causes control to jump to the catch block.
let g = newGlobal();
g.eval(`
function* gen() {
try {
yield 0;
return "fail";
} catch (exc) {
assertEq(exc, "fit");
return "ok";
}
}
`)
let dbg = new Debugger(g);
let hits = 0;
dbg.onEnterFrame = frame => {
assertEq(frame.callee.name, "gen");
if (++hits == 3) {
// First hit is when calling gen();
// second hit is resuming at the implicit initial yield;
// third hit is resuming inside the try block.
return {throw: "fit"};
}
};
let it = g.gen();
let result = it.next();
assertEq(result.done, false);
assertEq(result.value, 0);
result = it.next();
assertEq(result.done, true);
assertEq(result.value, "ok");

Просмотреть файл

@ -4647,7 +4647,7 @@ BaselineCompiler::emit_JSOP_AWAIT()
return emit_JSOP_YIELD();
}
typedef bool (*DebugAfterYieldFn)(JSContext*, BaselineFrame*);
typedef bool (*DebugAfterYieldFn)(JSContext*, BaselineFrame*, jsbytecode*, bool*);
static const VMFunction DebugAfterYieldInfo =
FunctionInfo<DebugAfterYieldFn>(jit::DebugAfterYield, "DebugAfterYield");
@ -4660,8 +4660,21 @@ BaselineCompiler::emit_JSOP_DEBUGAFTERYIELD()
frame.assertSyncedStack();
masm.loadBaselineFramePtr(BaselineFrameReg, R0.scratchReg());
prepareVMCall();
pushArg(ImmPtr(pc));
pushArg(R0.scratchReg());
return callVM(DebugAfterYieldInfo);
if (!callVM(DebugAfterYieldInfo))
return false;
icEntries_.back().setFakeKind(ICEntry::Kind_DebugAfterYield);
Label done;
masm.branchTest32(Assembler::Zero, ReturnReg, ReturnReg, &done);
{
masm.loadValue(frame.addressOfReturnValue(), JSReturnOperand);
masm.jump(&return_);
}
masm.bind(&done);
return true;
}
typedef bool (*FinalSuspendFn)(JSContext*, HandleObject, jsbytecode*);

Просмотреть файл

@ -103,6 +103,7 @@ struct DebugModeOSREntry
frameKind == ICEntry::Kind_EarlyStackCheck ||
frameKind == ICEntry::Kind_DebugTrap ||
frameKind == ICEntry::Kind_DebugPrologue ||
frameKind == ICEntry::Kind_DebugAfterYield ||
frameKind == ICEntry::Kind_DebugEpilogue;
}
@ -307,6 +308,8 @@ ICEntryKindToString(ICEntry::Kind kind)
return "debug trap";
case ICEntry::Kind_DebugPrologue:
return "debug prologue";
case ICEntry::Kind_DebugAfterYield:
return "debug after yield";
case ICEntry::Kind_DebugEpilogue:
return "debug epilogue";
default:
@ -367,6 +370,7 @@ PatchBaselineFramesForDebugMode(JSContext* cx,
// - All the ways above.
// C. From the debug trap handler.
// D. From the debug prologue.
// K. From a JSOP_DEBUGAFTERYIELD instruction.
// E. From the debug epilogue.
//
// Cycles (On to Off to On)+ or (Off to On to Off)+:
@ -470,6 +474,7 @@ PatchBaselineFramesForDebugMode(JSContext* cx,
kind == ICEntry::Kind_EarlyStackCheck ||
kind == ICEntry::Kind_DebugTrap ||
kind == ICEntry::Kind_DebugPrologue ||
kind == ICEntry::Kind_DebugAfterYield ||
kind == ICEntry::Kind_DebugEpilogue);
// We will have allocated a new recompile info, so delete the
@ -546,6 +551,17 @@ PatchBaselineFramesForDebugMode(JSContext* cx,
popFrameReg = true;
break;
case ICEntry::Kind_DebugAfterYield:
// Case K above.
//
// Resume at the next instruction.
MOZ_ASSERT(*pc == JSOP_DEBUGAFTERYIELD);
recompInfo->resumeAddr = bl->nativeCodeForPC(script,
pc + JSOP_DEBUGAFTERYIELD_LENGTH,
&recompInfo->slotInfo);
popFrameReg = true;
break;
default:
// Case E above.
//
@ -945,9 +961,9 @@ HasForcedReturn(BaselineDebugModeOSRInfo* info, bool rv)
if (kind == ICEntry::Kind_DebugEpilogue)
return true;
// |rv| is the value in ReturnReg. If true, in the case of the prologue,
// it means a forced return.
if (kind == ICEntry::Kind_DebugPrologue)
// |rv| is the value in ReturnReg. If true, in the case of the prologue or
// after yield, it means a forced return.
if (kind == ICEntry::Kind_DebugPrologue || kind == ICEntry::Kind_DebugAfterYield)
return rv;
// N.B. The debug trap handler handles its own forced return, so no

Просмотреть файл

@ -256,8 +256,9 @@ class ICEntry
Kind_DebugTrap,
// A fake IC entry for returning from a callVM to
// Debug{Prologue,Epilogue}.
// Debug{Prologue,AfterYield,Epilogue}.
Kind_DebugPrologue,
Kind_DebugAfterYield,
Kind_DebugEpilogue,
Kind_Invalid

Просмотреть файл

@ -957,12 +957,19 @@ InterpretResume(JSContext* cx, HandleObject obj, HandleValue val, HandleProperty
}
bool
DebugAfterYield(JSContext* cx, BaselineFrame* frame)
DebugAfterYield(JSContext* cx, BaselineFrame* frame, jsbytecode* pc, bool* mustReturn)
{
*mustReturn = false;
// The BaselineFrame has just been constructed by JSOP_RESUME in the
// caller. We need to set its debuggee flag as necessary.
if (frame->script()->isDebuggee())
//
// If a breakpoint is set on JSOP_DEBUGAFTERYIELD, or stepping is enabled,
// we may already have done this work. Don't fire onEnterFrame again.
if (frame->script()->isDebuggee() && !frame->isDebuggee()) {
frame->setIsDebuggee();
return DebugPrologue(cx, frame, pc, mustReturn);
}
return true;
}
@ -975,13 +982,19 @@ GeneratorThrowOrReturn(JSContext* cx, BaselineFrame* frame, Handle<GeneratorObje
// the exception handler where we will clear the pc.
JSScript* script = frame->script();
uint32_t offset = script->yieldAndAwaitOffsets()[genObj->yieldAndAwaitIndex()];
frame->setOverridePc(script->offsetToPC(offset));
jsbytecode* pc = script->offsetToPC(offset);
frame->setOverridePc(pc);
// In the interpreter, GeneratorObject::resume marks the generator as running,
// so we do the same.
genObj->setRunning();
MOZ_ALWAYS_TRUE(DebugAfterYield(cx, frame));
bool mustReturn = false;
if (!DebugAfterYield(cx, frame, pc, &mustReturn))
return false;
if (mustReturn)
resumeKind = GeneratorObject::RETURN;
MOZ_ALWAYS_FALSE(js::GeneratorThrowOrReturn(cx, frame, genObj, arg, resumeKind));
return false;
}
@ -1091,11 +1104,15 @@ HandleDebugTrap(JSContext* cx, BaselineFrame* frame, uint8_t* retAddr, bool* mus
jsbytecode* pc = script->baselineScript()->icEntryFromReturnAddress(retAddr).pc(script);
if (*pc == JSOP_DEBUGAFTERYIELD) {
// JSOP_DEBUGAFTERYIELD will set the frame's debuggee flag, but if we
// set a breakpoint there we have to do it now.
// JSOP_DEBUGAFTERYIELD will set the frame's debuggee flag and call the
// onEnterFrame handler, but if we set a breakpoint there we have to do
// it now.
MOZ_ASSERT(!frame->isDebuggee());
if (!DebugAfterYield(cx, frame))
if (!DebugAfterYield(cx, frame, pc, mustReturn))
return false;
if (*mustReturn)
return true;
}
MOZ_ASSERT(frame->isDebuggee());

Просмотреть файл

@ -785,7 +785,7 @@ MOZ_MUST_USE bool
InterpretResume(JSContext* cx, HandleObject obj, HandleValue val, HandlePropertyName kind,
MutableHandleValue rval);
MOZ_MUST_USE bool
DebugAfterYield(JSContext* cx, BaselineFrame* frame);
DebugAfterYield(JSContext* cx, BaselineFrame* frame, jsbytecode* pc, bool* mustReturn);
MOZ_MUST_USE bool
GeneratorThrowOrReturn(JSContext* cx, BaselineFrame* frame, Handle<GeneratorObject*> genObj,
HandleValue arg, uint32_t resumeKind);

Просмотреть файл

@ -1817,6 +1817,15 @@ Debugger::fireEnterFrame(JSContext* cx, MutableHandleValue vp)
RootedValue scriptFrame(cx);
FrameIter iter(cx);
#if DEBUG
// Assert that the hook won't be able to re-enter the generator.
if (iter.hasScript() && *iter.pc() == JSOP_DEBUGAFTERYIELD) {
GeneratorObject* genObj = GetGeneratorObjectForFrame(cx, iter.abstractFramePtr());
MOZ_ASSERT(genObj->isRunning() || genObj->isClosing());
}
#endif
if (!getFrame(cx, iter, &scriptFrame))
return reportUncaughtException(ar);

Просмотреть файл

@ -154,6 +154,10 @@ class GeneratorObject : public NativeObject
MOZ_ASSERT_IF(yieldAndAwaitIndex == 0,
getFixedSlot(YIELD_AND_AWAIT_INDEX_SLOT).isUndefined());
MOZ_ASSERT_IF(yieldAndAwaitIndex != 0, isRunning() || isClosing());
setYieldAndAwaitIndexNoAssert(yieldAndAwaitIndex);
}
// Debugger has to flout the state machine rules a bit.
void setYieldAndAwaitIndexNoAssert(uint32_t yieldAndAwaitIndex) {
MOZ_ASSERT(yieldAndAwaitIndex < uint32_t(YIELD_AND_AWAIT_INDEX_CLOSING));
setFixedSlot(YIELD_AND_AWAIT_INDEX_SLOT, Int32Value(yieldAndAwaitIndex));
MOZ_ASSERT(isSuspended());

Просмотреть файл

@ -4286,6 +4286,20 @@ CASE(JSOP_RESUME)
TraceLogStartEvent(logger, scriptEvent);
TraceLogStartEvent(logger, TraceLogger_Interpreter);
switch (Debugger::onEnterFrame(cx, REGS.fp())) {
case ResumeMode::Continue:
break;
case ResumeMode::Throw:
case ResumeMode::Terminate:
goto error;
case ResumeMode::Return:
MOZ_ASSERT_IF(REGS.fp()->callee().isGenerator(), // as opposed to an async function
gen->isClosed());
if (!ForcedReturn(cx, REGS))
goto error;
goto successful_return_continuation;
}
switch (resumeKind) {
case GeneratorObject::NEXT:
break;