Commit 1b414e28 authored by littledan's avatar littledan Committed by Commit bot

Reland Async/await Promise dependency graph

This patch knits together Promises returned by async/await such that when
one async function awaits the result of another one, catch prediction works
across the boundaries, whether the exception comes synchronously or
asynchronously. Edges are added in three places:
- When a locally uncaught await happens, if the value passed into await
  is a Promise, from the awaited value to the Promise under construction
  in the broader async function
- From a "throwaway" Promise, which may be found on the Promise debug
  stack, to the Promise under construction in the async function that
  surrounds it
- When a Promise is resolved with another Promise (e.g., when returning a
  Promise from an async function)

In this reland, the caught tests are broken up into four parts to avoid
timeouts.

BUG=v8:5167

Review-Url: https://codereview.chromium.org/2346363004
Cr-Commit-Position: refs/heads/master@{#39564}
parent c604eae3
......@@ -199,11 +199,11 @@
V(normal_ic_symbol) \
V(not_mapped_symbol) \
V(premonomorphic_symbol) \
V(promise_await_handler_symbol) \
V(promise_combined_deferred_symbol) \
V(promise_debug_marker_symbol) \
V(promise_deferred_reactions_symbol) \
V(promise_forwarding_handler_symbol) \
V(promise_fulfill_reactions_symbol) \
V(promise_handled_by_symbol) \
V(promise_handled_hint_symbol) \
V(promise_has_handler_symbol) \
V(promise_raw_symbol) \
......
......@@ -33,7 +33,10 @@ utils.Import(function(from) {
ResolvePromise = from.ResolvePromise;
});
var promiseAwaitHandlerSymbol = utils.ImportNow("promise_await_handler_symbol");
var promiseHandledBySymbol =
utils.ImportNow("promise_handled_by_symbol");
var promiseForwardingHandlerSymbol =
utils.ImportNow("promise_forwarding_handler_symbol");
var promiseHandledHintSymbol =
utils.ImportNow("promise_handled_hint_symbol");
var promiseHasHandlerSymbol =
......@@ -56,10 +59,11 @@ function PromiseCastResolved(value) {
// Shared logic for the core of await. The parser desugars
// await awaited
// into
// yield AsyncFunctionAwait{Caught,Uncaught}(.generator, awaited)
// yield AsyncFunctionAwait{Caught,Uncaught}(.generator, awaited, .promise)
// The 'awaited' parameter is the value; the generator stands in
// for the asyncContext, and mark is metadata for debugging
function AsyncFunctionAwait(generator, awaited, mark) {
// for the asyncContext, and .promise is the larger promise under
// construction by the enclosing async function.
function AsyncFunctionAwait(generator, awaited, outerPromise) {
// Promise.resolve(awaited).then(
// value => AsyncFunctionNext(value),
// error => AsyncFunctionThrow(error)
......@@ -83,12 +87,6 @@ function AsyncFunctionAwait(generator, awaited, mark) {
return;
}
if (mark && DEBUG_IS_ACTIVE && IsPromise(awaited)) {
// Mark the reject handler callback such that it does not influence
// catch prediction.
SET_PRIVATE(onRejected, promiseAwaitHandlerSymbol, true);
}
// Just forwarding the exception, so no debugEvent for throwawayCapability
var throwawayCapability = NewPromiseCapability(GlobalPromise, false);
......@@ -96,24 +94,38 @@ function AsyncFunctionAwait(generator, awaited, mark) {
// unhandled reject events as its work is done
SET_PRIVATE(throwawayCapability.promise, promiseHasHandlerSymbol, true);
return PerformPromiseThen(promise, onFulfilled, onRejected,
throwawayCapability);
PerformPromiseThen(promise, onFulfilled, onRejected, throwawayCapability);
if (DEBUG_IS_ACTIVE && !IS_UNDEFINED(outerPromise)) {
if (IsPromise(awaited)) {
// Mark the reject handler callback to be a forwarding edge, rather
// than a meaningful catch handler
SET_PRIVATE(onRejected, promiseForwardingHandlerSymbol, true);
}
// Mark the dependency to outerPromise in case the throwaway Promise is
// found on the Promise stack
SET_PRIVATE(throwawayCapability.promise, promiseHandledBySymbol,
outerPromise);
}
}
// Called by the parser from the desugaring of 'await' when catch
// prediction indicates no locally surrounding catch block
function AsyncFunctionAwaitUncaught(generator, awaited) {
// TODO(littledan): Install a dependency edge from awaited to outerPromise
return AsyncFunctionAwait(generator, awaited, true);
function AsyncFunctionAwaitUncaught(generator, awaited, outerPromise) {
AsyncFunctionAwait(generator, awaited, outerPromise);
}
// Called by the parser from the desugaring of 'await' when catch
// prediction indicates that there is a locally surrounding catch block
function AsyncFunctionAwaitCaught(generator, awaited) {
function AsyncFunctionAwaitCaught(generator, awaited, outerPromise) {
if (DEBUG_IS_ACTIVE && IsPromise(awaited)) {
SET_PRIVATE(awaited, promiseHandledHintSymbol, true);
}
return AsyncFunctionAwait(generator, awaited, false);
// Pass undefined for the outer Promise to not waste time setting up
// or following the dependency chain when this Promise is already marked
// as handled
AsyncFunctionAwait(generator, awaited, UNDEFINED);
}
// How the parser rejects promises from async/await desugaring
......
......@@ -219,7 +219,8 @@ function PostNatives(utils) {
"iterator_symbol",
"promise_result_symbol",
"promise_state_symbol",
"promise_await_handler_symbol",
"promise_forwarding_handler_symbol",
"promise_handled_by_symbol",
"promise_handled_hint_symbol",
"promise_has_handler_symbol",
"object_freeze",
......
......@@ -12,10 +12,10 @@
// Imports
var InternalArray = utils.InternalArray;
var promiseAwaitHandlerSymbol =
utils.ImportNow("promise_await_handler_symbol");
var promiseCombinedDeferredSymbol =
utils.ImportNow("promise_combined_deferred_symbol");
var promiseHandledBySymbol =
utils.ImportNow("promise_handled_by_symbol");
var promiseForwardingHandlerSymbol =
utils.ImportNow("promise_forwarding_handler_symbol");
var promiseHasHandlerSymbol =
utils.ImportNow("promise_has_handler_symbol");
var promiseRejectReactionsSymbol =
......@@ -222,6 +222,7 @@ function PromiseAttachCallbacks(promise, deferred, onResolve, onReject) {
function PromiseIdResolveHandler(x) { return x; }
function PromiseIdRejectHandler(r) { %_ReThrow(r); }
SET_PRIVATE(PromiseIdRejectHandler, promiseForwardingHandlerSymbol, true);
function PromiseNopResolver() {}
......@@ -236,7 +237,7 @@ function IsPromise(x) {
}
function PromiseCreate() {
return new GlobalPromise(PromiseNopResolver)
return new GlobalPromise(PromiseNopResolver);
}
// ES#sec-promise-resolve-functions
......@@ -287,6 +288,10 @@ function ResolvePromise(promise, resolution) {
var id;
var name = "PromiseResolveThenableJob";
var instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting && IsPromise(resolution)) {
// Mark the dependency of the new promise on the resolution
SET_PRIVATE(resolution, promiseHandledBySymbol, promise);
}
%EnqueueMicrotask(function() {
if (instrumenting) {
%DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
......@@ -310,7 +315,8 @@ function ResolvePromise(promise, resolution) {
return;
}
}
FulfillPromise(promise, kFulfilled, resolution, promiseFulfillReactionsSymbol);
FulfillPromise(promise, kFulfilled, resolution,
promiseFulfillReactionsSymbol);
}
// ES#sec-rejectpromise
......@@ -472,6 +478,10 @@ function PromiseAll(iterable) {
var resolutions = new InternalArray();
var count;
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
function CreateResolveElementFunction(index, values, promiseCapability) {
var alreadyCalled = false;
return (x) => {
......@@ -492,10 +502,14 @@ function PromiseAll(iterable) {
for (var value of iterable) {
var nextPromise = this.resolve(value);
++count;
nextPromise.then(
var throwawayPromise = nextPromise.then(
CreateResolveElementFunction(i, resolutions, deferred),
deferred.reject);
SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
// For catch prediction, mark that rejections here are semantically
// handled by the combined Promise.
if (IsPromise(throwawayPromise)) {
SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
}
++i;
}
......@@ -522,10 +536,20 @@ function PromiseRace(iterable) {
// false debugEvent so that forwarding the rejection through race does not
// trigger redundant ExceptionEvents
var deferred = NewPromiseCapability(this, false);
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
try {
for (var value of iterable) {
this.resolve(value).then(deferred.resolve, deferred.reject);
SET_PRIVATE(deferred.reject, promiseCombinedDeferredSymbol, deferred);
var throwawayPromise = this.resolve(value).then(deferred.resolve,
deferred.reject);
// For catch prediction, mark that rejections here are semantically
// handled by the combined Promise.
if (IsPromise(throwawayPromise)) {
SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
}
}
} catch (e) {
deferred.reject(e)
......@@ -537,19 +561,18 @@ function PromiseRace(iterable) {
// Utility for debugger
function PromiseHasUserDefinedRejectHandlerCheck(handler, deferred) {
// If this handler was installed by async/await, it does not indicate
// that there is a user-defined reject handler.
if (GET_PRIVATE(handler, promiseAwaitHandlerSymbol)) return false;
if (handler !== PromiseIdRejectHandler) {
var combinedDeferred = GET_PRIVATE(handler, promiseCombinedDeferredSymbol);
if (IS_UNDEFINED(combinedDeferred)) return true;
if (PromiseHasUserDefinedRejectHandlerRecursive(combinedDeferred.promise)) {
return true;
}
} else if (PromiseHasUserDefinedRejectHandlerRecursive(deferred.promise)) {
return true;
// Recurse to the forwarding Promise, if any. This may be due to
// - await reaction forwarding to the throwaway Promise, which has
// a dependency edge to the outer Promise.
// - PromiseIdResolveHandler forwarding to the output of .then
// - Promise.all/Promise.race forwarding to a throwaway Promise, which
// has a dependency edge to the generated outer Promise.
if (GET_PRIVATE(handler, promiseForwardingHandlerSymbol)) {
return PromiseHasUserDefinedRejectHandlerRecursive(deferred.promise);
}
return false;
// Otherwise, this is a real reject handler for the Promise
return true;
}
function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
......@@ -557,6 +580,17 @@ function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
// in an async function, then it has a user-defined reject handler.
if (GET_PRIVATE(promise, promiseHandledHintSymbol)) return true;
// If this Promise is subsumed by another Promise (a Promise resolved
// with another Promise, or an intermediate, hidden, throwaway Promise
// within async/await), then recurse on the outer Promise.
// In this case, the dependency is one possible way that the Promise
// could be resolved, so it does not subsume the other following cases.
var outerPromise = GET_PRIVATE(promise, promiseHandledBySymbol);
if (outerPromise &&
PromiseHasUserDefinedRejectHandlerRecursive(outerPromise)) {
return true;
}
var queue = GET_PRIVATE(promise, promiseRejectReactionsSymbol);
var deferreds = GET_PRIVATE(promise, promiseDeferredReactionsSymbol);
......@@ -577,6 +611,8 @@ function PromiseHasUserDefinedRejectHandlerRecursive(promise) {
// Return whether the promise will be handled by a user-defined reject
// handler somewhere down the promise chain. For this, we do a depth-first
// search for a reject handler that's not the default PromiseIdRejectHandler.
// This function also traverses dependencies of one Promise on another,
// set up through async/await and Promises resolved with Promises.
function PromiseHasUserDefinedRejectHandler() {
return PromiseHasUserDefinedRejectHandlerRecursive(this);
};
......
......@@ -4564,7 +4564,7 @@ Expression* Parser::RewriteAwaitExpression(Expression* value, int await_pos) {
// yield do {
// promise_tmp = .promise;
// tmp = <operand>;
// %AsyncFunctionAwait(.generator_object, tmp);
// %AsyncFunctionAwait(.generator_object, tmp, promise_tmp);
// promise_tmp
// }
// The value of the expression is returned to the caller of the async
......@@ -4609,11 +4609,13 @@ Expression* Parser::RewriteAwaitExpression(Expression* value, int await_pos) {
zone());
ZoneList<Expression*>* async_function_await_args =
new (zone()) ZoneList<Expression*>(2, zone());
new (zone()) ZoneList<Expression*>(3, zone());
Expression* generator_object =
factory()->NewVariableProxy(generator_object_variable);
async_function_await_args->Add(generator_object, zone());
async_function_await_args->Add(factory()->NewVariableProxy(temp_var), zone());
async_function_await_args->Add(factory()->NewVariableProxy(promise_temp_var),
zone());
// The parser emits calls to AsyncFunctionAwaitCaught, but the
// AstNumberingVisitor will rewrite this to AsyncFunctionAwaitUncaught
......
......@@ -31,6 +31,19 @@ function rejectConstructor() {
async function argThrower(x = (() => { throw "d"; })()) { } // Exception d
async function awaitThrow() {
await undefined;
throw "e"; // Exception e
}
function constructorThrow() {
return new Promise((resolve, reject) =>
Promise.resolve().then(() =>
reject("f") // Exception f
)
);
}
function suppressThrow() {
return thrower();
}
......@@ -80,7 +93,43 @@ async function indirectAwaitCatch(producer) {
}
}
let catches = [caught, indirectCaught, indirectAwaitCatch];
function switchCatch(producer) {
let resolve;
let promise = new Promise(r => resolve = r);
async function localCaught() {
try {
await promise; // force switching to localUncaught and back
await producer();
} catch (e) { }
}
async function localUncaught() {
await undefined;
resolve();
}
localCaught();
localUncaught();
}
function switchDotCatch(producer) {
let resolve;
let promise = new Promise(r => resolve = r);
async function localCaught() {
await promise; // force switching to localUncaught and back
await producer();
}
async function localUncaught() {
await undefined;
resolve();
}
localCaught().catch(() => {});
localUncaught();
}
let catches = [caught,
indirectCaught,
indirectAwaitCatch,
switchCatch,
switchDotCatch];
let noncatches = [uncaught, indirectUncaught];
let lateCatches = [dotCatch,
indirectReturnDotCatch,
......@@ -89,18 +138,19 @@ let lateCatches = [dotCatch,
let throws = [thrower, reject, argThrower, suppressThrow];
let nonthrows = [awaitReturn, scalar, nothing];
let lateThrows = [awaitThrow, constructorThrow];
let uncatchable = [rejectConstructor];
let cases = [];
for (let producer of throws) {
for (let producer of throws.concat(lateThrows)) {
for (let consumer of catches) {
cases.push({ producer, consumer, expectedEvents: 1, caught: true });
cases.push({ producer, consumer, expectedEvents: 0, caught: false });
}
}
for (let producer of throws) {
for (let producer of throws.concat(lateThrows)) {
for (let consumer of noncatches) {
cases.push({ producer, consumer, expectedEvents: 1, caught: true });
cases.push({ producer, consumer, expectedEvents: 1, caught: false });
......@@ -121,34 +171,46 @@ for (let producer of uncatchable) {
}
}
for (let producer of throws) {
for (let producer of lateThrows) {
for (let consumer of lateCatches) {
cases.push({ producer, consumer, expectedEvents: 1, caught: true });
cases.push({ producer, consumer, expectedEvents: 1, caught: false });
cases.push({ producer, consumer, expectedEvents: 0, caught: false });
}
}
for (let {producer, consumer, expectedEvents, caught} of cases) {
Debug.setListener(listener);
if (caught) {
Debug.setBreakOnException();
} else {
Debug.setBreakOnUncaughtException();
for (let producer of throws) {
for (let consumer of lateCatches) {
cases.push({ producer, consumer, expectedEvents: 1, caught: true });
cases.push({ producer, consumer, expectedEvents: 1, caught: false });
}
}
events = 0;
consumer(producer);
%RunMicrotasks();
Debug.setListener(null);
if (caught) {
Debug.clearBreakOnException();
} else {
Debug.clearBreakOnUncaughtException();
}
if (expectedEvents != events) {
print(`producer ${producer} consumer ${consumer} expectedEvents ` +
`${expectedEvents} caught ${caught} events ${events}`);
quit(1);
function runPart(n) {
let subcases = cases.slice(n * cases.length / 4,
((n + 1) * cases.length) / 4);
for (let {producer, consumer, expectedEvents, caught} of subcases) {
Debug.setListener(listener);
if (caught) {
Debug.setBreakOnException();
} else {
Debug.setBreakOnUncaughtException();
}
events = 0;
consumer(producer);
%RunMicrotasks();
Debug.setListener(null);
if (caught) {
Debug.clearBreakOnException();
} else {
Debug.clearBreakOnUncaughtException();
}
if (expectedEvents != events) {
print(`producer ${producer} consumer ${consumer} expectedEvents ` +
`${expectedEvents} caught ${caught} events ${events}`);
quit(1);
}
}
}
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --harmony-async-await --expose-debug-as debug
// Files: test/mjsunit/harmony/async-debug-caught-exception-cases.js
runPart(0);
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --harmony-async-await --expose-debug-as debug
// Files: test/mjsunit/harmony/async-debug-caught-exception-cases.js
runPart(1);
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --harmony-async-await --expose-debug-as debug
// Files: test/mjsunit/harmony/async-debug-caught-exception-cases.js
runPart(2);
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --harmony-async-await --expose-debug-as debug
// Files: test/mjsunit/harmony/async-debug-caught-exception-cases.js
runPart(3);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment