-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Promise.prototype.finally Fixes #3520 #4668
Conversation
fe5200c
to
1430bc2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not qualified to review 99% of this PR, but i had this one comment :-)
assert.isFalse(descriptor.enumerable, "Promise.prototype.finally.enumerable === false"); | ||
assert.isTrue(descriptor.configurable, "Promise.prototype.finally.configurable === true"); | ||
assert.areEqual('function', typeof descriptor.value, "typeof Promise.prototype.finally === 'function'"); | ||
assert.areEqual(1, Promise.prototype.finally.length, "Promise.prototype.finally.length === 1"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you also add an assert that the .name
is "finally"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - and added equivalents for .then and .catch as they didn't exist either.
|
||
JavascriptLibrary* library = scriptContext->GetLibrary(); | ||
|
||
Assert(JavascriptPromiseThenFinallyFunction::Is(function)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that for some of these asserts (this one in particular) you should use AssertOrFailFast
. The difference being, Assert
is debug build only, while AssertOrFailFast
still happens in release, and if somehow the function
was the wrong kind of object, we don't want to accidentally do the wrong thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, since you do that in the JavascriptPromiseThenFinallyFunction::FromVar
method, you should just call that I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity (and for future reference on my part 😄), what is the criteria for whether something should be a regular assert vs. a FailFast assert? CC codebase has both kinds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's something that could lead to type confusion / executing wrong code, it should typically be a failfast to avoid security issues. If it is something that shouldn't happen, and we'd like to know in tests when it happens so we can fix/understand it, but is recoverable then a plain assert is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My thinking was that there was no existing code path to get there without that Assert being true and so no need to add any extra cost at runtime and future regressions could be detected by the Assert before making it into any releases. I've made the change though.
|
||
|
||
//8. Return ? Invoke(promise, "then", <<valueThink>>) | ||
JavascriptPromise* promise = UnsafeFromVar(promiseVar); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you convert from Var
to JavascriptPromise
here? It looks to me like you just access properties on the object, none of the native methods of JavascriptPromise
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a mistake - I rewrote the logic in this function several times and this was a holdover from an earlier version, fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll give this a more thorough review (relatively) soon but for now just a small typo I noticed.
JavascriptPromiseThunkFinallyFunction* Thunk = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, args[1], This->GetShouldThrow()); | ||
|
||
|
||
//8. Return ? Invoke(promise, "then", <<valueThink>>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in comment: Should be valueThunk
, not "think"
960a7e8
to
b3f2360
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just a few comments. We have to fix the one where we use args[1]
without checking the arg count but others are mostly nitpicks.
|
||
//5. If IsCallable(onFinally) is false | ||
//a. Let thenFinally be onFinally | ||
//b. let catchFinally be onFinally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: 'let' here is not capitalized and the a and b items are not indented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
// d. Set thenFinally and catchFinally's [[OnFinally]] internal slots to onFinally. | ||
|
||
Var thenFinally; | ||
Var catchFinally; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like if these were initialized to nullptr
and have an assert after the conditional that each is not nullptr
anymore. Should cause no overhead in release build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
JavascriptLibrary* library = scriptContext->GetLibrary(); | ||
|
||
JavascriptPromiseThenFinallyFunction* This = JavascriptPromiseThenFinallyFunction::FromVar(function); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a big fan of naming this JavascriptPromiseThenFinallyFunction*
as This
. Can we call it finallyFunction
or thenFinallyFunction
or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK changed (sorry Javascript is my native language was trying to make it look like this.property)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, we just don't usually use this naming convention in our codebase. 👍
Var result = CALL_FUNCTION(scriptContext->GetThreadContext(), This->GetOnFinally(), CallInfo(CallFlags_Value, 1), library->GetUndefined()); | ||
|
||
//4. Let C be F.[[Constructor]] | ||
//5.Assert IsConstructor(C) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: A space here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
//7. Let valueThunk be equivalent to a function that returns value | ||
//OR 7. Let thrower be equivalent to a function that throws reason | ||
JavascriptPromiseThunkFinallyFunction* Thunk = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, args[1], This->GetShouldThrow()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same naming nitpick as above. Can we not use Thunk
here but something like thunkFinallyFunction
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
JavascriptPromiseThunkFinallyFunction* Thunk = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, args[1], This->GetShouldThrow()); | ||
|
||
//8. Return ? Invoke(promise, "then", <<valueThunk>>) | ||
RecyclableObject* promise = RecyclableObject::UnsafeFromVar(promiseVar); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure UnsafeFromVar
is ok here? Maybe regular FromVar
is better unless CreateResolvedPromise
has a guarantee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine - CreateResolvedPromise uses a FromVar on the constructor to check it's safe then uses it to generate the promise. For this promise to be invalid therefore without hitting the FailFast in CreateResolvedPromise would require CreateResolvedPromise to be broken. - Not changed - will change if you still want me to though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FromVar
in CreateResolvedPromise
is done to the constructor
object, though. Here we are converting the promise object which was returned from CreateResolvedPromise
which could be any Var
, couldn't it? It's just whatever was returned from the constructor
when we called it in CreatePromiseCapabilityRecord
. Since we're calling as-constructor, the return should be an object, I guess. I do wonder if it would be possible to get it to be a non-object. Seems might be possible. I think we should be cautious and follow the Invoke
rabbit-hole down to 7.3.2 GetV(V,P) (https://tc39.github.io/ecma262/#sec-getv) where we should do ToObject
on promise
. We do something very similar in EntryAll
on a newly constructed promise Var.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I've put in an AssertOrFailFast for it being an object, I think that should do the job? If not, not quite sure what you wanted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rhuanjl I think he just wanted you to change the UnsafeFromVar to a FromVar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the confusion - I said a lot of words.
When I saw this code at first I wondered if UnsafeFromVar would be actually safe because promiseVar is a Var and it isn't clear that it always must be a RecyclableObject. That's why I thought FromVar would be safer because it will check at runtime.
After taking a look further, I believe it may be possible for CreateResolvedPromise to return a non-object because the constructor may be overridden via Symbol.species. If that's the case, we should actually throw a TypeError while doing Invoke according to ECMA. Invoke calls GetV which performs ToObject on V. Does that make sense? 🤔
I believe the conversion should look like this:
RecyclableObject* promise = JavascriptOperators::ToObject(promiseVar, scriptContext);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep that makes sense - well that aspect of the spec does not seem sensible at all but ah well. Will re-push after re-building and re-running tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Many aspects of the spec do not seem sensible. Thanks for your efforts. 😄
|
||
//7. Let valueThunk be equivalent to a function that returns value | ||
//OR 7. Let thrower be equivalent to a function that throws reason | ||
JavascriptPromiseThunkFinallyFunction* Thunk = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, args[1], This->GetShouldThrow()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure args[1]
exists. This should check args.Info.Count
and pass undefined
if the then finally function was called without value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I'd only been thinking of the normal usage pattern - forgot you could call this with no argument by overwriting .then(); Fixed.
RecyclableObject* undefinedVar = scriptContext->GetLibrary()->GetUndefined(); | ||
|
||
return CALL_FUNCTION(scriptContext->GetThreadContext(), | ||
func, Js::CallInfo(CallFlags_Value, 3), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we should just be passing 2 arguments here - this
and valueThunk
, right? I think you can remove undefinedVar
and change arg count to 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, fixed.
ARGUMENTS(args, callInfo); | ||
Assert(!(callInfo.Flags & CallFlags_New)); | ||
|
||
JavascriptPromiseThunkFinallyFunction* This = JavascriptPromiseThunkFinallyFunction::FromVar(function); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more nitpick about This
, can we name it something like thunkFinallyFunction
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed.
Assert(scriptContext->GetConfig()->IsES6PromiseEnabled()); | ||
|
||
FunctionInfo* functionInfo = RecyclerNew(this->GetRecycler(), FunctionInfo, entryPoint); | ||
DynamicType* type = CreateDeferredPrototypeFunctionType(entryPoint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use GetDeferredAnonymousFunctionTypeHandler()
to avoid having to set the name
property here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did I not notice that function... I spent 10-15 minutes searching JavascriptLibrary.cpp for a way to do that but didn't find it. Changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, this comment has moved down to the other CreateDeferredPrototypeFunctionType call not where you originally placed it.
@boingoing thanks for the review - I've made the changes and re-pushed. I've also added an extra test case to confirm that thenFinally handles the situation of being called without an argument correctly (and supplies the undefined value). Will update and squash later on assuming you're happy with my updates. Edit: oops my new test case is wrong will update shortly |
@boingoing Thanks again for the review I've fixed all of your points now, the commit "Responding to feedback" contains all responses to your points. Please let me know when good to squash. |
@rhuanjl Looks good to me. Thanks a lot for responding to those feedback and sorry for being a bit pedantic about it. If you squash these commits, I will begin the process of testing internally and shepherd the change through our checkin process. |
Thanks @boingoing squashed and re-pushed. Did @dilijev want to review as well? |
Taking a look. |
@@ -559,6 +559,159 @@ namespace Js | |||
return CreateThenPromise(promise, fulfillmentHandler, rejectionHandler, scriptContext); | |||
} | |||
|
|||
// Promise.prototype.finally as described in the draft of ES 2018 Section 25.4.5.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ECMA section numbers aren't immutable, but the anchors are considered to be. Use #sec-promise.prototype.finally
instead of a section number.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was copying what was already in the file on this one - most of the Promise methods have comments referring to section numbers per ES2015 (which are also mostly out of date now) if I'm going to fix this point assume I should fix all of them at the same time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well fix this one now and follow up with the others (this guidance about using anchors instead of section numbers is relatively new so I'm not surprised there are still a lot of section numbers throughout the code).
In reply to: 167731238 [](ancestors = 167731238)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to update the other section numbers, I'd like to make sure we also do an audit to make sure no behavior in the functions has changed between spec versions. Then we can be sure the implementation holds to the new spec version and update the spec reference at the same time. So, probably best to follow-up on the other methods.
Looks like we need tests of behavior (besides throwing behavior) for |
} | ||
|
||
JavascriptLibrary* library = scriptContext->GetLibrary(); | ||
RecyclableObject* promise = RecyclableObject::UnsafeFromVar(args[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnsafeFromVar(args[0]); [](start = 54, length = 23)
Is it okay to call UnsafeFromVar(args[0]) here? -- do we know for sure this has to be a RecyclableObject at this point? It is possible to call with this
as, say, a number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, because the check above covers that it is an object -- is it necessarily a RecyclableObject or could it be something else (I'm not sure the answer to this question)
In reply to: 167733902 [](ancestors = 167733902)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We did a check above to make sure we have an args[0] and that it is an object. Should be guaranteed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a type error thrown a couple of lines above if the arg is not an Object.
I'll also add a new test case that confirms that a type error is thrown if this is called with any non-promise object.
@rhuanjl looks like there's a conflict with the latest version of bytecode. You'll want to merge with master and regenerate bytecode before the final squash. |
I wonder if there should be a test for what happens if a promise has been rejected and the resulting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, save for a nitpick re: comment style and a concern about tests (see my comment above).
|
||
JavascriptPromiseThunkFinallyFunction* thunkFinallyFunction = library->CreatePromiseThunkFinallyFunction(EntryThunkFinallyFunction, valueOrReason, thenFinallyFunction->GetShouldThrow()); | ||
|
||
//8. Return ? Invoke(promise, "then", <<valueThunk>>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Prevailing convention seems to be to have a space between //
and the comment text. In any case comments are easier to read with that space IMO.
@fatcerberus your interpretation of the spec is correct. |
@boingoing I think this is good to go now? I've updated to latest and addressed all review points - I've also added extra test cases for the case @fatcerberus and @ljharb mentioned, hopefully not controversial to do that - they're the last two cases in ES6PromiseAsync.js |
@rhuanjl Yes, looks good to me. I will take a final look over the code and pull the branch down to do internal testing before landing. Thank you for your patience. |
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: space after //
|
||
JavascriptLibrary* library = scriptContext->GetLibrary(); | ||
RecyclableObject* promise = RecyclableObject::UnsafeFromVar(args[0]); | ||
//3. Let C be ? SpeciesConstructor(promise, %Promise%). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: space after //
|
||
// 5. If IsCallable(onFinally) is false | ||
// a. Let thenFinally be onFinally | ||
// b. Let catchFinally be onFinally |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing space
|
||
Var valueOrReason = nullptr; | ||
|
||
if(args.Info.Count > 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing space after if
@boingoing I've added some spaces I was missing that @fatcerberus noticed - I'll hold on squashing until you've done your final review. |
@rhuanjl I added a commit to update the bytecode cache GUID. We need this for internal processes. Otherwise we are good to go from my perspective. Please squash it and I'll run the internal testing once more. 👍 |
@boingoing ok squashed, thank you and sorry that this has taken a lot of your time. |
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.
@rhuanjl Thanks for your contribution, this is merged! No worries about my time, by the way, you've saved us a lot of time by implementing it in the first place. 👍 |
Ok, thanks for landing this @boingoing and thanks to everyone else for the reviews. Also thanks again @fatcerberus for regenerating the bytecode for me (couldn't see a way to do that on macOS, and have never learnt how to do anything on Windows other than use Excel) |
Yay! Any idea when to expect it in Edge? |
@rhuanjl the byte code regenerate script really just calls ch with some parameters. In theory it should work on xplat although you'd have to rewrite the logic in bash or something. This is something quick we should probably add. |
@ljharb hard to say exactly, but think it will be in insiders flights in a few months -- general availability whenever the following version of windows is released. |
Refs: chakra-core/ChakraCore#4668 Fixes: #464 PR-URL: #568
Refs: chakra-core/ChakraCore#4668 Fixes: #464 PR-URL: #568
Refs: chakra-core/ChakraCore#4668 Fixes: #464 PR-URL: #568 Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
Refs: chakra-core/ChakraCore#4668 Fixes: nodejs#464 PR-URL: nodejs#568 Reviewed-By: Jimmy Thomson <jithomso@microsoft.com>
This PR implements Promise.prototype.finally.
Fixes #3520
Relevant ECMASpec: https://tc39.github.io/ecma262/#sec-promise.prototype.finally
Notes:
cc: @dilijev @ljharb
Thanks to my friend @fatcerberus for running the RegenerateBytecode script for me.