-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
repl: support top-level await #15566
Conversation
Can you write a small example where let and const are failing? |
Great work! This is the right approach IMO. Is everything with acorn OK license wise? |
df5df77
to
1ea5a70
Compare
Sure. const a = await Promise.resolve(1); Enter a = 3; Expected: Actual: |
1ea5a70
to
4aee1db
Compare
@benjamingr Acorn uses the MIT License so our licenses are compatible. I've also added Acorn to |
I mean, Node’s REPL already has its problems with top-level scope (Btw, I would like it if the REPL just allowed overwriting |
4aee1db
to
91b037f
Compare
That is certainly true. (e.g. #13919)
Technically yes, but I'm not convinced yet that's a good idea as it goes against the keywords' intended semantics, and what Chrome is doing. It sounds like a slippery slope. |
+1 for allowing writing over const variables |
91b037f
to
f0b77b7
Compare
REPL is an environment meant for experimentation. I feel like true const-ness actually doesn't really make much sense there anyway. |
Is it transformed to the following? // const a = await Promise.resolve(1);
// a = 3
(async () => { void (a = await Promise.resolve(1)) })()
a = 3 If every I am not that experienced with this REPL project, but if this var a = await Promise.resolve(1);
console.log(a); // 1
a = 3;
console.log(a); // 3 is transformed into this: (async () => { void (a = await Promise.resolve(1)) })()
// console.log(a); // ReferenceError - a is not declared
a = 3
console.log(a); // 3
// setTimeout(() => {
// console.log(a); // 1
// }, 10); then I think we have a problem, because I'd expect this behavior: (async () => {
var a = await Promise.resolve(1);
console.log(a); // 1
a = 3;
console.log(a); // 3
})() To make it work this way we need to change the REPL state to pending until the promise is fulfilled and continue with the next line only after that. I am not sure whether that would be an acceptable behavior. Another possible solution would be to display a message if the promise is fulfilled and you can use the results. If you choose that one, then it might be beneficial to prevent the user from setting a new value to the variable before it gets its value from the promise. Maybe I am misunderstanding something, because I don't have a testing environment to check your commit and tbh. I don't fully understand it, but this might be a harder problem than just wrapping the actual line. |
@TimothyGu Behaviour is difference in canary. |
I'm sorry, I don't quite understand what you mean. The code you put in Canary's DevTools and the output you got is the exact same as what you would get from this PR. > const b = await 4
undefined
> b
4
> const c = await Promise.resolve(1)
undefined
> c
1
> c = 4
4
> c
4 |
@TimothyGu (async (self) => {
var $ret = await func();
// const case
Object.defineProperty( self, 'a', {
get: function () {
return $ret;
},
set: function() {
throw new TypeError('Assignment to constant variable')
}
});
// let re-declaring case
// if self has 'a', throw SyntaxError
Object.keys(self).findIndex((key) => key === 'a') !== -1 && throw new SyntaxError("Identifier 'a' has already been declared");
// let and var case
self['a'] = $ret;
void 0;
})(global); ? |
No it doesn't actually transform to that. After you enter the first line, the REPL will PAUSE until the promise returned from the async function is resolved. So when you can enter the next line, > var a = await Promise.resolve(1);
undefined
> console.log(a)
1
undefined
> a = 3
3
> console.log(a)
3
undefined Again, I encourage y'all to actually build this version and test it out. I'm fine with answering questions, but the fastest way to answer those questions are to find out about them yourselves 😘 |
@princejwesley Wrong stuff, ignorevar a = 0
let a = await 0 |
@princejwesley |
@TimothyGu I am just trying to mimic the |
@princejwesley I got you. While there are indeed some creative ways to do that, seeing how people seem to not like the restrictions of let and const at all in REPL (1/2/3) I don't think we should try to make it worse ;) |
@TimothyGu Okay, thanks! |
f0b77b7
to
15b4da6
Compare
15b4da6
to
38f5425
Compare
@benjamingr I'm not opposing the feature. I just favor reverting until the feature is more consistent so we don't land it in a release with odd behavior we are stuck with. |
I'm in favor of this feature, but am also +1 on reverting until this is better sorted out. If we do decide to go the revert route, here is a PR: #17807 |
There are no REPL implications we would be “stuck” with, since any fix to REPL would be a semver-patch change. The special async mode is certainly not ideal, but the way we are operating regarding source code transformation is identical to how DevTools is; and it is my strong belief that if something is good enough for the most widely used browser in the world, it should be good enough for Node.js (not saying it couldn’t be better). I’m fine with putting this behind a flag, but given the positive developer feedback we have received so far, and the strictly additive nature of this feature (it doesn’t break any existing use cases) I am against reverting this wholesale. |
V8 also have an open issue on moving to a more native/feature safe without code transform to do this. Given that they are seeking to change things as well, I'm not swayed that because it is broken other places it should be ok to be broken all the places.
That seems fine, I wouldn't want it unflagged until stuff is ironed out. Some of the behavior does need to be specified like how this mode made const not const, allowed variable re-binding, and is using the global object instead of scope. |
Now that I see how this is done, it looks like this could easily be done in userland with Node 8, right? |
@jedwards1211 it is non-trivial and I am having to patch internals on node's repl to fix various things. I think it is unlikely to be able to be done completely in userland without something very large in scope that reimplements a large part of the repl. |
Go for it! I'd love to see what userland comes up with 🙌 |
@bmeck The following hack works okay in my personal scripts. Obviously it's not production quality: if (/\bawait\b/.test(cmd) && !/\basync\b/.test(cmd)) {
const match = /^\s*var\b(.*)/.exec(cmd)
if (match) {
cmd = `(async () => { void (${match[1]}) })()`
} else {
cmd = `(async () => { return ${cmd} })()`
}
} What part of the repl are you having to reimplement? |
@jedwards1211 if you enable such a transform to always be present repl.js most of the test suite for REPL fails. In the case of the specific above it has problems for multiple statement commands. In the case of the top level await transform that landed various things are problematic: see comment above , rewriting the |
@bmeck I'm confused why a |
@jedwards1211 there are 3 pause mechanics in repl Which is managing interrupt events from TTY for the most part. Which is managing backpressure for the most part. Which is intended to queue lines while paused, but doesn't do so if you use APIs like Edit To see more complete examples of this behavior not being fully paused in the current mechanics pipe into the repl using |
@jedwards1211 sorry to bother you, I'd like to use your hack, but I don't know where this code should go? |
@wmertens it goes within the |
PR-URL: #19604 Refs: #17807 Refs: #15566 (comment) Reviewed-By: Gus Caplan <[email protected]>
PR-URL: #19604 Refs: #17807 Refs: #15566 (comment) Reviewed-By: Gus Caplan <[email protected]>
This PR depends upon and is rebased on top of #16392.
This PR endeavors to fix #13209, following the footsteps of Google Chrome.
Algorithmically, it uses much of the same strategies as the Chromium CL -- namely, using a JS parser (Acorn) and then transforming the to-be-executed source code based on that AST. In fact, the AST node visitors are pretty much a direct port of DevTools code from DevTools' homebrew AST walker to one supplied by Acorn.
Unlike the usual parse-transform-serialize pipeline (i.e. "the Babel way"), however, I decided to adopt "the Bublé way", which is to mutate the source code directly based on the locations in the AST. While by design this approach is more fragile than a full AST serialization, it is 1) much faster, 2) requires less bundled code, 3) the approach used by Chrome DevTools, and 4) just enough for this specific use case, because we know transformed code does not need to be transformed again thereafter.
A huge focus of #13209 (and a must-have) is support for await expressions used in variable declarations and assignments. Like DevTools, this PR supports variable declarations by transforming them into assignments. Take
it is transformed to
While this works well for
var
, it doesn't for top-levellet
andconst
(let
andconst
still work as expected in blocks), which will lose their lexical specialness (or constantness) with this approach. However, I simply can't think of any good way to support top-levellet
andconst
fully. (Note, DevTools has the same problem; tryconst a = await Promise.resolve(1);
back-to-back multiple times.) If you've got an idea, please comment!The implementation in this PR is fully complete with the above caveat (though a comment or two can perhaps be added in the right places). However, tests still need to be added, especially around the SIGINT handling part, which turned out to be much more complex than the actual source transformation. For source code transformation, Chromium has two very comprehensive sets of tests that we can borrow:
Anyway, please test and report back how it works!
Many thanks to @ak239 for his DevTools implementation; wouldn't have been able to finish this without his work!
/cc @benjamingr @princejwesley
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
repl, readline, deps