Bug 1185106 - Part 2: Implement ShellPromise. (r=till)

This commit is contained in:
Mariusz Kierski 2015-10-05 13:24:03 -07:00
Родитель bc4f9d9099
Коммит 9963b230dc
11 изменённых файлов: 513 добавлений и 18 удалений

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

@ -8,6 +8,8 @@
#include "jscntxt.h"
#include "builtin/SelfHostingDefines.h"
#include "jsobjinlines.h"
using namespace js;
@ -26,19 +28,61 @@ static const JSFunctionSpec promise_static_methods[] = {
JS_FS_END
};
static bool
namespace js {
bool
PromiseConstructor(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
JSObject* obj = NewBuiltinClassInstance(cx, &ShellPromiseObject::class_);
if (!obj)
if (args.length() == 0) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
"ShellPromise.constructor", "0", "s");
return false;
// TODO: store the resolve and reject callbacks.
args.rval().setObject(*obj);
}
HandleValue fn = args.get(0);
if (!IsCallable(fn)) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NOT_CALLABLE,
"Argument 1 of ShellPromise.constructor");
return false;
}
RootedObject promise(cx, NewBuiltinClassInstance(cx, &ShellPromiseObject::class_));
if (!promise)
return false;
JS_SetReservedSlot(promise, PROMISE_STATE_SLOT, NumberValue(PROMISE_STATE_PENDING));
JS_SetReservedSlot(promise, PROMISE_VALUE_SLOT, NullValue());
JS_SetReservedSlot(promise, PROMISE_DEFERREDS_SLOT, NullValue());
RootedValue initRval(cx);
JS::AutoValueVector argValues(cx);
argValues.append(ObjectValue(*promise));
argValues.append(fn);
HandleValueArray arr(argValues);
JSAtom* promiseInitAtom;
if (!(promiseInitAtom = Atomize(cx, "Promise_init", 12)))
return false;
RootedPropertyName name(cx, promiseInitAtom->asPropertyName());
RootedValue selfHostedFun(cx);
if (!GlobalObject::getIntrinsicValue(cx, cx->global(), name, &selfHostedFun))
return false;
if (!JS_CallFunctionValue(cx, promise, selfHostedFun, arr, &initRval))
return false;
args.rval().setObject(*promise);
return true;
}
}
static JSObject*
CreatePromisePrototype(JSContext* cx, JSProtoKey key)
{
@ -47,7 +91,7 @@ CreatePromisePrototype(JSContext* cx, JSProtoKey key)
const Class ShellPromiseObject::class_ = {
"ShellPromise",
JSCLASS_HAS_CACHED_PROTO(JSProto_ShellPromise),
JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | JSCLASS_HAS_CACHED_PROTO(JSProto_ShellPromise),
nullptr, /* addProperty */
nullptr, /* delProperty */
nullptr, /* getProperty */

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

@ -16,10 +16,14 @@ class AutoSetNewObjectMetadata;
class ShellPromiseObject : public NativeObject
{
public:
static const unsigned RESERVED_SLOTS = 3;
static const Class class_;
static const Class protoClass_;
};
bool
PromiseConstructor(JSContext* cx, unsigned argc, Value* vp);
} // namespace js
#endif /* builtin_Promise_h */

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

@ -2,26 +2,240 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
function Promise_all(iterable) {
global.print("Hi, I'm Promise_all. Please implement me.");
/**
* This implementation is by no means complete and is using a polyfill.
* In particular, it doesn't fully comply to ES6 Promises spec.
* The list of incompatibilities may not be complete and includes:
*
* - no [Symbol.species] implementation
* - implementation is not really async at all, but all is executed
* in correct order
* - Promise.race is not implemented (not necessary for async/await)
* - Promise.all implementation currently only handles arrays, no other
* iterables.
*/
/**
* This polyfill implements the Promise specification in the following way:
* At first, Promise is initialized with the intrinsic Promise constructor,
* with its initialization completed by calling Promise_init. The goal is to
* completely resolve/reject the promise. There are certain helper functions:
* - resolveOneStep() executes "one step" of the promise - which means
* getting either the final value or next promise towards complete execution.
* - resolveCompletely() executes the entire promise, which merely means
* resolving one step at a time, until the final step no longer resolves
* to a promise (which means either resolving to a value or rejecting).
*
* Once resolution is finished, resolveCompletely() is called in order to
* update state of the promise. It also spawns callbacks that may have been
* deferred with a then() - they are NOT normally taken into consideration,
* because resolveCompletely() just runs one path.
*/
/**
* Below is a simple simulation of an event loop. Events enqueued using
* this setTimeout implementation must be manually triggered by calling
* runEvents().
*/
var eventQueue = new List();
function runEvents() {
while (eventQueue.length > 0) {
var evt = callFunction(std_Array_pop, eventQueue);
evt();
}
}
function Promise_race(iterable) {
global.print("Hi, I'm Promise_race. Please implement me.");
function setTimeout(cb, interval) {
if (!IsCallable(cb))
ThrowTypeError(JSMSG_NOT_CALLABLE, "Argument 0");
if (interval !== 0)
ThrowTypeError(JSMSG_SETTIMEOUT_INTERVAL_NONZERO);
callFunction(std_Array_push, eventQueue, cb);
}
function Promise_reject(iterable) {
global.print("Hi, I'm Promise_reject. Please implement me.");
function Handler(onFulfilled, onRejected, resolve, reject) {
this.onFulfilled = IsCallable(onFulfilled) ? onFulfilled : null;
this.onRejected = IsCallable(onRejected) ? onRejected : null;
this.resolve = resolve;
this.reject = reject;
}
function Promise_resolve(iterable) {
global.print("Hi, I'm Promise_resolve. Please implement me.");
MakeConstructible(Handler, std_Object_create(null));
function Promise_setState(promise, state) {
UnsafeSetReservedSlot(promise, PROMISE_STATE_SLOT, state);
}
function Promise_getState(promise) {
return UnsafeGetReservedSlot(promise, PROMISE_STATE_SLOT);
}
function Promise_setDeferreds(promise, deferreds) {
UnsafeSetReservedSlot(promise, PROMISE_DEFERREDS_SLOT, deferreds);
}
function Promise_getDeferreds(promise) {
return UnsafeGetReservedSlot(promise, PROMISE_DEFERREDS_SLOT);
}
function Promise_setValue(promise, value) {
UnsafeSetReservedSlot(promise, PROMISE_VALUE_SLOT, value);
}
function Promise_getValue(promise) {
return UnsafeGetReservedSlot(promise, PROMISE_VALUE_SLOT);
}
function Promise_init(promise, fn) {
Promise_setDeferreds(promise, new List());
resolveOneStep(fn,
function(value) { resolveCompletely(promise, value); },
function(reason) { reject(promise, reason); });
}
function Promise_isThenable(valueOrPromise) {
if (valueOrPromise && (typeof valueOrPromise === 'object' || IsCallable(valueOrPromise))) {
var then = valueOrPromise.then;
if (IsCallable(then))
return true;
}
return false;
}
function Promise_all(promises) {
var length = promises.length;
var results = [];
return NewPromise(function (resolve, reject) {
if (length === 0)
return resolve([]);
var remaining = length;
function resolveChain(index, valueOrPromise) {
try {
if (Promise_isThenable(valueOrPromise)) {
callFunction(valueOrPromise.then, valueOrPromise,
function (valueOrPromise) { resolveChain(index, valueOrPromise); },
reject);
} else {
_DefineDataProperty(results, index, valueOrPromise);
if (--remaining === 0)
resolve(results);
}
} catch (ex) {
reject(ex);
}
}
for (var i = 0; i < length; i++)
resolveChain(i, promises[i]);
});
}
function Promise_race(values) {
ThrowTypeError(JSMSG_NOT_IMPLEMENTED, "Promise.race");
}
function Promise_reject(value) {
return NewPromise(function (resolve, reject) {
reject(value);
});
}
function Promise_resolve(value) {
if (value && typeof value === 'object' && IsPromise(value))
return value;
return NewPromise(function (resolve) {
resolve(value);
});
}
function Promise_catch(onRejected) {
global.print("Hi, I'm Promise_catch. Please implement me.");
return callFunction(Promise_then, this, undefined, onRejected);
}
function asap(cb) {
setTimeout(cb, 0);
}
function deferOrExecute(promise, deferred) {
if (Promise_getState(promise) === PROMISE_STATE_PENDING) {
Promise_getDeferreds(promise).push(deferred);
return;
}
asap(function() {
var cb = Promise_getState(promise) === PROMISE_STATE_RESOLVED ?
deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
var value = Promise_getValue(promise);
(Promise_getState(promise) === PROMISE_STATE_RESOLVED ? deferred.resolve : deferred.reject)(value);
return;
}
var returnValue;
try {
returnValue = cb(Promise_getValue(promise));
} catch (e) {
deferred.reject(e);
return;
}
deferred.resolve(returnValue);
});
}
function resolveOneStep(fn, onFulfilled, onRejected) {
var done = false;
var callOnce = function(cb) {
return function(value) {
if (done) return;
done = true;
cb(value);
};
};
try {
fn(callOnce(onFulfilled), callOnce(onRejected));
} catch (ex) {
callOnce(onRejected)(ex);
}
}
function resolveCompletely(promise, valueOrPromise) {
try {
// FIXME this is probably not a type error
if (valueOrPromise === promise)
ThrowTypeError(JSMSG_PROMISE_RESOLVED_WITH_ITSELF);
if (Promise_isThenable(valueOrPromise)) {
resolveOneStep(function(resolve, reject) { valueOrPromise.then(resolve, reject); },
function(value) { resolveCompletely(promise, value); },
function(value) { reject(promise, value); });
}
else
callFunction(resolvingFinished, promise, PROMISE_STATE_RESOLVED, valueOrPromise);
} catch (ex) {
callFunction(reject, promise, ex);
}
}
function reject(promise, reason) {
callFunction(resolvingFinished, promise, PROMISE_STATE_REJECTED, reason);
}
function resolvingFinished(state, newValue) {
Promise_setState(this, state);
Promise_setValue(this, newValue);
var deferreds = Promise_getDeferreds(this);
for (var i = 0, len = deferreds.length; i < len; i++)
deferOrExecute(this, deferreds[i]);
Promise_setDeferreds(this, null);
}
function Promise_then(onFulfilled, onRejected) {
global.print("Hi, I'm Promise_then. Please implement me.");
var promise = this;
var newPromise = NewPromise(function(resolve, reject) {
deferOrExecute(promise,
new Handler(onFulfilled, onRejected, resolve, reject));
});
runEvents();
return newPromise;
}

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

@ -33,6 +33,15 @@
// Stores the private WeakMap slot used for WeakSets
#define WEAKSET_MAP_SLOT 0
// Slots for use with promises implementation.
#define PROMISE_STATE_SLOT 0
#define PROMISE_VALUE_SLOT 1
#define PROMISE_DEFERREDS_SLOT 2
#define PROMISE_STATE_PENDING 0
#define PROMISE_STATE_RESOLVED 1
#define PROMISE_STATE_REJECTED 2
#define ITERATOR_SLOT_TARGET 0
// Used for collection iterators.
#define ITERATOR_SLOT_RANGE 1

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

@ -106,6 +106,7 @@ MSG_DEF(JSMSG_TERMINATED, 1, JSEXN_ERR, "Script terminated by timeo
MSG_DEF(JSMSG_PROTO_NOT_OBJORNULL, 1, JSEXN_TYPEERR, "{0}.prototype is not an object or null")
MSG_DEF(JSMSG_CANT_CALL_CLASS_CONSTRUCTOR, 0, JSEXN_TYPEERR, "class constructors must be invoked with |new|")
MSG_DEF(JSMSG_DISABLED_DERIVED_CLASS, 1, JSEXN_INTERNALERR, "{0} temporarily disallowed in derived class constructors")
MSG_DEF(JSMSG_NOT_CALLABLE, 1, JSEXN_TYPEERR, "{0} is not callable")
// JSON
MSG_DEF(JSMSG_JSON_BAD_PARSE, 3, JSEXN_SYNTAXERR, "JSON.parse: {0} at line {1} column {2} of the JSON data")
@ -508,6 +509,10 @@ MSG_DEF(JSMSG_NO_INDEXED_SETTER, 2, JSEXN_TYPEERR, "{0} doesn't have an
// Super
MSG_DEF(JSMSG_CANT_DELETE_SUPER, 0, JSEXN_REFERENCEERR, "invalid delete involving 'super'")
// Promise
MSG_DEF(JSMSG_PROMISE_RESOLVED_WITH_ITSELF, 0, JSEXN_TYPEERR, "A promise cannot be resolved with itself")
MSG_DEF(JSMSG_SETTIMEOUT_INTERVAL_NONZERO, 0, JSEXN_TYPEERR, "Intervals other than 0 are not supported")
// Modules
MSG_DEF(JSMSG_BAD_DEFAULT_EXPORT, 0, JSEXN_SYNTAXERR, "default export cannot be provided by export *")
MSG_DEF(JSMSG_MISSING_INDIRECT_EXPORT, 0, JSEXN_SYNTAXERR, "indirect export not found")

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

@ -1537,6 +1537,68 @@ PrintErr(JSContext* cx, unsigned argc, Value* vp)
return PrintInternal(cx, args, gErrFile);
}
static bool
SetTimeout(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 2) {
JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_MORE_ARGS_NEEDED,
"setTimeout", args.length() == 0 ? "0" : "1", "s");
return false;
}
RootedValue rval(cx);
JS::AutoValueVector argValues(cx);
argValues.append(args.get(0));
argValues.append(args.get(1));
HandleValueArray arr(argValues);
JSAtom* setTimeoutAtom;
if (!(setTimeoutAtom = Atomize(cx, "setTimeout", 10)))
return false;
RootedPropertyName name(cx, setTimeoutAtom->asPropertyName());
RootedValue selfHostedFun(cx);
if (!GlobalObject::getIntrinsicValue(cx, cx->global(), name, &selfHostedFun))
return false;
RootedObject undef(cx);
if (!JS_CallFunctionValue(cx, undef, selfHostedFun, arr, &rval))
return false;
args.rval().set(rval);
return true;
}
static bool
RunEvents(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
RootedValue rval(cx);
JS::AutoValueVector argValues(cx);
HandleValueArray arr(argValues);
JSAtom* runEvents;
if (!(runEvents = Atomize(cx, "runEvents", 9)))
return false;
RootedPropertyName name(cx, runEvents->asPropertyName());
RootedValue selfHostedFun(cx);
if (!GlobalObject::getIntrinsicValue(cx, cx->global(), name, &selfHostedFun))
return false;
RootedObject undef(cx);
if (!JS_CallFunctionValue(cx, undef, selfHostedFun, arr, &rval))
return false;
args.rval().set(rval);
return true;
}
static bool
Help(JSContext* cx, unsigned argc, Value* vp);
@ -4598,6 +4660,15 @@ static const JSFunctionSpecWithHelp shell_functions[] = {
"dateNow()",
" Return the current time with sub-ms precision."),
JS_FN_HELP("setTimeout", SetTimeout, 2, 2,
"setTimeout(fn, timeout)",
" Execute a function after a specified timeout. Currently only 0 is supported."),
JS_FN_HELP("runEvents", RunEvents, 2, 2,
"runEvents()",
" Run events that were scheduled using setTimeout() calls.\n"
" This call is required, because there is no real event loop."),
JS_FN_HELP("help", Help, 0, 0,
"help([name ...])",
" Display usage and help messages."),

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

@ -0,0 +1,45 @@
/**
* Because there is no way to "wait" for a callback in this testing system,
* there was a need to make one big promise out of all test cases.
*/
var Promise = ShellPromise;
Promise.all([
assertEventuallyEq(new Promise(resolve => resolve(2)), 2),
assertEventuallyThrows(new Promise((_, reject) => reject(new Error())), Error),
assertEventuallyThrows(new Promise(() => { throw new Error(); }), Error),
assertEventuallyEq(new Promise(resolve => resolve())
.then(() => 3), 3),
assertEventuallyEq(new Promise(resolve => resolve())
.then(() => new Promise(r => r(3))), 3),
assertEventuallyEq(new Promise((_, reject) => reject(new Error()))
.catch(() => new Promise(r => r(3))), 3),
assertEventuallyThrows(new Promise(resolve => resolve())
.then(() => { throw new Error(); }), Error),
assertEventuallyEq(new Promise((_, reject) => reject(new Error()))
.catch(() => 4), 4),
assertEventuallyEq(Promise.resolve(5), 5),
assertEventuallyThrows(Promise.reject(new Error()), Error),
assertEventuallyDeepEq(Promise.all([]), []),
assertEventuallyDeepEq(Promise.all(Array(10).fill()
.map((_, id) => new Promise(resolve => resolve(id)))),
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
])
.then(() => {
if (typeof reportCompare === "function")
reportCompare(true, true);
});

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

@ -0,0 +1,12 @@
var Promise = ShellPromise;
var oldThen = Promise.prototype.then;
// Changing then() should not break catch()
Promise.prototype.then = function() { throw new Error(); };
new Promise(a => { throw new Error(); })
.catch(() => {
if (typeof reportCompare === "function")
reportCompare(true, true);
});

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

@ -0,0 +1,23 @@
/**
* These functions are inspired by chai-as-promised library for promise testing
* in JS applications. They check if promises eventually resolve to a given value
* or are rejected with a specified error type.
*/
if (typeof assertEventuallyEq === 'undefined') {
assertEventuallyEq = function(promise, expected) {
return promise.then(actual => assertEq(actual, expected));
};
}
if (typeof assertEventuallyThrows === 'undefined') {
assertEventuallyThrows = function(promise, expectedErrorType) {
return promise.catch(actualE => assertEq(actualE instanceof expectedErrorType, true));
};
}
if (typeof assertEventuallyDeepEq === 'undefined') {
assertEventuallyDeepEq = function(promise, expected) {
return promise.then(actual => assertDeepEq(actual, expected));
};
}

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

@ -23,6 +23,7 @@
#include "builtin/MapObject.h"
#include "builtin/ModuleObject.h"
#include "builtin/Object.h"
#include "builtin/Promise.h"
#include "builtin/Reflect.h"
#include "builtin/SelfHostingDefines.h"
#include "builtin/SIMD.h"
@ -439,6 +440,54 @@ intrinsic_GetIteratorPrototype(JSContext* cx, unsigned argc, Value* vp)
return true;
}
static bool
intrinsic_NewPromise(JSContext* cx, unsigned argc, Value* vp)
{
return js::PromiseConstructor(cx, argc, vp);
}
static bool
intrinsic_IsPromise(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 1);
MOZ_ASSERT(args[0].isObject());
bool isPromise = args[0].toObject().getClass() == &ShellPromiseObject::class_;
args.rval().setBoolean(isPromise);
return true;
}
static bool
intrinsic_SetFunName(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 2);
MOZ_ASSERT(args[0].isObject() && args[0].toObject().is<JSFunction>());
MOZ_ASSERT(args[1].isString());
JSAtom* atom = AtomizeString(cx, args[1].toString());
if (atom == nullptr)
return false;
RootedFunction fun(cx, &args[0].toObject().as<JSFunction>());
fun->setFlags(fun->flags() & ~JSFunction::HAS_GUESSED_ATOM);
fun->initAtom(atom);
return true;
}
static bool
intrinsic_GetFunName(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 1);
MOZ_ASSERT(args[0].isObject() && args[0].toObject().is<JSFunction>());
PropertyName* name = args[0].toObject().as<JSFunction>().name();
if (!name)
args.rval().setUndefined();
else
args.rval().setString(name);
return true;
}
static bool
intrinsic_NewArrayIterator(JSContext* cx, unsigned argc, Value* vp)
{
@ -1192,6 +1241,20 @@ intrinsic_IsWeakSet(JSContext* cx, unsigned argc, Value* vp)
return true;
}
bool
intrinsic_SetFunctionExtendedSlot(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
MOZ_ASSERT(args.length() == 3);
MOZ_ASSERT(args[0].isObject());
MOZ_ASSERT(args[0].toObject().is<JSFunction>());
MOZ_ASSERT(args[1].isInt32());
args[0].toObject().as<JSFunction>().setExtendedSlot(args[1].toPrivateUint32(), args[2]);
args.rval().setUndefined();
return true;
}
/**
* Returns the default locale as a well-formed, but not necessarily canonicalized,
* BCP-47 language tag.
@ -1431,6 +1494,7 @@ static const JSFunctionSpec intrinsic_functions[] = {
JS_FN("_ConstructorForTypedArray", intrinsic_ConstructorForTypedArray, 1,0),
JS_FN("DecompileArg", intrinsic_DecompileArg, 2,0),
JS_FN("RuntimeDefaultLocale", intrinsic_RuntimeDefaultLocale, 0,0),
JS_FN("SetFunctionExtendedSlot", intrinsic_SetFunctionExtendedSlot, 3,0),
JS_FN("LocalTZA", intrinsic_LocalTZA, 0,0),
JS_INLINABLE_FN("_IsConstructing", intrinsic_IsConstructing, 0,0,
@ -1518,7 +1582,11 @@ static const JSFunctionSpec intrinsic_functions[] = {
CallNonGenericSelfhostedMethod<Is<StarGeneratorObject>>, 2, 0),
JS_FN("IsWeakSet", intrinsic_IsWeakSet, 1,0),
JS_FN("NewPromise", intrinsic_NewPromise, 1,0),
JS_FN("IsPromise", intrinsic_IsPromise, 1,0),
JS_FN("SetFunName", intrinsic_SetFunName, 2,0),
JS_FN("GetFunName", intrinsic_GetFunName, 1,0),
// See builtin/TypedObject.h for descriptors of the typedobj functions.
JS_FN("NewOpaqueTypedObject", js::NewOpaqueTypedObject, 1, 0),
JS_FN("NewDerivedTypedObject", js::NewDerivedTypedObject, 3, 0),

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

@ -29,11 +29,11 @@ namespace js {
*
* https://developer.mozilla.org/en-US/docs/SpiderMonkey/Internals/Bytecode
*/
static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 310;
static const uint32_t XDR_BYTECODE_VERSION_SUBTRAHEND = 311;
static const uint32_t XDR_BYTECODE_VERSION =
uint32_t(0xb973c0de - XDR_BYTECODE_VERSION_SUBTRAHEND);
static_assert(JSErr_Limit == 415,
static_assert(JSErr_Limit == 418,
"GREETINGS, POTENTIAL SUBTRAHEND INCREMENTER! If you added or "
"removed MSG_DEFs from js.msg, you should increment "
"XDR_BYTECODE_VERSION_SUBTRAHEND and update this assertion's "