Skip to content

Commit eeab7bc

Browse files
committed
repl: support top-level await
Much of the AST visitor code was ported from Chrome DevTools code written by Aleksey Kozyatinskiy <kozyatinskiy@chromium.org>. PR-URL: #15566 Fixes: #13209 Refs: https://chromium.googlesource.com/chromium/src/+/e8111c396fef38da6654093433b4be93bed01dce Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent ab64b6d commit eeab7bc

File tree

5 files changed

+449
-7
lines changed

5 files changed

+449
-7
lines changed

lib/internal/repl/await.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
3+
const acorn = require('internal/deps/acorn/dist/acorn');
4+
const walk = require('internal/deps/acorn/dist/walk');
5+
6+
const noop = () => {};
7+
const visitorsWithoutAncestors = {
8+
ClassDeclaration(node, state, c) {
9+
if (state.ancestors[state.ancestors.length - 2] === state.body) {
10+
state.prepend(node, `${node.id.name}=`);
11+
}
12+
walk.base.ClassDeclaration(node, state, c);
13+
},
14+
FunctionDeclaration(node, state, c) {
15+
state.prepend(node, `${node.id.name}=`);
16+
},
17+
FunctionExpression: noop,
18+
ArrowFunctionExpression: noop,
19+
MethodDefinition: noop,
20+
AwaitExpression(node, state, c) {
21+
state.containsAwait = true;
22+
walk.base.AwaitExpression(node, state, c);
23+
},
24+
ReturnStatement(node, state, c) {
25+
state.containsReturn = true;
26+
walk.base.ReturnStatement(node, state, c);
27+
},
28+
VariableDeclaration(node, state, c) {
29+
if (node.kind === 'var' ||
30+
state.ancestors[state.ancestors.length - 2] === state.body) {
31+
if (node.declarations.length === 1) {
32+
state.replace(node.start, node.start + node.kind.length, 'void');
33+
} else {
34+
state.replace(node.start, node.start + node.kind.length, 'void (');
35+
}
36+
37+
for (const decl of node.declarations) {
38+
state.prepend(decl, '(');
39+
state.append(decl, decl.init ? ')' : '=undefined)');
40+
}
41+
42+
if (node.declarations.length !== 1) {
43+
state.append(node.declarations[node.declarations.length - 1], ')');
44+
}
45+
}
46+
47+
walk.base.VariableDeclaration(node, state, c);
48+
}
49+
};
50+
51+
const visitors = {};
52+
for (const nodeType of Object.keys(walk.base)) {
53+
const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType];
54+
visitors[nodeType] = (node, state, c) => {
55+
const isNew = node !== state.ancestors[state.ancestors.length - 1];
56+
if (isNew) {
57+
state.ancestors.push(node);
58+
}
59+
callback(node, state, c);
60+
if (isNew) {
61+
state.ancestors.pop();
62+
}
63+
};
64+
}
65+
66+
function processTopLevelAwait(src) {
67+
const wrapped = `(async () => { ${src} })()`;
68+
const wrappedArray = wrapped.split('');
69+
let root;
70+
try {
71+
root = acorn.parse(wrapped, { ecmaVersion: 8 });
72+
} catch (err) {
73+
return null;
74+
}
75+
const body = root.body[0].expression.callee.body;
76+
const state = {
77+
body,
78+
ancestors: [],
79+
replace(from, to, str) {
80+
for (var i = from; i < to; i++) {
81+
wrappedArray[i] = '';
82+
}
83+
if (from === to) str += wrappedArray[from];
84+
wrappedArray[from] = str;
85+
},
86+
prepend(node, str) {
87+
wrappedArray[node.start] = str + wrappedArray[node.start];
88+
},
89+
append(node, str) {
90+
wrappedArray[node.end - 1] += str;
91+
},
92+
containsAwait: false,
93+
containsReturn: false
94+
};
95+
96+
walk.recursive(body, state, visitors);
97+
98+
// Do not transform if
99+
// 1. False alarm: there isn't actually an await expression.
100+
// 2. There is a top-level return, which is not allowed.
101+
if (!state.containsAwait || state.containsReturn) {
102+
return null;
103+
}
104+
105+
const last = body.body[body.body.length - 1];
106+
if (last.type === 'ExpressionStatement') {
107+
// For an expression statement of the form
108+
// ( expr ) ;
109+
// ^^^^^^^^^^ // last
110+
// ^^^^ // last.expression
111+
//
112+
// We do not want the left parenthesis before the `return` keyword;
113+
// therefore we prepend the `return (` to `last`.
114+
//
115+
// On the other hand, we do not want the right parenthesis after the
116+
// semicolon. Since there can only be more right parentheses between
117+
// last.expression.end and the semicolon, appending one more to
118+
// last.expression should be fine.
119+
state.prepend(last, 'return (');
120+
state.append(last.expression, ')');
121+
}
122+
123+
return wrappedArray.join('');
124+
}
125+
126+
module.exports = {
127+
processTopLevelAwait
128+
};

lib/repl.js

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
'use strict';
4444

4545
const internalModule = require('internal/module');
46+
const { processTopLevelAwait } = require('internal/repl/await');
4647
const internalUtil = require('internal/util');
4748
const { isTypedArray } = require('internal/util/types');
4849
const util = require('util');
@@ -200,6 +201,7 @@ function REPLServer(prompt,
200201
function defaultEval(code, context, file, cb) {
201202
var err, result, script, wrappedErr;
202203
var wrappedCmd = false;
204+
var awaitPromise = false;
203205
var input = code;
204206

205207
if (/^\s*\{/.test(code) && /\}\s*$/.test(code)) {
@@ -211,6 +213,15 @@ function REPLServer(prompt,
211213
wrappedCmd = true;
212214
}
213215

216+
if (code.includes('await')) {
217+
const potentialWrappedCode = processTopLevelAwait(code);
218+
if (potentialWrappedCode !== null) {
219+
code = potentialWrappedCode;
220+
wrappedCmd = true;
221+
awaitPromise = true;
222+
}
223+
}
224+
214225
// first, create the Script object to check the syntax
215226

216227
if (code === '\n')
@@ -231,8 +242,9 @@ function REPLServer(prompt,
231242
} catch (e) {
232243
debug('parse error %j', code, e);
233244
if (wrappedCmd) {
234-
wrappedCmd = false;
235245
// unwrap and try again
246+
wrappedCmd = false;
247+
awaitPromise = false;
236248
code = input;
237249
wrappedErr = e;
238250
continue;
@@ -251,6 +263,20 @@ function REPLServer(prompt,
251263
// predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9`
252264
regExMatcher.test(savedRegExMatches.join(sep));
253265

266+
let finished = false;
267+
function finishExecution(err, result) {
268+
if (finished) return;
269+
finished = true;
270+
271+
// After executing the current expression, store the values of RegExp
272+
// predefined properties back in `savedRegExMatches`
273+
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
274+
savedRegExMatches[idx] = RegExp[`$${idx}`];
275+
}
276+
277+
cb(err, result);
278+
}
279+
254280
if (!err) {
255281
// Unset raw mode during evaluation so that Ctrl+C raises a signal.
256282
let previouslyInRawMode;
@@ -301,15 +327,53 @@ function REPLServer(prompt,
301327
return;
302328
}
303329
}
304-
}
305330

306-
// After executing the current expression, store the values of RegExp
307-
// predefined properties back in `savedRegExMatches`
308-
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
309-
savedRegExMatches[idx] = RegExp[`$${idx}`];
331+
if (awaitPromise && !err) {
332+
let sigintListener;
333+
pause();
334+
let promise = result;
335+
if (self.breakEvalOnSigint) {
336+
const interrupt = new Promise((resolve, reject) => {
337+
sigintListener = () => {
338+
reject(new Error('Script execution interrupted.'));
339+
};
340+
prioritizedSigintQueue.add(sigintListener);
341+
});
342+
promise = Promise.race([promise, interrupt]);
343+
}
344+
345+
promise.then((result) => {
346+
// Remove prioritized SIGINT listener if it was not called.
347+
// TODO(TimothyGu): Use Promise.prototype.finally when it becomes
348+
// available.
349+
prioritizedSigintQueue.delete(sigintListener);
350+
351+
finishExecution(undefined, result);
352+
unpause();
353+
}, (err) => {
354+
// Remove prioritized SIGINT listener if it was not called.
355+
prioritizedSigintQueue.delete(sigintListener);
356+
357+
if (err.message === 'Script execution interrupted.') {
358+
// The stack trace for this case is not very useful anyway.
359+
Object.defineProperty(err, 'stack', { value: '' });
360+
}
361+
362+
unpause();
363+
if (err && process.domain) {
364+
debug('not recoverable, send to domain');
365+
process.domain.emit('error', err);
366+
process.domain.exit();
367+
return;
368+
}
369+
finishExecution(err);
370+
});
371+
}
310372
}
311373

312-
cb(err, result);
374+
if (!awaitPromise || err) {
375+
finishExecution(err, result);
376+
}
313377
}
314378

315379
self.eval = self._domain.bind(eval_);
@@ -457,7 +521,15 @@ function REPLServer(prompt,
457521

458522
var sawSIGINT = false;
459523
var sawCtrlD = false;
524+
const prioritizedSigintQueue = new Set();
460525
self.on('SIGINT', function onSigInt() {
526+
if (prioritizedSigintQueue.size > 0) {
527+
for (const task of prioritizedSigintQueue) {
528+
task();
529+
}
530+
return;
531+
}
532+
461533
var empty = self.line.length === 0;
462534
self.clearLine();
463535
_turnOffEditorMode(self);

node.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
'lib/internal/process/write-coverage.js',
119119
'lib/internal/readline.js',
120120
'lib/internal/repl.js',
121+
'lib/internal/repl/await.js',
121122
'lib/internal/socket_list.js',
122123
'lib/internal/test/unicode.js',
123124
'lib/internal/tls.js',
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const { processTopLevelAwait } = require('internal/repl/await');
6+
7+
// Flags: --expose-internals
8+
9+
// This test was created based on
10+
// https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/inspector-unit/preprocess-top-level-awaits.js?rcl=358caaba5e763e71c4abb9ada2d9cd8b1188cac9
11+
12+
const testCases = [
13+
[ '0',
14+
null ],
15+
[ 'await 0',
16+
'(async () => { return (await 0) })()' ],
17+
[ 'await 0;',
18+
'(async () => { return (await 0); })()' ],
19+
[ '(await 0)',
20+
'(async () => { return ((await 0)) })()' ],
21+
[ '(await 0);',
22+
'(async () => { return ((await 0)); })()' ],
23+
[ 'async function foo() { await 0; }',
24+
null ],
25+
[ 'async () => await 0',
26+
null ],
27+
[ 'class A { async method() { await 0 } }',
28+
null ],
29+
[ 'await 0; return 0;',
30+
null ],
31+
[ 'var a = await 1',
32+
'(async () => { void (a = await 1) })()' ],
33+
[ 'let a = await 1',
34+
'(async () => { void (a = await 1) })()' ],
35+
[ 'const a = await 1',
36+
'(async () => { void (a = await 1) })()' ],
37+
[ 'for (var i = 0; i < 1; ++i) { await i }',
38+
'(async () => { for (void (i = 0); i < 1; ++i) { await i } })()' ],
39+
[ 'for (let i = 0; i < 1; ++i) { await i }',
40+
'(async () => { for (let i = 0; i < 1; ++i) { await i } })()' ],
41+
[ 'var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
42+
'(async () => { void ( ({a} = {a:1}), ([b] = [1]), ' +
43+
'({c:{d}} = {c:{d: await 1}})) })()' ],
44+
/* eslint-disable no-template-curly-in-string */
45+
[ 'console.log(`${(await { a: 1 }).a}`)',
46+
'(async () => { return (console.log(`${(await { a: 1 }).a}`)) })()' ],
47+
/* eslint-enable no-template-curly-in-string */
48+
[ 'await 0; function foo() {}',
49+
'(async () => { await 0; foo=function foo() {} })()' ],
50+
[ 'await 0; class Foo {}',
51+
'(async () => { await 0; Foo=class Foo {} })()' ],
52+
[ 'if (await true) { function foo() {} }',
53+
'(async () => { if (await true) { foo=function foo() {} } })()' ],
54+
[ 'if (await true) { class Foo{} }',
55+
'(async () => { if (await true) { class Foo{} } })()' ],
56+
[ 'if (await true) { var a = 1; }',
57+
'(async () => { if (await true) { void (a = 1); } })()' ],
58+
[ 'if (await true) { let a = 1; }',
59+
'(async () => { if (await true) { let a = 1; } })()' ],
60+
[ 'var a = await 1; let b = 2; const c = 3;',
61+
'(async () => { void (a = await 1); void (b = 2); void (c = 3); })()' ],
62+
[ 'let o = await 1, p',
63+
'(async () => { void ( (o = await 1), (p=undefined)) })()' ]
64+
];
65+
66+
for (const [input, expected] of testCases) {
67+
assert.strictEqual(processTopLevelAwait(input), expected);
68+
}

0 commit comments

Comments
 (0)