diff --git a/js/src/doc/Debugger/Conventions.md b/js/src/doc/Debugger/Conventions.md index 731adfae6b743..45e9d43e7e65f 100644 --- a/js/src/doc/Debugger/Conventions.md +++ b/js/src/doc/Debugger/Conventions.md @@ -66,11 +66,9 @@ Similarly: ## Completion Values -When a debuggee stack frame completes its execution, or when some sort -of debuggee call initiated by the debugger finishes, the `Debugger` -interface provides a value describing how the code completed; these are -called *completion values*. A completion value has one of the -following forms: +The `Debugger` API often needs to convey the result of running some JS code. For example, suppose you get a `frame.onPop` callback telling you that a method in the debuggee just finished. Did it return successfully? Did it throw? What did it return? The debugger passes the `onPop` handler a *completion value* that tells what happened. + +A completion value is one of these: { return: value } : The code completed normally, returning value. Value is a @@ -82,12 +80,51 @@ following forms: the value was thrown, and may be missing. `null` -: The code was terminated, as if by the "slow script" dialog box. +: The code was terminated, as if by the "slow script" ribbon. + +Generators and async functions add a wrinkle: they can suspend themselves (with `yield` or `await`), which removes their frame from the stack. Later, the generator or async frame might be returned to the stack and continue running where it left off. Does it count as "completion" when a generator suspends itself? + +The `Debugger` API says yes. `yield` and `await` do trigger the `frame.onPop` handler, passing a completion value that explains why the frame is being suspended. The completion value gets an extra `.yield` or `.await` property, to distinguish this kind of completion from a normal `return`. + +
+{ return: *value*, yield: true }
+
+ +where *value* is a debuggee value for the iterator result object, like `{ value: 1, done: false }`, for the yield. + +When a generator function is called, it first evaluates any default argument +expressions and destructures its arguments. Then its frame is suspended, and the +new generator object is returned to the caller. This initial suspension is reported +to any `onPop` handlers as a completion value of the form: + +
+{ return: *generatorObject*, yield: true, initial: true }
+
+ +where *generatorObject* is a debuggee value for the generator object being +returned to the caller. + +When an async function awaits a promise, its suspension is reported to any +`onPop` handlers as a completion value of the form: + +
+{ return: *promise*, await: true }
+
+ +where *promise* is a debuggee value for the promise being returned to the +caller. + +The first time a call to an async function awaits, returns, or throws, a promise +of its result is returned to the caller. Subsequent resumptions of the async +call, if any, are initiated directly from the job queue's event loop, with no +calling frame on the stack. Thus, if needed, an `onPop` handler can distinguish +an async call's initial suspension, which returns the promise, from any +subsequent suspensions by checking the `Debugger.Frame`'s `older` property: if +that is `null`, the call was resumed directly from the event loop. -If control reaches the end of a generator frame, the completion value is -{ throw: stop } where stop is a -`Debugger.Object` object representing the `StopIteration` object being -thrown. +Async generators are a combination of async functions and generators that can +use both `yield` and `await` expressions. Suspensions of async generator frames +are reported using any combination of the completion values above. ## Resumption Values diff --git a/js/src/doc/Debugger/Debugger.Frame.md b/js/src/doc/Debugger/Debugger.Frame.md index 41a52aea2e7f4..14b7bbcf2a8ee 100644 --- a/js/src/doc/Debugger/Debugger.Frame.md +++ b/js/src/doc/Debugger/Debugger.Frame.md @@ -117,6 +117,10 @@ the `Debugger.onEnterFrame` handler is called each time a frame is resumed. (This means these events can fire multiple times for the same `Frame` object, which is odd, but accurately conveys what's happening.) +The [completion value][cv] passed to the `frame.onPop` handler for a suspension +contains additional properties to clarify what's going on. See the documentation +for completion values for details. + ## Stepping Into Generators: The "Initial Yield" @@ -279,19 +283,19 @@ the compartment to which the handler method belongs. frames. `onPop` -: This property must be either `undefined` or a function. If it is a - function, SpiderMonkey calls it just before this frame is popped, - passing a [completion value][cv] indicating how this frame's execution - completed, and providing this `Debugger.Frame` instance as the `this` - value. The function should return a [resumption value][rv] indicating - how execution should proceed. On newly created frames, this property's - value is `undefined`. +: This property must be either `undefined` or a function. If it is a function, + SpiderMonkey calls it just before this frame is popped or suspended, passing + a [completion value][cv] indicating the reason, and providing this + `Debugger.Frame` instance as the `this` value. The function should return a + [resumption value][rv] indicating how execution should proceed. On newly + created frames, this property's value is `undefined`. When this handler is called, this frame's current execution location, as reflected in its `offset` and `environment` properties, is the operation - which caused it to be unwound. In frames returning or throwing an - exception, the location is often a return or a throw statement. In frames - propagating exceptions, the location is a call. + which caused it to be unwound. In frames returning or throwing an exception, + the location is often a return or a throw statement. In frames propagating + exceptions, the location is a call. In generator or async function frames, + the location may be a `yield` or `await` expression. When an `onPop` call reports the completion of a construction call (that is, a function called via the `new` operator), the completion @@ -320,15 +324,15 @@ the compartment to which the handler method belongs. resumption value each handler returns establishes the completion value reported to the next handler. + The `onPop` handler is typically called only once for a given + `Debugger.Frame`, after which the frame becomes inactive. However, in the + case of [generators and async functions](#suspended), `onPop` fires each + time the frame is suspended. + This handler is not called on `"debugger"` frames. It is also not called when unwinding a frame due to an over-recursion or out-of-memory exception. - The `onPop` handler is typically called only once for a given frame, - after which the frame becomes inactive. However, in the case of - [generators and async functions](suspended), `onPop` fires each time - the frame is suspended. - ## Function Properties of the Debugger.Frame Prototype Object diff --git a/js/src/gc/Marking.cpp b/js/src/gc/Marking.cpp index 2f5c11c338fec..a16a67522df68 100644 --- a/js/src/gc/Marking.cpp +++ b/js/src/gc/Marking.cpp @@ -431,6 +431,11 @@ JS_PUBLIC_API void JS::UnsafeTraceRoot(JSTracer* trc, T* thingp, js::TraceNullableRoot(trc, thingp, name); } +namespace js { +class SavedFrame; +class AbstractGeneratorObject; +} // namespace js + // Instantiate a copy of the Tracing templates for each public GC pointer type. #define INSTANTIATE_PUBLIC_TRACE_FUNCTIONS(type) \ template JS_PUBLIC_API void JS::UnsafeTraceRoot(JSTracer*, type*, \ @@ -441,6 +446,8 @@ JS_PUBLIC_API void JS::UnsafeTraceRoot(JSTracer* trc, T* thingp, JSTracer*, type*, const char*); FOR_EACH_PUBLIC_GC_POINTER_TYPE(INSTANTIATE_PUBLIC_TRACE_FUNCTIONS) FOR_EACH_PUBLIC_TAGGED_GC_POINTER_TYPE(INSTANTIATE_PUBLIC_TRACE_FUNCTIONS) +INSTANTIATE_PUBLIC_TRACE_FUNCTIONS(SavedFrame*); +INSTANTIATE_PUBLIC_TRACE_FUNCTIONS(AbstractGeneratorObject*); #undef INSTANTIATE_PUBLIC_TRACE_FUNCTIONS namespace js { diff --git a/js/src/jit-test/lib/match-debugger.js b/js/src/jit-test/lib/match-debugger.js index 431279c4af265..4c24a35b05c78 100644 --- a/js/src/jit-test/lib/match-debugger.js +++ b/js/src/jit-test/lib/match-debugger.js @@ -5,15 +5,44 @@ if (typeof Match !== 'function') { } class DebuggerObjectPattern extends Match.Pattern { - constructor(className) { + constructor(className, props) { super(); this.className = className; + if (props) { + this.props = Match.Pattern.OBJECT_WITH_EXACTLY(props); + } } match(actual) { - if (!(actual instanceof Debugger.Object) || actual.class !== this.className) { - throw new Match.MatchError(`Expected Debugger.Object of class ${this.className}, got ${actual}`); + if (!(actual instanceof Debugger.Object)) { + throw new Match.MatchError(`Expected Debugger.Object, got ${actual}`); + } + + if (actual.class !== this.className) { + throw new Match.MatchError(`Expected Debugger.Object of class ${this.className}, got Debugger.Object of class ${actual.class}`); } + + if (this.props !== undefined) { + const lifted = {}; + for (const name of actual.getOwnPropertyNames()) { + const desc = actual.getOwnPropertyDescriptor(name); + if (!('value' in desc)) { + throw new Match.MatchError(`Debugger.Object referent has non-value property ${uneval(name)}`); + } + lifted[name] = desc.value; + } + + try { + this.props.match(lifted); + } catch (inner) { + if (!(inner instanceof Match.MatchError)) { + throw inner; + } + inner.message = `matching Debugger.Object referent properties:\n${inner.message}`; + throw inner; + } + } + return true; } } diff --git a/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js b/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js index 322ce88f7a14f..8c4352bc74596 100644 --- a/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js +++ b/js/src/jit-test/tests/debug/Frame-onPop-async-generators-01.js @@ -38,11 +38,11 @@ collect().then(value => { assertDeepEq(value, [2, 4]); Pattern([ - EXACT({ return: new DebuggerObjectPattern("Promise") }), - EXACT({ return: 2 }), - EXACT({ return: new DebuggerObjectPattern("Promise") }), - EXACT({ return: 4 }), - EXACT({ return: "ok" }), + EXACT({ return: new DebuggerObjectPattern("Promise"), await: true }), + EXACT({ return: 2, yield: true }), + EXACT({ return: new DebuggerObjectPattern("Promise"), await: true }), + EXACT({ return: 4, yield: true }), + EXACT({ return: "ok", await: true }), EXACT({ return: "ok" }), ]).assert(log); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js index 6ada610b1332a..993e2e11bbcab 100644 --- a/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-06.js @@ -23,6 +23,6 @@ g.eval(` assertDeepEq([... g.f()], [3]); Pattern([ - EXACT({ return: new DebuggerObjectPattern("Object") }), - EXACT({ return: new DebuggerObjectPattern("Object") }), + EXACT({ return: new DebuggerObjectPattern("Object", { value: 3, done: false }), yield: true }), + EXACT({ return: new DebuggerObjectPattern("Object", { value: "ok", done: true }) }), ]).assert(log); diff --git a/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js b/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js new file mode 100644 index 0000000000000..f1b18ff05dbf8 --- /dev/null +++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-07.js @@ -0,0 +1,40 @@ +// Completion values for generators report yields and initial yields. + +load(libdir + "asserts.js"); +load(libdir + 'match.js'); +load(libdir + 'match-debugger.js'); +const { Pattern } = Match; +const { OBJECT_WITH_EXACTLY: X } = Pattern; + +const g = newGlobal({ newCompartment: true }); +g.eval(` + function* f() { + yield "yielding"; + return "returning"; + } +`); + +const dbg = new Debugger(g); +const completions = []; +dbg.onEnterFrame = frame => { + frame.onPop = completion => { + completions.push(completion); + }; +}; + +assertDeepEq([... g.f()], ["yielding"]); +print(uneval(completions)); +Pattern([ + X({ + return: new DebuggerObjectPattern("Generator", {}), + yield: true, + initial: true + }), + X({ + return: new DebuggerObjectPattern("Object", { value: "yielding", done: false }), + yield: true + }), + X({ + return: new DebuggerObjectPattern("Object", { value: "returning", done: true }) + }), +]).assert(completions); diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 7d2e77f60ab77..da00e2f856b75 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -210,6 +210,7 @@ MACRO(index, index, "index") \ MACRO(infinity, infinity, "infinity") \ MACRO(Infinity, Infinity, "Infinity") \ + MACRO(initial, initial, "initial") \ MACRO(InitializeCollator, InitializeCollator, "InitializeCollator") \ MACRO(InitializeDateTimeFormat, InitializeDateTimeFormat, \ "InitializeDateTimeFormat") \ diff --git a/js/src/vm/Debugger.cpp b/js/src/vm/Debugger.cpp index b1c2c0cc9671c..507abe8e6e231 100644 --- a/js/src/vm/Debugger.cpp +++ b/js/src/vm/Debugger.cpp @@ -16,6 +16,7 @@ #include "jsfriendapi.h" #include "jsnum.h" +#include "builtin/Promise.h" #include "frontend/BytecodeCompilation.h" #include "frontend/Parser.h" #include "gc/FreeOp.h" @@ -42,7 +43,6 @@ #include "vm/AsyncIteration.h" #include "vm/DebuggerMemory.h" #include "vm/GeckoProfiler.h" -#include "vm/GeneratorObject.h" #include "vm/JSContext.h" #include "vm/JSObject.h" #include "vm/Realm.h" @@ -982,13 +982,18 @@ bool Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, mozilla::DebugOnly> debuggeeGlobal = cx->global(); - bool suspending = false; + // These are updated below, but consulted by the cleanup code we register now, + // so declare them here, initialized to quiescent values. + Rooted completion(cx); bool success = false; + auto frameMapsGuard = MakeScopeExit([&] { - // Clean up all Debugger.Frame instances on exit. On suspending, pass - // the flag that says to leave those frames `.live`. Note that if - // suspending && !success, the generator is closed, not suspended. - removeFromFrameMapsAndClearBreakpointsIn(cx, frame, suspending && success); + // Clean up all Debugger.Frame instances on exit. On suspending, pass the + // flag that says to leave those frames `.live`. Note that if the completion + // is a suspension but success is false, the generator gets closed, not + // suspended. + removeFromFrameMapsAndClearBreakpointsIn( + cx, frame, success && completion.get().suspending()); }); // The onPop handler and associated clean up logic should not run multiple @@ -1002,34 +1007,9 @@ bool Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, return frameOk; } - // Determine if we are suspending this frame or popping it forever. - Rooted genObj(cx); - if (frame.isGeneratorFrame()) { - // Since generators are never wasm, we can assume pc is not nullptr, and - // that analyzing bytecode is meaningful. - MOZ_ASSERT(!frame.isWasmDebugFrame()); - - // If we're leaving successfully at a yield opcode, we're probably - // suspending; the `isClosed()` check detects a debugger forced return - // from an `onStep` handler, which looks almost the same. - // - // GetGeneratorObjectForFrame can return nullptr even when a generator - // object does exist, if the frame is paused between the GENERATOR and - // SETALIASEDVAR opcodes. But by checking the opcode first we eliminate that - // possibility, so it's fine to call genObj->isClosed(). - genObj = GetGeneratorObjectForFrame(cx, frame); - suspending = - frameOk && - (*pc == JSOP_INITIALYIELD || *pc == JSOP_YIELD || *pc == JSOP_AWAIT) && - !genObj->isClosed(); - } - - // Save the frame's completion value. - ResumeMode resumeMode; - RootedValue value(cx); - RootedSavedFrame exnStack(cx); - Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &resumeMode, - &value, &exnStack); + completion = Completion::fromJSFramePop(cx, frame, pc, frameOk); + Rooted genObj( + cx, completion.get().maybeGeneratorObject()); // Preserve the debuggee's microtask event queue while we run the hooks, so // the debugger's microtask checkpoints don't run from the debuggee's @@ -1056,16 +1036,11 @@ bool Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, Maybe ar; ar.emplace(cx, dbg->object); - RootedValue wrappedValue(cx, value); - RootedValue completion(cx); - if (!dbg->wrapDebuggeeValue(cx, &wrappedValue)) { - resumeMode = dbg->reportUncaughtException(ar); - break; - } + // The resumption requested by the onPop handler we're about to call. + ResumeMode nextResumeMode; + RootedValue nextValue(cx); // Call the onPop handler. - ResumeMode nextResumeMode = resumeMode; - RootedValue nextValue(cx, wrappedValue); bool success; { // Mark the generator as running, to prevent reentrance. @@ -1076,8 +1051,8 @@ bool Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, // to JavaScript, so reentrance isn't possible anyway. So there's no // harm done if this has no effect in that case. AutoSetGeneratorRunning asgr(cx, genObj); - success = handler->onPop(cx, frameobj, nextResumeMode, &nextValue, - exnStack); + success = handler->onPop(cx, frameobj, completion, nextResumeMode, + &nextValue); } nextResumeMode = dbg->processParsedHandlerResult( ar, frame, pc, success, nextResumeMode, &nextValue); @@ -1088,16 +1063,17 @@ bool Debugger::slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame, MOZ_ASSERT(cx->compartment() == debuggeeGlobal->compartment()); MOZ_ASSERT(!cx->isExceptionPending()); - // ResumeMode::Continue means "make no change". - if (nextResumeMode != ResumeMode::Continue) { - resumeMode = nextResumeMode; - value = nextValue; - } + completion.get().updateForNextHandler(nextResumeMode, nextValue); } } } - // Establish (resumeMode, value) as our resumption value. + // Now that we've run all the handlers, extract the final resumption mode. */ + ResumeMode resumeMode; + RootedValue value(cx); + RootedSavedFrame exnStack(cx); + completion.get().toResumeMode(resumeMode, &value, &exnStack); + switch (resumeMode) { case ResumeMode::Return: frame.setReturnValue(value); @@ -1895,6 +1871,264 @@ ResumeMode Debugger::processHandlerResult(Maybe& ar, bool success, /*** Debuggee completion values *********************************************/ +/* static */ +Completion Completion::fromJSResult(JSContext* cx, bool ok, const Value& rv) { + MOZ_ASSERT_IF(ok, !cx->isExceptionPending()); + + if (ok) { + return Completion(Return(rv)); + } + + if (!cx->isExceptionPending()) { + return Completion(Terminate()); + } + + RootedValue exception(cx); + SavedFrame* stack = cx->getPendingExceptionStack(); + MOZ_ALWAYS_TRUE(cx->getPendingException(&exception)); + + cx->clearPendingException(); + + return Completion(Throw(exception, stack)); +} + +/* static */ +Completion Completion::fromJSFramePop(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, bool ok) { + // Only Wasm frames get a null pc. + MOZ_ASSERT_IF(!frame.isWasmDebugFrame(), pc); + + // If this isn't a generator suspension, then that's already handled above. + if (!ok || !frame.isGeneratorFrame()) { + return fromJSResult(cx, ok, frame.returnValue()); + } + + // A generator is being suspended or returning. + + // Since generators are never wasm, we can assume pc is not nullptr, and + // that analyzing bytecode is meaningful. + MOZ_ASSERT(!frame.isWasmDebugFrame()); + + // If we're leaving successfully at a yield opcode, we're probably + // suspending; the `isClosed()` check detects a debugger forced return from + // an `onStep` handler, which looks almost the same. + // + // GetGeneratorObjectForFrame can return nullptr even when a generator + // object does exist, if the frame is paused between the GENERATOR and + // SETALIASEDVAR opcodes. But by checking the opcode first we eliminate that + // possibility, so it's fine to call genObj->isClosed(). + Rooted generatorObj( + cx, GetGeneratorObjectForFrame(cx, frame)); + switch (*pc) { + case JSOP_INITIALYIELD: + MOZ_ASSERT(!generatorObj->isClosed()); + return Completion(InitialYield(generatorObj)); + + case JSOP_YIELD: + MOZ_ASSERT(!generatorObj->isClosed()); + return Completion(Yield(generatorObj, frame.returnValue())); + + case JSOP_AWAIT: + MOZ_ASSERT(!generatorObj->isClosed()); + return Completion(Await(generatorObj, frame.returnValue())); + + default: + return Completion(Return(frame.returnValue())); + } +} + +void Completion::trace(JSTracer* trc) { + variant.match([=](auto& var) { var.trace(trc); }); +} + +struct MOZ_STACK_CLASS Completion::BuildValueMatcher { + JSContext* cx; + Debugger* dbg; + MutableHandleValue result; + + BuildValueMatcher(JSContext* cx, Debugger* dbg, MutableHandleValue result) + : cx(cx), dbg(dbg), result(result) { + cx->check(dbg->toJSObject()); + } + + bool operator()(const Completion::Return& ret) { + RootedNativeObject obj(cx, newObject()); + RootedValue retval(cx, ret.value); + if (!obj || !wrap(&retval) || !add(obj, cx->names().return_, retval)) { + return false; + } + result.setObject(*obj); + return true; + } + + bool operator()(const Completion::Throw& thr) { + RootedNativeObject obj(cx, newObject()); + RootedValue exc(cx, thr.exception); + if (!obj || !wrap(&exc) || !add(obj, cx->names().throw_, exc)) { + return false; + } + if (thr.stack) { + RootedValue stack(cx, ObjectValue(*thr.stack)); + if (!wrapStack(&stack) || !add(obj, cx->names().stack, stack)) { + return false; + } + } + result.setObject(*obj); + return true; + } + + bool operator()(const Completion::Terminate& term) { + result.setNull(); + return true; + } + + bool operator()(const Completion::InitialYield& initialYield) { + RootedNativeObject obj(cx, newObject()); + RootedValue gen(cx, ObjectValue(*initialYield.generatorObject)); + if (!obj || !wrap(&gen) || !add(obj, cx->names().return_, gen) || + !add(obj, cx->names().yield, TrueHandleValue) || + !add(obj, cx->names().initial, TrueHandleValue)) { + return false; + } + result.setObject(*obj); + return true; + } + + bool operator()(const Completion::Yield& yield) { + RootedNativeObject obj(cx, newObject()); + RootedValue iteratorResult(cx, yield.iteratorResult); + if (!obj || !wrap(&iteratorResult) || + !add(obj, cx->names().return_, iteratorResult) || + !add(obj, cx->names().yield, TrueHandleValue)) { + return false; + } + result.setObject(*obj); + return true; + } + + bool operator()(const Completion::Await& await) { + RootedNativeObject obj(cx, newObject()); + RootedValue awaitee(cx, await.awaitee); + if (!obj || !wrap(&awaitee) || !add(obj, cx->names().return_, awaitee) || + !add(obj, cx->names().await, TrueHandleValue)) { + return false; + } + result.setObject(*obj); + return true; + } + + private: + NativeObject* newObject() const { + return NewBuiltinClassInstance(cx); + } + + bool add(HandleNativeObject obj, PropertyName* name, + HandleValue value) const { + return NativeDefineDataProperty(cx, obj, name, value, JSPROP_ENUMERATE); + } + + bool wrap(MutableHandleValue v) const { + return dbg->wrapDebuggeeValue(cx, v); + } + + // Saved stacks are wrapped for direct consumption by debugger code. + bool wrapStack(MutableHandleValue stack) const { + return cx->compartment()->wrap(cx, stack); + } +}; + +bool Completion::buildCompletionValue(JSContext* cx, Debugger* dbg, + MutableHandleValue result) const { + return variant.match(BuildValueMatcher(cx, dbg, result)); +} + +AbstractGeneratorObject* Completion::maybeGeneratorObject() const { + if (variant.is()) { + return variant.as().generatorObject; + } + + if (variant.is()) { + return variant.as().generatorObject; + } + + if (variant.is()) { + return variant.as().generatorObject; + } + + return nullptr; +} + +void Completion::updateForNextHandler(ResumeMode resumeMode, + HandleValue value) { + switch (resumeMode) { + case ResumeMode::Continue: + // No change to how we'll resume. + break; + + case ResumeMode::Throw: + // Since this is a new exception, the stack for the old one may not apply. + // If we extend resumption values to specify stacks, we could revisit + // this. + variant = Variant(Throw(value, nullptr)); + break; + + case ResumeMode::Terminate: + variant = Variant(Terminate()); + break; + + case ResumeMode::Return: + variant = Variant(Return(value)); + break; + + default: + MOZ_CRASH("invalid resumeMode value"); + } +} + +struct MOZ_STACK_CLASS Completion::ToResumeModeMatcher { + MutableHandleValue value; + MutableHandleSavedFrame exnStack; + ToResumeModeMatcher(MutableHandleValue value, + MutableHandleSavedFrame exnStack) + : value(value), exnStack(exnStack) {} + + ResumeMode operator()(const Return& ret) { + value.set(ret.value); + return ResumeMode::Return; + } + + ResumeMode operator()(const Throw& thr) { + value.set(thr.exception); + exnStack.set(thr.stack); + return ResumeMode::Throw; + } + + ResumeMode operator()(const Terminate& term) { + value.setUndefined(); + return ResumeMode::Terminate; + } + + ResumeMode operator()(const InitialYield& initialYield) { + value.setObject(*initialYield.generatorObject); + return ResumeMode::Return; + } + + ResumeMode operator()(const Yield& yield) { + value.set(yield.iteratorResult); + return ResumeMode::Return; + } + + ResumeMode operator()(const Await& await) { + value.set(await.awaitee); + return ResumeMode::Return; + } +}; + +void Completion::toResumeMode(ResumeMode& resumeMode, MutableHandleValue value, + MutableHandleSavedFrame exnStack) const { + resumeMode = variant.match(ToResumeModeMatcher(value, exnStack)); +} + /* static */ void Debugger::resultToCompletion(JSContext* cx, bool ok, const Value& rv, ResumeMode* resumeMode, @@ -8923,39 +9157,19 @@ void ScriptedOnPopHandler::trace(JSTracer* tracer) { } bool ScriptedOnPopHandler::onPop(JSContext* cx, HandleDebuggerFrame frame, - ResumeMode& resumeMode, MutableHandleValue vp, - HandleSavedFrame exnStack) { - Debugger* dbg = frame->owner(); - - // Make it possible to distinguish 'return' from 'await' completions. - // Bug 1470558 will investigate a more robust solution. - bool isAfterAwait = false; - AbstractFramePtr referent = DebuggerFrame::getReferent(frame); - if (resumeMode == ResumeMode::Return && referent && - referent.isFunctionFrame() && referent.callee()->isAsync() && - !referent.callee()->isGenerator()) { - AutoRealm ar(cx, referent.callee()); - if (frame->hasGenerator()) { - AbstractGeneratorObject& genObj = frame->unwrappedGenerator(); - isAfterAwait = !genObj.isClosed() && genObj.isRunning(); - } - } + const Completion& completion, + ResumeMode& resumeMode, + MutableHandleValue vp) { + Debugger* dbg = Debugger::fromChildJSObject(frame); - RootedValue completion(cx); - if (!dbg->newCompletionValue(cx, resumeMode, vp, exnStack, &completion)) { + RootedValue completionValue(cx); + if (!completion.buildCompletionValue(cx, dbg, &completionValue)) { return false; } - if (isAfterAwait) { - RootedObject obj(cx, &completion.toObject()); - if (!DefineDataProperty(cx, obj, cx->names().await, TrueHandleValue)) { - return false; - } - } - RootedValue fval(cx, ObjectValue(*object_)); RootedValue rval(cx); - if (!js::Call(cx, fval, frame, completion, &rval)) { + if (!js::Call(cx, fval, frame, completionValue, &rval)) { return false; } diff --git a/js/src/vm/Debugger.h b/js/src/vm/Debugger.h index 478ed74553a56..dc7f4c23290c5 100644 --- a/js/src/vm/Debugger.h +++ b/js/src/vm/Debugger.h @@ -21,15 +21,24 @@ #include "js/GCVariant.h" #include "js/HashTable.h" #include "js/Promise.h" +#include "js/RootingAPI.h" #include "js/Utility.h" #include "js/Wrapper.h" #include "proxy/DeadObjectProxy.h" +#include "vm/GeneratorObject.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/Realm.h" #include "vm/SavedStacks.h" #include "vm/Stack.h" +/* + * Windows 3.x used a cooperative multitasking model, with a Yield macro that + * let you relinquish control to other cooperative threads. Microsoft replaced + * it with an empty macro long ago. We should be free to use it in our code. + */ +#undef Yield + namespace js { class AbstractGeneratorObject; @@ -87,6 +96,165 @@ enum class ResumeMode { Return, }; +/** + * A completion value, describing how some sort of JavaScript evaluation + * completed. This is used to tell an onPop handler what's going on with the + * frame, and to report the outcome of call, apply, setProperty, and getProperty + * operations. + * + * Local variables of type Completion should be held in Rooted locations, + * and passed using Handle and MutableHandle. + */ +class Completion { + public: + struct Return { + explicit Return(const Value& value) : value(value) {} + Value value; + + void trace(JSTracer* trc) { + JS::UnsafeTraceRoot(trc, &value, "js::Completion::Return::value"); + } + }; + + struct Throw { + Throw(const Value& exception, SavedFrame* stack) + : exception(exception), stack(stack) {} + Value exception; + SavedFrame* stack; + + void trace(JSTracer* trc) { + JS::UnsafeTraceRoot(trc, &exception, "js::Completion::Throw::exception"); + JS::UnsafeTraceRoot(trc, &stack, "js::Completion::Throw::stack"); + } + }; + + struct Terminate { + void trace(JSTracer* trc) {} + }; + + struct InitialYield { + explicit InitialYield(AbstractGeneratorObject* generatorObject) + : generatorObject(generatorObject) {} + AbstractGeneratorObject* generatorObject; + + void trace(JSTracer* trc) { + JS::UnsafeTraceRoot(trc, &generatorObject, + "js::Completion::InitialYield::generatorObject"); + } + }; + + struct Yield { + Yield(AbstractGeneratorObject* generatorObject, const Value& iteratorResult) + : generatorObject(generatorObject), iteratorResult(iteratorResult) {} + AbstractGeneratorObject* generatorObject; + Value iteratorResult; + + void trace(JSTracer* trc) { + JS::UnsafeTraceRoot(trc, &generatorObject, + "js::Completion::Yield::generatorObject"); + JS::UnsafeTraceRoot(trc, &iteratorResult, + "js::Completion::Yield::iteratorResult"); + } + }; + + struct Await { + Await(AbstractGeneratorObject* generatorObject, const Value& awaitee) + : generatorObject(generatorObject), awaitee(awaitee) {} + AbstractGeneratorObject* generatorObject; + Value awaitee; + + void trace(JSTracer* trc) { + JS::UnsafeTraceRoot(trc, &generatorObject, + "js::Completion::Await::generatorObject"); + JS::UnsafeTraceRoot(trc, &awaitee, "js::Completion::Await::awaitee"); + } + }; + + // The JS::Result macros want to assign to an existing variable, so having a + // default constructor is handy. + Completion() : variant(Terminate()) {} + + // Construct a completion from a specific variant. + // + // Unfortunately, using a template here would prevent the implicit definitions + // of the copy and move constructor and assignment operators, which is icky. + explicit Completion(Return&& variant) + : variant(std::forward(variant)) {} + explicit Completion(Throw&& variant) + : variant(std::forward(variant)) {} + explicit Completion(Terminate&& variant) + : variant(std::forward(variant)) {} + explicit Completion(InitialYield&& variant) + : variant(std::forward(variant)) {} + explicit Completion(Yield&& variant) + : variant(std::forward(variant)) {} + explicit Completion(Await&& variant) + : variant(std::forward(variant)) {} + + // Capture a JavaScript operation result as a Completion value. This clears + // any exception and stack from cx, taking ownership of them itself. + static Completion fromJSResult(JSContext* cx, bool ok, const Value& rv); + + // Construct a completion given an AbstractFramePtr that is being popped. This + // clears any exception and stack from cx, taking ownership of them itself. + static Completion fromJSFramePop(JSContext* cx, AbstractFramePtr frame, + const jsbytecode* pc, bool ok); + + template + bool is() const { + return variant.template is(); + } + + template + V& as() { + return variant.template as(); + } + + template + const V& as() const { + return variant.template as(); + } + + void trace(JSTracer* trc); + + /* True if this completion is a suspension of a generator or async call. */ + bool suspending() const { + return variant.is() || variant.is() || + variant.is(); + } + + /* + * If this completion is a suspension of a generator or async call, return the + * call's generator object, nullptr otherwise. + */ + AbstractGeneratorObject* maybeGeneratorObject() const; + + /* Set `result` to a Debugger API completion value describing this completion. + */ + bool buildCompletionValue(JSContext* cx, Debugger* dbg, + MutableHandleValue result) const; + + /* + * Set `resumeMode`, `value`, and `exnStack` to values describing this + * completion. + */ + void toResumeMode(ResumeMode& resumeMode, MutableHandleValue value, + MutableHandleSavedFrame exnStack) const; + /* + * Given a `ResumeMode` and value (typically derived from a resumption value + * returned by a Debugger hook), update this completion as requested. + */ + void updateForNextHandler(ResumeMode resumeMode, HandleValue value); + + private: + using Variant = + mozilla::Variant; + struct BuildValueMatcher; + struct ToResumeModeMatcher; + + Variant variant; +}; + typedef HashSet, ZoneAllocPolicy> WeakGlobalObjectSet; @@ -1389,15 +1557,16 @@ class ScriptedOnStepHandler final : public OnStepHandler { */ struct OnPopHandler : Handler { /* - * If a frame is about the be popped, this method is called with the frame - * as argument, and `resumeMode` and `vp` set to a completion value specifying - * how this frame's execution completed. If successful, this method should - * return true, with `resumeMode` and `vp` set to a resumption value - * specifying how execution should continue. + * The given `frame` is about to be popped; `completion` explains why. + * + * When this method returns true, it must set `resumeMode` and `vp` to a + * resumption value specifying how execution should continue. + * + * When this method returns false, it should set an exception on `cx`. */ virtual bool onPop(JSContext* cx, HandleDebuggerFrame frame, - ResumeMode& resumeMode, MutableHandleValue vp, - HandleSavedFrame exnStack) = 0; + const Completion& completion, ResumeMode& resumeMode, + MutableHandleValue vp) = 0; }; class ScriptedOnPopHandler final : public OnPopHandler { @@ -1407,8 +1576,8 @@ class ScriptedOnPopHandler final : public OnPopHandler { virtual void drop() override; virtual void trace(JSTracer* tracer) override; virtual bool onPop(JSContext* cx, HandleDebuggerFrame frame, - ResumeMode& resumeMode, MutableHandleValue vp, - HandleSavedFrame exnStack) override; + const Completion& completion, ResumeMode& resumeMode, + MutableHandleValue vp) override; private: HeapPtr object_;