GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — repl: support top-level await by TimothyGu · Pull Request #15566 · nodejs/node

Conversation

@TimothyGu

@TimothyGu TimothyGu commented Sep 23, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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

var a = await func();

it is transformed to

(async () => { void (a = await func();) })()

While this works well for var, it doesn't for top-level let and const (let and const 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-level let and const fully. (Note, DevTools has the same problem; try const 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:

  • https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/devtools/console/console-top-level-await.js
  • https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/inspector-unit/preprocess-top-level-awaits-expected.txt

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), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

repl, readline, deps

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Nostradamos, ARitz-Cracker, alexkozy, discosultan, onetom, jbryson3, schickling, mikermcneil, devsnek, bricss, and 20 more reacted with thumbs up emoji princejwesley, siygle, bwhitty, motet-a, billybonks, pannous, martinheidegger, Nostradamos, ARitz-Cracker, watilde, and 11 more reacted with hooray emoji topaxi, abdulhannanali, kiding, ExE-Boss, and samuelscheit reacted with heart emoji ExE-Boss and samuelscheit reacted with rocket emoji
All reactions
  • 👍 30 reactions
  • 🎉 21 reactions
  • ❤️ 5 reactions
  • 🚀 2 reactions
@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs involving general changes in the lib/ or src/ directories. label Sep 23, 2017
@hiroppy hiroppy added the repl Issues and PRs related to the REPL subsystem. label Sep 23, 2017
@TimothyGu TimothyGu added readline Issues and PRs related to the built-in readline module. and removed lib / src Issues and PRs involving general changes in the lib/ or src/ directories. labels Sep 23, 2017
@TimothyGu TimothyGu mentioned this pull request Sep 23, 2017
repl: allow await in REPL #13209
Closed
@ghost

ghost commented Sep 23, 2017

Copy link
Copy Markdown

Can you write a small example where let and const are failing?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@benjamingr

benjamingr commented Sep 23, 2017

Copy link
Copy Markdown
Member

Great work! This is the right approach IMO.

Is everything with acorn OK license wise?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu
TimothyGu force-pushed the repl-await branch from df5df77 to 1ea5a70 Compare September 23, 2017 17:34
@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@Inf3rno

Can you write a small example where let and const are failing?

Sure.

const a = await Promise.resolve(1);

Enter

a = 3;

Expected: TypeError: Assignment to constant variable.

Actual: 3

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu
TimothyGu force-pushed the repl-await branch from 1ea5a70 to 4aee1db Compare September 23, 2017 17:40
@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@benjamingr Acorn uses the MIT License so our licenses are compatible. I've also added Acorn to tools/license-builder.sh and the root-level LICENSE file.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@addaleax

addaleax commented Sep 23, 2017

Copy link
Copy Markdown
Member

I mean, Node’s REPL already has its problems with top-level scope const and let … if we’re starting to transform anyway, is replacing those a viable option?

(Btw, I would like it if the REPL just allowed overwriting const variables …)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu
TimothyGu force-pushed the repl-await branch from 4aee1db to 91b037f Compare September 23, 2017 17:43
@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@addaleax

Node’s REPL already has its problems with top-level scope const and let …

That is certainly true. (e.g. #13919)

if we’re starting to transform anyway, is replacing those a viable option?

(Btw, I would like it if the REPL just allowed overwriting const variables …)

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@benjamingr

benjamingr commented Sep 23, 2017

Copy link
Copy Markdown
Member

+1 for allowing writing over const variables

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu
TimothyGu force-pushed the repl-await branch from 91b037f to f0b77b7 Compare September 23, 2017 18:20
@Qard

Qard commented Sep 23, 2017

Copy link
Copy Markdown
Member

REPL is an environment meant for experimentation. I feel like true const-ness actually doesn't really make much sense there anyway.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ghost

ghost commented Sep 23, 2017 •
edited by ghost
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

@TimothyGu

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 await gets its own asnyc function wrapper, then I don't think we can do much about it. Still I think it is worth breaking this const feature. Maybe we should allow only var by await and send an error message if somebody tries to use const or let with await.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@princejwesley

princejwesley commented Sep 23, 2017

Copy link
Copy Markdown
Contributor

@TimothyGu Behaviour is difference in canary.

image

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@princejwesley

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
princejwesley reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@princejwesley

princejwesley commented Sep 23, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

@TimothyGu
can we translate var|const|let a = await func(); into

(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);

?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@Inf3rno

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

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, a has to be defined already.

> 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 😘

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Sep 23, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

@princejwesley No, because that would break cases like

Wrong stuff, ignore
var a = 0
let a = await 0
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@princejwesley let and const are not supposed to influence the global object at all.

princejwesley reacted with laugh emoji
All reactions
  • 😄 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@princejwesley

princejwesley commented Sep 23, 2017

Copy link
Copy Markdown
Contributor

@TimothyGu I am just trying to mimic the let/const/var behaviour with the context.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Sep 23, 2017

Copy link
Copy Markdown
Member Author

@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 ;)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ghost

ghost commented Sep 25, 2017

Copy link
Copy Markdown

@TimothyGu Okay, thanks!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu
TimothyGu force-pushed the repl-await branch from f0b77b7 to 15b4da6 Compare September 25, 2017 06:56
@TimothyGu TimothyGu changed the title [WIP] repl: support top-level await repl: support top-level await Sep 25, 2017
@TimothyGu
TimothyGu force-pushed the repl-await branch from 15b4da6 to 38f5425 Compare September 25, 2017 07:01
@cjihrig cjihrig mentioned this pull request Dec 21, 2017
Revert: repl: support top-level await #17807
Closed
2 tasks done
@cjihrig

cjihrig commented Dec 21, 2017

Copy link
Copy Markdown
Contributor

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

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@TimothyGu

TimothyGu commented Dec 21, 2017

Copy link
Copy Markdown
Member Author

we don't land it in a release with odd behavior we are stuck with

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.

jdalton, BridgeAR, kiding, Gabriele-Tomberli, BrandonZacharie, and Angarsk8 reacted with thumbs up emoji
All reactions
  • 👍 6 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bmeck

bmeck commented Dec 21, 2017

Copy link
Copy Markdown
Member

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).

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.

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.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jedwards1211

jedwards1211 commented Dec 27, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Now that I see how this is done, it looks like this could easily be done in userland with Node 8, right?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bmeck

bmeck commented Dec 27, 2017

Copy link
Copy Markdown
Member

@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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jdalton

jdalton commented Dec 27, 2017

Copy link
Copy Markdown
Member

@jedwards1211

Now that I see how this is done, it looks like this could easily be done in userland with Node 8, right?

Go for it! I'd love to see what userland comes up with 🙌

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jedwards1211

jedwards1211 commented Dec 27, 2017

Copy link
Copy Markdown

@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?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bmeck

bmeck commented Dec 27, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

@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 pause mechanic is pretty significant and making sure that none of the internals rely on synchronous behavior is a main reason why the test suite fails. The transform that landed side stepped the failures by splitting the REPL into two evaluation modes and did not run the existing tests against the new mode, running the transform in the test suite you can see lots of problems.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jedwards1211

jedwards1211 commented Dec 28, 2017

Copy link
Copy Markdown

@bmeck I'm confused why a pause mechanic was necessary, since the eval option accepts a callback, which was sufficient for what I'm doing in userland.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bmeck

bmeck commented Dec 28, 2017 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

@jedwards1211 there are 3 pause mechanics in repl

  1. the readline interface.pause()

Which is managing interrupt events from TTY for the most part.

  1. the readable stream.pause()

Which is managing backpressure for the most part.

  1. the internal REPL pause system

Which is intended to queue lines while paused, but doesn't do so if you use APIs like replserver.eval or fire events like 'line' on the other 2 pausable mechanisms directly. It does not fully queue things during an asynchronous eval.

Edit To see more complete examples of this behavior not being fully paused in the current mechanics pipe into the repl using cat multi-line-file | node -i and mix await with other things. (irc log link)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cjihrig cjihrig mentioned this pull request Jan 10, 2018
[v9.x backport] test: fix require-deps-deprecation for installed deps #18077
Closed
3 tasks done
@devsnek devsnek mentioned this pull request Feb 23, 2018
Promise eval in repl yields inaccessible object #18905
Closed
@wmertens

wmertens commented Mar 14, 2018

Copy link
Copy Markdown

@jedwards1211 sorry to bother you, I'd like to use your hack, but I don't know where this code should go?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jedwards1211

jedwards1211 commented Mar 14, 2018

Copy link
Copy Markdown

@wmertens it goes within the eval function you pass when creating the repl

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@targos targos removed the backport-requested-v9.x label Mar 24, 2018
@TimothyGu TimothyGu mentioned this pull request Mar 26, 2018
repl: hide top-level await feature behind a flag #19604
Closed
3 tasks done
TimothyGu added a commit to TimothyGu/node that referenced this pull request Apr 14, 2018
@TimothyGu
repl: hide top-level await feature behind a flag
d8d5c2a
Refs: nodejs#17807
Refs: nodejs#15566 (comment)
TimothyGu added a commit that referenced this pull request Apr 18, 2018
@TimothyGu
repl: hide top-level await feature behind a flag
be92eab
PR-URL: #19604
Refs: #17807
Refs: #15566 (comment)
Reviewed-By: Gus Caplan <[email protected]>
jasnell pushed a commit that referenced this pull request Apr 19, 2018
@TimothyGu @jasnell
repl: hide top-level await feature behind a flag
f2396ee
PR-URL: #19604
Refs: #17807
Refs: #15566 (comment)
Reviewed-By: Gus Caplan <[email protected]>
@kitsonk kitsonk mentioned this pull request Jul 26, 2018
Top Level Await microsoft/TypeScript#25988
Closed
2 of 4 tasks
@F3n67u F3n67u mentioned this pull request Oct 18, 2024
enable-experimental-regexp-engine v8 flag doesn't work with promises #55400
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@jasnell jasnell jasnell left review comments
@Qard Qard Qard approved these changes
@bnoordhuis bnoordhuis bnoordhuis approved these changes
+1 more reviewer
@mscdex mscdex mscdex left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

notable-change PRs with changes that should be highlighted in changelogs. readline Issues and PRs related to the built-in readline module. repl Issues and PRs related to the REPL subsystem.

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

repl: allow await in REPL

Uh oh!

There was an error while loading. Please reload this page.

20 participants

@TimothyGu @benjamingr @addaleax @Qard @princejwesley @jasnell @targos @mscdex @jdalton @Fishrock123 @kiding @devsnek @MylesBorins @gibfahn @bmeck @Trott @mcollina @cjihrig @jedwards1211 @wmertens
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.