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

Clean up promises and fix an edge case bug

This patch builds on previous Promise spec compliance work by
cleaning out some old code which existed to support
Promise.prototype.chain, rephrasing some code to correspond more
closely to the specification, and removing some incorrect brand
checking. A test is added for a bug in an edge case which was fixed.

R=rossberg
BUG=v8:3641
LOG=Y

Review URL: https://codereview.chromium.org/1488783002

Cr-Commit-Position: refs/heads/master@{#32627}
parent 412aefac
...@@ -40,9 +40,6 @@ function CreateResolvingFunctions(promise) { ...@@ -40,9 +40,6 @@ function CreateResolvingFunctions(promise) {
var resolve = function(value) { var resolve = function(value) {
if (alreadyResolved === true) return; if (alreadyResolved === true) return;
alreadyResolved = true; alreadyResolved = true;
if (value === promise) {
return PromiseReject(promise, MakeTypeError(kPromiseCyclic, value));
}
PromiseResolve(promise, value); PromiseResolve(promise, value);
}; };
...@@ -116,37 +113,11 @@ function PromiseDone(promise, status, value, promiseQueue) { ...@@ -116,37 +113,11 @@ function PromiseDone(promise, status, value, promiseQueue) {
} }
} }
function PromiseCoerce(constructor, x) {
if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {
var then;
try {
then = x.then;
} catch(r) {
return %_Call(PromiseRejected, constructor, r);
}
if (IS_CALLABLE(then)) {
var deferred = NewPromiseCapability(constructor);
try {
%_Call(then, x, deferred.resolve, deferred.reject);
} catch(r) {
deferred.reject(r);
}
return deferred.promise;
}
}
return x;
}
function PromiseHandle(value, handler, deferred) { function PromiseHandle(value, handler, deferred) {
try { try {
%DebugPushPromise(deferred.promise, PromiseHandle); %DebugPushPromise(deferred.promise, PromiseHandle);
var result = handler(value); var result = handler(value);
if (result === deferred.promise) deferred.resolve(result);
throw MakeTypeError(kPromiseCyclic, result);
else if (IsPromise(result))
%_Call(PromiseChain, result, deferred.resolve, deferred.reject);
else
deferred.resolve(result);
} catch (exception) { } catch (exception) {
try { deferred.reject(exception); } catch (e) { } try { deferred.reject(exception); } catch (e) { }
} finally { } finally {
...@@ -193,28 +164,29 @@ function PromiseCreate() { ...@@ -193,28 +164,29 @@ function PromiseCreate() {
} }
function PromiseResolve(promise, x) { function PromiseResolve(promise, x) {
if (GET_PRIVATE(promise, promiseStatusSymbol) === 0) { if (x === promise) {
if (IS_SPEC_OBJECT(x)) { return PromiseReject(promise, MakeTypeError(kPromiseCyclic, x));
// 25.4.1.3.2 steps 8-12 }
try { if (IS_SPEC_OBJECT(x)) {
var then = x.then; // 25.4.1.3.2 steps 8-12
} catch (e) { try {
return PromiseReject(promise, e); var then = x.then;
} } catch (e) {
if (IS_CALLABLE(then)) { return PromiseReject(promise, e);
// PromiseResolveThenableJob }
return %EnqueueMicrotask(function() { if (IS_CALLABLE(then)) {
try { // PromiseResolveThenableJob
var callbacks = CreateResolvingFunctions(promise); return %EnqueueMicrotask(function() {
%_Call(then, x, callbacks.resolve, callbacks.reject); try {
} catch (e) { var callbacks = CreateResolvingFunctions(promise);
PromiseReject(promise, e); %_Call(then, x, callbacks.resolve, callbacks.reject);
} } catch (e) {
}); PromiseReject(promise, e);
} }
});
} }
PromiseDone(promise, +1, x, promiseOnResolveSymbol);
} }
PromiseDone(promise, +1, x, promiseOnResolveSymbol);
} }
function PromiseReject(promise, r) { function PromiseReject(promise, r) {
...@@ -245,6 +217,7 @@ function NewPromiseCapability(C) { ...@@ -245,6 +217,7 @@ function NewPromiseCapability(C) {
} else { } else {
var result = {promise: UNDEFINED, resolve: UNDEFINED, reject: UNDEFINED }; var result = {promise: UNDEFINED, resolve: UNDEFINED, reject: UNDEFINED };
result.promise = new C(function(resolve, reject) { result.promise = new C(function(resolve, reject) {
// TODO(littledan): Check for resolve and reject being not undefined
result.resolve = resolve; result.resolve = resolve;
result.reject = reject; result.reject = reject;
}); });
...@@ -257,15 +230,13 @@ function PromiseDeferred() { ...@@ -257,15 +230,13 @@ function PromiseDeferred() {
} }
function PromiseResolved(x) { function PromiseResolved(x) {
if (this === GlobalPromise) { return %_Call(PromiseCast, this, x);
// Optimized case, avoid extra closure.
return PromiseCreateAndSet(+1, x);
} else {
return new this(function(resolve, reject) { resolve(x) });
}
} }
function PromiseRejected(r) { function PromiseRejected(r) {
if (!IS_SPEC_OBJECT(this)) {
throw MakeTypeError(kCalledOnNonObject, PromiseRejected);
}
var promise; var promise;
if (this === GlobalPromise) { if (this === GlobalPromise) {
// Optimized case, avoid extra closure. // Optimized case, avoid extra closure.
...@@ -279,15 +250,18 @@ function PromiseRejected(r) { ...@@ -279,15 +250,18 @@ function PromiseRejected(r) {
return promise; return promise;
} }
// Simple chaining. // Multi-unwrapped chaining with thenable coercion.
// PromiseChain a.k.a. flatMap function PromiseThen(onResolve, onReject) {
function PromiseChainInternal(constructor, onResolve, onReject) { var constructor = this.constructor;
onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve; onResolve = IS_CALLABLE(onResolve) ? onResolve : PromiseIdResolveHandler;
onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject; onReject = IS_CALLABLE(onReject) ? onReject : PromiseIdRejectHandler;
var deferred = NewPromiseCapability(constructor); var deferred = NewPromiseCapability(constructor);
switch (GET_PRIVATE(this, promiseStatusSymbol)) { switch (GET_PRIVATE(this, promiseStatusSymbol)) {
case UNDEFINED: case UNDEFINED:
// TODO(littledan): The type check should be called before
// constructing NewPromiseCapability; this is observable when
// erroneously copying this method to other classes.
throw MakeTypeError(kNotAPromise, this); throw MakeTypeError(kNotAPromise, this);
case 0: // Pending case 0: // Pending
GET_PRIVATE(this, promiseOnResolveSymbol).push(onResolve, deferred); GET_PRIVATE(this, promiseOnResolveSymbol).push(onResolve, deferred);
...@@ -317,47 +291,25 @@ function PromiseChainInternal(constructor, onResolve, onReject) { ...@@ -317,47 +291,25 @@ function PromiseChainInternal(constructor, onResolve, onReject) {
return deferred.promise; return deferred.promise;
} }
// Chain is left around for now as an alias for then
function PromiseChain(onResolve, onReject) { function PromiseChain(onResolve, onReject) {
return %_Call(PromiseChainInternal, this, this.constructor, return %_Call(PromiseThen, this, onResolve, onReject);
onResolve, onReject);
} }
function PromiseCatch(onReject) { function PromiseCatch(onReject) {
return this.then(UNDEFINED, onReject); return this.then(UNDEFINED, onReject);
} }
// Multi-unwrapped chaining with thenable coercion.
function PromiseThen(onResolve, onReject) {
onResolve = IS_CALLABLE(onResolve) ? onResolve : PromiseIdResolveHandler;
onReject = IS_CALLABLE(onReject) ? onReject : PromiseIdRejectHandler;
var that = this;
var constructor = this.constructor;
return %_Call(
PromiseChainInternal,
this,
constructor,
function(x) {
x = PromiseCoerce(constructor, x);
if (x === that) {
return onReject(MakeTypeError(kPromiseCyclic, x));
} else if (IsPromise(x)) {
return x.then(onResolve, onReject);
} else {
return onResolve(x);
}
},
onReject
);
}
// Combinators. // Combinators.
function PromiseCast(x) { function PromiseCast(x) {
if (!IS_SPEC_OBJECT(this)) {
throw MakeTypeError(kCalledOnNonObject, PromiseCast);
}
if (IsPromise(x) && x.constructor === this) { if (IsPromise(x) && x.constructor === this) {
return x; return x;
} else { } else {
return new this(function(resolve) { resolve(x) }); return new this(function(resolve, reject) { resolve(x) });
} }
} }
......
...@@ -193,8 +193,9 @@ function assertAsyncDone(iteration) { ...@@ -193,8 +193,9 @@ function assertAsyncDone(iteration) {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
// Note: Chain now has then-style semantics, here and in future tests.
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "resolved/chain") }, function(x) { assertAsync(x === 5, "resolved/chain") },
assertUnreachable assertUnreachable
) )
assertAsyncRan() assertAsyncRan()
...@@ -216,8 +217,8 @@ function assertAsyncDone(iteration) { ...@@ -216,8 +217,8 @@ function assertAsyncDone(iteration) {
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "rejected/chain") }, assertUnreachable,
assertUnreachable function(x) { assertAsync(x === 5, "rejected/chain") }
) )
assertAsyncRan() assertAsyncRan()
})(); })();
...@@ -233,17 +234,16 @@ function assertAsyncDone(iteration) { ...@@ -233,17 +234,16 @@ function assertAsyncDone(iteration) {
assertAsyncRan() assertAsyncRan()
})(); })();
/* TODO(caitp): remove tests once PromiseChain is removed, per bug 3237
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain(function(x) { return x }, assertUnreachable).chain( p3.chain(function(x) { return x }, assertUnreachable).chain(
function(x) { assertAsync(x === p1, "resolved/chain/chain") }, function(x) { assertAsync(x === 5, "resolved/chain/chain") },
assertUnreachable assertUnreachable
) )
assertAsyncRan() assertAsyncRan()
})();*/ })();
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
...@@ -371,7 +371,7 @@ function assertAsyncDone(iteration) { ...@@ -371,7 +371,7 @@ function assertAsyncDone(iteration) {
var p2 = {then: function(onResolve, onReject) { onResolve(p1) }} var p2 = {then: function(onResolve, onReject) { onResolve(p1) }}
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "resolved/thenable/chain") }, function(x) { assertAsync(x === 5, "resolved/thenable/chain") },
assertUnreachable assertUnreachable
) )
assertAsyncRan() assertAsyncRan()
...@@ -393,8 +393,8 @@ function assertAsyncDone(iteration) { ...@@ -393,8 +393,8 @@ function assertAsyncDone(iteration) {
var p2 = {then: function(onResolve, onReject) { onResolve(p1) }} var p2 = {then: function(onResolve, onReject) { onResolve(p1) }}
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "rejected/thenable/chain") }, assertUnreachable,
assertUnreachable function(x) { assertAsync(x === 5, "rejected/thenable/chain") }
) )
assertAsyncRan() assertAsyncRan()
})(); })();
...@@ -416,7 +416,7 @@ function assertAsyncDone(iteration) { ...@@ -416,7 +416,7 @@ function assertAsyncDone(iteration) {
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/resolve") }, function(x) { assertAsync(x === 5, "chain/resolve") },
assertUnreachable assertUnreachable
) )
deferred.resolve(5) deferred.resolve(5)
...@@ -426,8 +426,8 @@ function assertAsyncDone(iteration) { ...@@ -426,8 +426,8 @@ function assertAsyncDone(iteration) {
(function() { (function() {
var deferred = Promise.defer() var deferred = Promise.defer()
var p1 = deferred.promise var p1 = deferred.promise
var p2 = Promise.accept(p1) var p2 = Promise.resolve(p1)
var p3 = Promise.accept(p2) var p3 = Promise.resolve(p2)
p3.then( p3.then(
function(x) { assertAsync(x === 5, "then/resolve") }, function(x) { assertAsync(x === 5, "then/resolve") },
assertUnreachable assertUnreachable
...@@ -442,8 +442,8 @@ function assertAsyncDone(iteration) { ...@@ -442,8 +442,8 @@ function assertAsyncDone(iteration) {
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/reject") }, assertUnreachable,
assertUnreachable function(x) { assertAsync(x === 5, "chain/reject") }
) )
deferred.reject(5) deferred.reject(5)
assertAsyncRan() assertAsyncRan()
...@@ -492,7 +492,7 @@ function assertAsyncDone(iteration) { ...@@ -492,7 +492,7 @@ function assertAsyncDone(iteration) {
var p2 = {then: function(onResolve, onReject) { onResolve(p1) }} var p2 = {then: function(onResolve, onReject) { onResolve(p1) }}
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/resolve/thenable") }, function(x) { assertAsync(x === 5, "chain/resolve/thenable") },
assertUnreachable assertUnreachable
) )
deferred.resolve(5) deferred.resolve(5)
...@@ -518,8 +518,8 @@ function assertAsyncDone(iteration) { ...@@ -518,8 +518,8 @@ function assertAsyncDone(iteration) {
var p2 = {then: function(onResolve, onReject) { onResolve(p1) }} var p2 = {then: function(onResolve, onReject) { onResolve(p1) }}
var p3 = Promise.accept(p2) var p3 = Promise.accept(p2)
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/reject/thenable") }, assertUnreachable,
assertUnreachable function(x) { assertAsync(x === 5, "chain/reject/thenable") }
) )
deferred.reject(5) deferred.reject(5)
assertAsyncRan() assertAsyncRan()
...@@ -538,19 +538,18 @@ function assertAsyncDone(iteration) { ...@@ -538,19 +538,18 @@ function assertAsyncDone(iteration) {
assertAsyncRan() assertAsyncRan()
})(); })();
/* TODO(caitp): remove tests once PromiseChain is removed, per bug v8:3237
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
var p2 = Promise.accept(p1) var p2 = Promise.accept(p1)
var deferred = Promise.defer() var deferred = Promise.defer()
var p3 = deferred.promise var p3 = deferred.promise
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/resolve2") }, function(x) { assertAsync(x === 5, "chain/resolve2") },
assertUnreachable assertUnreachable
) )
deferred.resolve(p2) deferred.resolve(p2)
assertAsyncRan() assertAsyncRan()
})(); */ })();
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
...@@ -591,19 +590,18 @@ function assertAsyncDone(iteration) { ...@@ -591,19 +590,18 @@ function assertAsyncDone(iteration) {
assertAsyncRan() assertAsyncRan()
})(); })();
/* TODO(caitp): remove tests once PromiseChain is removed, per bug v8:3237
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
var p2 = {then: function(onResolve, onReject) { onResolve(p1) }} var p2 = {then: function(onResolve, onReject) { onResolve(p1) }}
var deferred = Promise.defer() var deferred = Promise.defer()
var p3 = deferred.promise var p3 = deferred.promise
p3.chain( p3.chain(
function(x) { assertAsync(x === p2, "chain/resolve/thenable2") }, function(x) { assertAsync(x === 5, "chain/resolve/thenable2") },
assertUnreachable assertUnreachable
) )
deferred.resolve(p2) deferred.resolve(p2)
assertAsyncRan() assertAsyncRan()
})(); */ })();
(function() { (function() {
var p1 = Promise.accept(5) var p1 = Promise.accept(5)
...@@ -638,19 +636,17 @@ function assertAsyncDone(iteration) { ...@@ -638,19 +636,17 @@ function assertAsyncDone(iteration) {
assertAsyncRan() assertAsyncRan()
})(); })();
/* TODO(caitp): remove tests once PromiseChain is removed, per bug v8:3237
(function() { (function() {
var deferred = Promise.defer() var deferred = Promise.defer()
var p = deferred.promise var p = deferred.promise
deferred.resolve(p) deferred.resolve(p)
p.chain( p.chain(
function(x) { assertAsync(x === p, "cyclic/deferred/chain") }, assertUnreachable,
assertUnreachable function(r) { assertAsync(r instanceof TypeError, "cyclic/deferred/then") }
) )
assertAsyncRan() assertAsyncRan()
})();*/ })();
/* TODO(caitp): remove tests once PromiseChain is removed, per bug v8:3237
(function() { (function() {
var deferred = Promise.defer() var deferred = Promise.defer()
var p = deferred.promise var p = deferred.promise
...@@ -660,7 +656,7 @@ function assertAsyncDone(iteration) { ...@@ -660,7 +656,7 @@ function assertAsyncDone(iteration) {
function(r) { assertAsync(r instanceof TypeError, "cyclic/deferred/then") } function(r) { assertAsync(r instanceof TypeError, "cyclic/deferred/then") }
) )
assertAsyncRan() assertAsyncRan()
})();*/ })();
(function() { (function() {
Promise.all([]).chain( Promise.all([]).chain(
...@@ -763,7 +759,6 @@ function assertAsyncDone(iteration) { ...@@ -763,7 +759,6 @@ function assertAsyncDone(iteration) {
assertAsyncRan() assertAsyncRan()
})(); })();
(function() { (function() {
'use strict'; 'use strict';
var getCalls = 0; var getCalls = 0;
......
...@@ -54,6 +54,12 @@ ...@@ -54,6 +54,12 @@
# Issue 3784: setters-on-elements is flaky # Issue 3784: setters-on-elements is flaky
'setters-on-elements': [PASS, FAIL], 'setters-on-elements': [PASS, FAIL],
# Issue 3641: The new 'then' semantics suppress some exceptions.
# These tests may be changed or removed when 'chain' is deprecated.
'es6/debug-promises/reject-with-throw-in-reject': [FAIL],
'es6/debug-promises/reject-with-undefined-reject': [FAIL],
'es6/debug-promises/reject-with-invalid-reject': [FAIL],
############################################################################## ##############################################################################
# TurboFan compiler failures. # TurboFan compiler failures.
......
// Copyright 2015 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
// If a Promise's then method is overridden, that should be respected
// even if the promise is already resolved. x's resolution function is
// only called by Promise.resolve(); there shouldn't be a resolution
// check before when calling x.then.
// Async assert framework copied from mjsunit/es6/promises.js
var asyncAssertsExpected = 0;
function assertAsyncRan() { ++asyncAssertsExpected }
function assertLater(f, name) {
assertFalse(f()); // should not be true synchronously
++asyncAssertsExpected;
var iterations = 0;
function runAssertion() {
if (f()) {
print(name, "succeeded");
--asyncAssertsExpected;
} else if (iterations++ < 10) {
%EnqueueMicrotask(runAssertion);
} else {
%AbortJS(name + " FAILED!");
}
}
%EnqueueMicrotask(runAssertion);
}
function assertAsyncDone(iteration) {
var iteration = iteration || 0;
%EnqueueMicrotask(function() {
if (asyncAssertsExpected === 0)
assertAsync(true, "all")
else if (iteration > 10) // Shouldn't take more.
assertAsync(false, "all... " + asyncAssertsExpected)
else
assertAsyncDone(iteration + 1)
});
}
// End async assert framework
var y;
var x = Promise.resolve();
x.then = () => { y = true; }
Promise.resolve().then(() => x);
assertLater(() => y === true, "y === true");
assertAsyncDone();
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