Skip to content

Commit

Permalink
[MERGE #4668 @rhuanjl] Implement Promise.prototype.finally Fixes #3520
Browse files Browse the repository at this point in the history
Merge pull request #4668 from rhuanjl:experiment

This PR implements Promise.prototype.finally.

Fixes #3520
Relevant ECMASpec: https://tc39.github.io/ecma262/#sec-promise.prototype.finally

**Notes:**
1. I worked out how to do this by reading the code for Promise.prototype.catch and the internal AsyncSpawnExecutorFunction - there is a chance that some of what I've done isn't quite right though it does all seem to work.
2. I've run this against the relevant testcases from test262 and it passes them.
3. It probably could do with a few more CI tests - I added what seemed good to me based on what was there for other promise methods.
4. The large size of the diff is as I had to regenerate the bytecode for built in methods after adding the entry point for finally to JnDirectFields.h

**cc:** @dilijev @ljharb

Thanks to my friend @fatcerberus for running the RegenerateBytecode script for me.
  • Loading branch information
boingoing committed Feb 15, 2018
2 parents 46085eb + b8a38ed commit 3a28b84
Show file tree
Hide file tree
Showing 20 changed files with 4,512 additions and 4,038 deletions.
1 change: 1 addition & 0 deletions lib/Runtime/Base/JnDirectFields.h
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ ENTRY2(false_, _u("false")) // "false" cannot be an identifier in C++ so using "
ENTRY(flags)
ENTRY(fill)
ENTRY(filter)
ENTRY(finally)
ENTRY(find)
ENTRY(findIndex)
ENTRY(fixed)
Expand Down
4 changes: 2 additions & 2 deletions lib/Runtime/ByteCode/ByteCodeCacheReleaseFileVersion.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
//-------------------------------------------------------------------------------------------------------
// NOTE: If there is a merge conflict the correct fix is to make a new GUID.

// {3A82B6DA-8211-48BD-AB78-A3A92520F7E3}
// {5E35A82C-3DF6-456D-807F-F87DFE3D43D0}
const GUID byteCodeCacheReleaseFileVersion =
{ 0x3A82B6DA, 0x8211, 0x48BD, { 0xAB, 0x78, 0xA3, 0xA9, 0x25, 0x20, 0xF7, 0xE3 } };
{ 0x5e35a82c, 0x3df6, 0x456d, { 0x80, 0x7f, 0xf8, 0x7d, 0xfe, 0x3d, 0x43, 0xd0 } };
1,914 changes: 957 additions & 957 deletions lib/Runtime/Library/InJavascript/Intl.js.bc.32b.h
100644 → 100755

Large diffs are not rendered by default.

1,922 changes: 961 additions & 961 deletions lib/Runtime/Library/InJavascript/Intl.js.bc.64b.h
100644 → 100755

Large diffs are not rendered by default.

1,906 changes: 953 additions & 953 deletions lib/Runtime/Library/InJavascript/Intl.js.nojit.bc.32b.h
100644 → 100755

Large diffs are not rendered by default.

1,906 changes: 953 additions & 953 deletions lib/Runtime/Library/InJavascript/Intl.js.nojit.bc.64b.h
100644 → 100755

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/Runtime/Library/JavascriptBuiltInFunctionList.h
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ BUILTIN(JavascriptPromise, Race, EntryRace, FunctionInfo::ErrorOnNew)
BUILTIN(JavascriptPromise, Reject, EntryReject, FunctionInfo::ErrorOnNew)
BUILTIN(JavascriptPromise, Resolve, EntryResolve, FunctionInfo::ErrorOnNew)
BUILTIN(JavascriptPromise, Then, EntryThen, FunctionInfo::ErrorOnNew)
BUILTIN(JavascriptPromise, Finally, EntryFinally, FunctionInfo::ErrorOnNew)
BUILTIN(JavascriptPromise, Identity, EntryIdentityFunction, FunctionInfo::ErrorOnNew | FunctionInfo::DoNotProfile)
BUILTIN(JavascriptPromise, Thrower, EntryThrowerFunction, FunctionInfo::ErrorOnNew | FunctionInfo::DoNotProfile)
BUILTIN(JavascriptPromise, ResolveOrRejectFunction, EntryResolveOrRejectFunction, FunctionInfo::ErrorOnNew | FunctionInfo::DoNotProfile)
Expand Down
27 changes: 27 additions & 0 deletions lib/Runtime/Library/JavascriptLibrary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,10 @@ namespace Js
}
scriptContext->SetBuiltInLibraryFunction(JavascriptPromise::EntryInfo::Catch.GetOriginalEntryPoint(),
library->AddFunctionToLibraryObject(promisePrototype, PropertyIds::catch_, &JavascriptPromise::EntryInfo::Catch, 1));

scriptContext->SetBuiltInLibraryFunction(JavascriptPromise::EntryInfo::Finally.GetOriginalEntryPoint(),
library->AddFunctionToLibraryObject(promisePrototype, PropertyIds::finally, &JavascriptPromise::EntryInfo::Finally, 1));

library->AddMember(promisePrototype, PropertyIds::then, library->EnsurePromiseThenFunction(), PropertyBuiltInMethodDefaults);
scriptContext->SetBuiltInLibraryFunction(JavascriptPromise::EntryInfo::Then.GetOriginalEntryPoint(), library->EnsurePromiseThenFunction());

Expand Down Expand Up @@ -6956,6 +6960,29 @@ namespace Js
return function;
}

JavascriptPromiseThenFinallyFunction* JavascriptLibrary::CreatePromiseThenFinallyFunction(JavascriptMethod entryPoint, RecyclableObject* OnFinally, RecyclableObject* Constructor, bool shouldThrow)
{
Assert(scriptContext->GetConfig()->IsES6PromiseEnabled());

FunctionInfo* functionInfo = RecyclerNew(this->GetRecycler(), FunctionInfo, entryPoint);
DynamicType* type = DynamicType::New(scriptContext, TypeIds_Function, functionPrototype, entryPoint, GetDeferredAnonymousFunctionTypeHandler());

JavascriptPromiseThenFinallyFunction* function = RecyclerNewEnumClass(this->GetRecycler(), EnumFunctionClass, JavascriptPromiseThenFinallyFunction, type, functionInfo, OnFinally, Constructor, shouldThrow);
function->SetPropertyWithAttributes(PropertyIds::length, TaggedInt::ToVarUnchecked(1), PropertyConfigurable, nullptr);

return function;
}

JavascriptPromiseThunkFinallyFunction* JavascriptLibrary::CreatePromiseThunkFinallyFunction(JavascriptMethod entryPoint, Var value, bool shouldThrow)
{
Assert(scriptContext->GetConfig()->IsES6PromiseEnabled());

FunctionInfo* functionInfo = RecyclerNew(this->GetRecycler(), FunctionInfo, entryPoint);
DynamicType* type = CreateDeferredPrototypeFunctionType(entryPoint);

return RecyclerNewEnumClass(this->GetRecycler(), EnumFunctionClass, JavascriptPromiseThunkFinallyFunction, type, functionInfo, value, shouldThrow);
}

JavascriptExternalFunction* JavascriptLibrary::CreateWrappedExternalFunction(JavascriptExternalFunction* wrappedFunction)
{
// The wrapped function will have profiling, so the wrapper function does not need it.
Expand Down
2 changes: 2 additions & 0 deletions lib/Runtime/Library/JavascriptLibrary.h
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,8 @@ namespace Js
JavascriptPromiseReactionTaskFunction* CreatePromiseReactionTaskFunction(JavascriptMethod entryPoint, JavascriptPromiseReaction* reaction, Var argument);
JavascriptPromiseResolveThenableTaskFunction* CreatePromiseResolveThenableTaskFunction(JavascriptMethod entryPoint, JavascriptPromise* promise, RecyclableObject* thenable, RecyclableObject* thenFunction);
JavascriptPromiseAllResolveElementFunction* CreatePromiseAllResolveElementFunction(JavascriptMethod entryPoint, uint32 index, JavascriptArray* values, JavascriptPromiseCapability* capabilities, JavascriptPromiseAllResolveElementFunctionRemainingElementsWrapper* remainingElements);
JavascriptPromiseThenFinallyFunction* CreatePromiseThenFinallyFunction(JavascriptMethod entryPoint, RecyclableObject* OnFinally, RecyclableObject* Constructor, bool shouldThrow);
JavascriptPromiseThunkFinallyFunction* CreatePromiseThunkFinallyFunction(JavascriptMethod entryPoint, Var value, bool shouldThrow);
JavascriptExternalFunction* CreateWrappedExternalFunction(JavascriptExternalFunction* wrappedFunction);

#if ENABLE_NATIVE_CODEGEN
Expand Down
157 changes: 155 additions & 2 deletions lib/Runtime/Library/JavascriptPromise.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,159 @@ namespace Js
return CreateThenPromise(promise, fulfillmentHandler, rejectionHandler, scriptContext);
}

// Promise.prototype.finally as described in the draft ES 2018 #sec-promise.prototype.finally
Var JavascriptPromise::EntryFinally(RecyclableObject* function, CallInfo callInfo, ...)
{
PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
ARGUMENTS(args, callInfo);
Assert(!(callInfo.Flags & CallFlags_New));

ScriptContext* scriptContext = function->GetScriptContext();

AUTO_TAG_NATIVE_LIBRARY_ENTRY(function, callInfo, _u("Promise.prototype.finally"));
// 1. Let promise be the this value
// 2. If Type(promise) is not Object, throw a TypeError exception
if (args.Info.Count < 1 || !JavascriptOperators::IsObject(args[0]))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_This_NeedObject, _u("Promise.prototype.finally"));
}

JavascriptLibrary* library = scriptContext->GetLibrary();
RecyclableObject* promise = RecyclableObject::UnsafeFromVar(args[0]);
// 3. Let C be ? SpeciesConstructor(promise, %Promise%).
RecyclableObject* constructor = JavascriptOperators::SpeciesConstructor(promise, scriptContext->GetLibrary()->GetPromiseConstructor(), scriptContext);
// 4. Assert IsConstructor(C)
Assert(JavascriptOperators::IsConstructor(constructor));

// 5. If IsCallable(onFinally) is false
// a. Let thenFinally be onFinally
// b. Let catchFinally be onFinally
// 6. Else,
// a. Let thenFinally be a new built-in function object as defined in ThenFinally Function.
// b. Let catchFinally be a new built-in function object as defined in CatchFinally Function.
// c. Set thenFinally and catchFinally's [[Constructor]] internal slots to C.
// d. Set thenFinally and catchFinally's [[OnFinally]] internal slots to onFinally.

Var thenFinally = nullptr;
Var catchFinally = nullptr;

if (args.Info.Count > 1)
{
if (JavascriptConversion::IsCallable(args[1]))
{
//note to avoid duplicating code the ThenFinallyFunction works as both thenFinally and catchFinally using a flag
thenFinally = library->CreatePromiseThenFinallyFunction(EntryThenFinallyFunction, RecyclableObject::FromVar(args[1]), constructor, false);
catchFinally = library->CreatePromiseThenFinallyFunction(EntryThenFinallyFunction, RecyclableObject::FromVar(args[1]), constructor, true);
}
else
{
thenFinally = args[1];
catchFinally = args[1];
}
}
else
{
thenFinally = library->GetUndefined();
catchFinally = library->GetUndefined();
}

Assert(thenFinally != nullptr && catchFinally != nullptr);

// 7. Return ? Invoke(promise, "then", << thenFinally, catchFinally >>).
Var funcVar = JavascriptOperators::GetProperty(promise, Js::PropertyIds::then, scriptContext);
if (!JavascriptConversion::IsCallable(funcVar))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_FunctionArgument_NeedFunction, _u("Promise.prototype.finally"));
}
RecyclableObject* func = RecyclableObject::UnsafeFromVar(funcVar);

return CALL_FUNCTION(scriptContext->GetThreadContext(),
func, Js::CallInfo(CallFlags_Value, 3),
promise,
thenFinally,
catchFinally);
}

// ThenFinallyFunction as described in draft ES2018 #sec-thenfinallyfunctions
// AND CatchFinallyFunction as described in draft ES2018 #sec-catchfinallyfunctions
Var JavascriptPromise::EntryThenFinallyFunction(RecyclableObject* function, CallInfo callInfo, ...)
{
PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
ARGUMENTS(args, callInfo);
Assert(!(callInfo.Flags & CallFlags_New));
ScriptContext* scriptContext = function->GetScriptContext();

JavascriptLibrary* library = scriptContext->GetLibrary();

JavascriptPromiseThenFinallyFunction* thenFinallyFunction = JavascriptPromiseThenFinallyFunction::FromVar(function);

// 1. Let onFinally be F.[[OnFinally]]
// 2. Assert: IsCallable(onFinally)=true
Assert(JavascriptConversion::IsCallable(thenFinallyFunction->GetOnFinally()));

// 3. Let result be ? Call(onFinally, undefined)
Var result = CALL_FUNCTION(scriptContext->GetThreadContext(), thenFinallyFunction->GetOnFinally(), CallInfo(CallFlags_Value, 1), library->GetUndefined());

// 4. Let C be F.[[Constructor]]
// 5. Assert IsConstructor(C)
Assert(JavascriptOperators::IsConstructor(thenFinallyFunction->GetConstructor()));

// 6. Let promise be ? PromiseResolve(c, result)
Var promiseVar = CreateResolvedPromise(result, scriptContext, thenFinallyFunction->GetConstructor());

// 7. Let valueThunk be equivalent to a function that returns value
// OR 7. Let thrower be equivalent to a function that throws reason

Var valueOrReason = nullptr;

if (args.Info.Count > 1)
{
valueOrReason = args[1];
}
else
{
valueOrReason = scriptContext->GetLibrary()->GetUndefined();
}

JavascriptPromiseThunkFinallyFunction* thunkFinallyFunction = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, valueOrReason, thenFinallyFunction->GetShouldThrow());

// 8. Return ? Invoke(promise, "then", <<valueThunk>>)
RecyclableObject* promise = JavascriptOperators::ToObject(promiseVar, scriptContext);
Var funcVar = JavascriptOperators::GetProperty(promise, Js::PropertyIds::then, scriptContext);

if (!JavascriptConversion::IsCallable(funcVar))
{
JavascriptError::ThrowTypeError(scriptContext, JSERR_FunctionArgument_NeedFunction, _u("Promise.prototype.finally"));
}

RecyclableObject* func = RecyclableObject::FromVar(funcVar);

return CALL_FUNCTION(scriptContext->GetThreadContext(),
func, Js::CallInfo(CallFlags_Value, 2),
promiseVar,
thunkFinallyFunction);
}

// valueThunk Function as referenced within draft ES2018 #sec-thenfinallyfunctions
// and thrower as referenced within draft ES2018 #sec-catchfinallyfunctions
Var JavascriptPromise::EntryThunkFinallyFunction(RecyclableObject* function, CallInfo callInfo, ...)
{
PROBE_STACK(function->GetScriptContext(), Js::Constants::MinStackDefault);
ARGUMENTS(args, callInfo);
Assert(!(callInfo.Flags & CallFlags_New));

JavascriptPromiseThunkFinallyFunction* thunkFinallyFunction = JavascriptPromiseThunkFinallyFunction::FromVar(function);

if (!thunkFinallyFunction->GetShouldThrow())
{
return thunkFinallyFunction->GetValue();
}
else
{
JavascriptExceptionOperators::Throw(thunkFinallyFunction->GetValue(), function->GetScriptContext());
}
}

// Promise Reject and Resolve Functions as described in ES 2015 Section 25.4.1.4.1 and 25.4.1.4.2
Var JavascriptPromise::EntryResolveOrRejectFunction(RecyclableObject* function, CallInfo callInfo, ...)
{
Expand Down Expand Up @@ -913,8 +1066,8 @@ namespace Js
{
Assert(args[1] != nullptr);

return args[1];
}
return args[1];
}
else
{
return function->GetScriptContext()->GetLibrary()->GetUndefined();
Expand Down
85 changes: 85 additions & 0 deletions lib/Runtime/Library/JavascriptPromise.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,85 @@ namespace Js
#endif
};

class JavascriptPromiseThenFinallyFunction : public RuntimeFunction
{
protected:
DEFINE_VTABLE_CTOR(JavascriptPromiseThenFinallyFunction, RuntimeFunction);
DEFINE_MARSHAL_OBJECT_TO_SCRIPT_CONTEXT(JavascriptPromiseThenFinallyFunction);

public:
JavascriptPromiseThenFinallyFunction(DynamicType* type, FunctionInfo* functionInfo, RecyclableObject* OnFinally, RecyclableObject* Constructor, bool shouldThrow)
: RuntimeFunction(type, functionInfo), OnFinally(OnFinally), Constructor(Constructor), shouldThrow(shouldThrow)
{ }

inline static bool Is(Var var)
{
if (JavascriptFunction::Is(var))
{
JavascriptFunction* obj = JavascriptFunction::UnsafeFromVar(var);

return VirtualTableInfo<JavascriptPromiseThenFinallyFunction>::HasVirtualTable(obj)
|| VirtualTableInfo<CrossSiteObject<JavascriptPromiseThenFinallyFunction>>::HasVirtualTable(obj);
}

return false;
}

inline static JavascriptPromiseThenFinallyFunction* FromVar(Var var)
{
AssertOrFailFast(JavascriptPromiseThenFinallyFunction::Is(var));

return static_cast<JavascriptPromiseThenFinallyFunction*>(var);
}

inline bool GetShouldThrow() { return this->shouldThrow; }
inline RecyclableObject* GetOnFinally() { return this->OnFinally; }
inline RecyclableObject* GetConstructor() { return this->Constructor; }

private:
Field(RecyclableObject*) OnFinally;
Field(RecyclableObject*) Constructor;
Field(bool) shouldThrow;
};

class JavascriptPromiseThunkFinallyFunction : public RuntimeFunction
{
protected:
DEFINE_VTABLE_CTOR(JavascriptPromiseThunkFinallyFunction, RuntimeFunction);
DEFINE_MARSHAL_OBJECT_TO_SCRIPT_CONTEXT(JavascriptPromiseThunkFinallyFunction);

public:
JavascriptPromiseThunkFinallyFunction(DynamicType* type, FunctionInfo* functionInfo, Var value, bool shouldThrow)
: RuntimeFunction(type, functionInfo), value(value), shouldThrow(shouldThrow)
{ }

inline static bool Is(Var var)
{
if (JavascriptFunction::Is(var))
{
JavascriptFunction* obj = JavascriptFunction::UnsafeFromVar(var);

return VirtualTableInfo<JavascriptPromiseThunkFinallyFunction>::HasVirtualTable(obj)
|| VirtualTableInfo<CrossSiteObject<JavascriptPromiseThunkFinallyFunction>>::HasVirtualTable(obj);
}
return false;
}

inline static JavascriptPromiseThunkFinallyFunction* FromVar(Var var)
{
AssertOrFailFast(JavascriptPromiseThunkFinallyFunction::Is(var));

return static_cast<JavascriptPromiseThunkFinallyFunction*>(var);
}

inline bool GetShouldThrow() { return this->shouldThrow; }
inline Var GetValue() { return this->value; }

private:
Field(Var) value;
Field(bool) shouldThrow;
};

struct JavascriptPromiseAllResolveElementFunctionRemainingElementsWrapper
{
Field(uint32) remainingElements;
Expand Down Expand Up @@ -388,10 +467,13 @@ namespace Js
static FunctionInfo Reject;
static FunctionInfo Resolve;
static FunctionInfo Then;
static FunctionInfo Finally;

static FunctionInfo Identity;
static FunctionInfo Thrower;

static FunctionInfo FinallyValueFunction;
static FunctionInfo ThenFinallyFunction;
static FunctionInfo ResolveOrRejectFunction;
static FunctionInfo CapabilitiesExecutorFunction;
static FunctionInfo AllResolveElementFunction;
Expand All @@ -409,7 +491,10 @@ namespace Js
static Var EntryReject(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryResolve(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryThen(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryFinally(RecyclableObject* function, CallInfo callInfo, ...);

static Var EntryThunkFinallyFunction(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryThenFinallyFunction(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryCapabilitiesExecutorFunction(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryResolveOrRejectFunction(RecyclableObject* function, CallInfo callInfo, ...);
static Var EntryReactionTaskFunction(RecyclableObject* function, CallInfo callInfo, ...);
Expand Down
Loading

0 comments on commit 3a28b84

Please sign in to comment.