Skip to content

Commit

Permalink
repl: support top-level await
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
TimothyGu committed Nov 16, 2017
1 parent ab64b6d commit eeab7bc
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 7 deletions.
128 changes: 128 additions & 0 deletions lib/internal/repl/await.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

const acorn = require('internal/deps/acorn/dist/acorn');
const walk = require('internal/deps/acorn/dist/walk');

const noop = () => {};
const visitorsWithoutAncestors = {
ClassDeclaration(node, state, c) {
if (state.ancestors[state.ancestors.length - 2] === state.body) {
state.prepend(node, `${node.id.name}=`);
}
walk.base.ClassDeclaration(node, state, c);
},
FunctionDeclaration(node, state, c) {
state.prepend(node, `${node.id.name}=`);
},
FunctionExpression: noop,
ArrowFunctionExpression: noop,
MethodDefinition: noop,
AwaitExpression(node, state, c) {
state.containsAwait = true;
walk.base.AwaitExpression(node, state, c);
},
ReturnStatement(node, state, c) {
state.containsReturn = true;
walk.base.ReturnStatement(node, state, c);
},
VariableDeclaration(node, state, c) {
if (node.kind === 'var' ||
state.ancestors[state.ancestors.length - 2] === state.body) {
if (node.declarations.length === 1) {
state.replace(node.start, node.start + node.kind.length, 'void');
} else {
state.replace(node.start, node.start + node.kind.length, 'void (');
}

for (const decl of node.declarations) {
state.prepend(decl, '(');
state.append(decl, decl.init ? ')' : '=undefined)');
}

if (node.declarations.length !== 1) {
state.append(node.declarations[node.declarations.length - 1], ')');
}
}

walk.base.VariableDeclaration(node, state, c);
}
};

const visitors = {};
for (const nodeType of Object.keys(walk.base)) {
const callback = visitorsWithoutAncestors[nodeType] || walk.base[nodeType];
visitors[nodeType] = (node, state, c) => {
const isNew = node !== state.ancestors[state.ancestors.length - 1];
if (isNew) {
state.ancestors.push(node);
}
callback(node, state, c);
if (isNew) {
state.ancestors.pop();
}
};
}

function processTopLevelAwait(src) {
const wrapped = `(async () => { ${src} })()`;
const wrappedArray = wrapped.split('');
let root;
try {
root = acorn.parse(wrapped, { ecmaVersion: 8 });
} catch (err) {
return null;
}
const body = root.body[0].expression.callee.body;
const state = {
body,
ancestors: [],
replace(from, to, str) {
for (var i = from; i < to; i++) {
wrappedArray[i] = '';
}
if (from === to) str += wrappedArray[from];
wrappedArray[from] = str;
},
prepend(node, str) {
wrappedArray[node.start] = str + wrappedArray[node.start];
},
append(node, str) {
wrappedArray[node.end - 1] += str;
},
containsAwait: false,
containsReturn: false
};

walk.recursive(body, state, visitors);

// Do not transform if
// 1. False alarm: there isn't actually an await expression.
// 2. There is a top-level return, which is not allowed.
if (!state.containsAwait || state.containsReturn) {
return null;
}

const last = body.body[body.body.length - 1];
if (last.type === 'ExpressionStatement') {
// For an expression statement of the form
// ( expr ) ;
// ^^^^^^^^^^ // last
// ^^^^ // last.expression
//
// We do not want the left parenthesis before the `return` keyword;
// therefore we prepend the `return (` to `last`.
//
// On the other hand, we do not want the right parenthesis after the
// semicolon. Since there can only be more right parentheses between
// last.expression.end and the semicolon, appending one more to
// last.expression should be fine.
state.prepend(last, 'return (');
state.append(last.expression, ')');
}

return wrappedArray.join('');
}

module.exports = {
processTopLevelAwait
};
86 changes: 79 additions & 7 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
'use strict';

const internalModule = require('internal/module');
const { processTopLevelAwait } = require('internal/repl/await');
const internalUtil = require('internal/util');
const { isTypedArray } = require('internal/util/types');
const util = require('util');
Expand Down Expand Up @@ -200,6 +201,7 @@ function REPLServer(prompt,
function defaultEval(code, context, file, cb) {
var err, result, script, wrappedErr;
var wrappedCmd = false;
var awaitPromise = false;
var input = code;

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

if (code.includes('await')) {
const potentialWrappedCode = processTopLevelAwait(code);
if (potentialWrappedCode !== null) {
code = potentialWrappedCode;
wrappedCmd = true;
awaitPromise = true;
}
}

// first, create the Script object to check the syntax

if (code === '\n')
Expand All @@ -231,8 +242,9 @@ function REPLServer(prompt,
} catch (e) {
debug('parse error %j', code, e);
if (wrappedCmd) {
wrappedCmd = false;
// unwrap and try again
wrappedCmd = false;
awaitPromise = false;
code = input;
wrappedErr = e;
continue;
Expand All @@ -251,6 +263,20 @@ function REPLServer(prompt,
// predefined RegExp properties `RegExp.$1`, `RegExp.$2` ... `RegExp.$9`
regExMatcher.test(savedRegExMatches.join(sep));

let finished = false;
function finishExecution(err, result) {
if (finished) return;
finished = true;

// After executing the current expression, store the values of RegExp
// predefined properties back in `savedRegExMatches`
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
savedRegExMatches[idx] = RegExp[`$${idx}`];
}

cb(err, result);
}

if (!err) {
// Unset raw mode during evaluation so that Ctrl+C raises a signal.
let previouslyInRawMode;
Expand Down Expand Up @@ -301,15 +327,53 @@ function REPLServer(prompt,
return;
}
}
}

// After executing the current expression, store the values of RegExp
// predefined properties back in `savedRegExMatches`
for (var idx = 1; idx < savedRegExMatches.length; idx += 1) {
savedRegExMatches[idx] = RegExp[`$${idx}`];
if (awaitPromise && !err) {
let sigintListener;
pause();
let promise = result;
if (self.breakEvalOnSigint) {
const interrupt = new Promise((resolve, reject) => {
sigintListener = () => {
reject(new Error('Script execution interrupted.'));
};
prioritizedSigintQueue.add(sigintListener);
});
promise = Promise.race([promise, interrupt]);
}

promise.then((result) => {
// Remove prioritized SIGINT listener if it was not called.
// TODO(TimothyGu): Use Promise.prototype.finally when it becomes
// available.
prioritizedSigintQueue.delete(sigintListener);

finishExecution(undefined, result);
unpause();
}, (err) => {
// Remove prioritized SIGINT listener if it was not called.
prioritizedSigintQueue.delete(sigintListener);

if (err.message === 'Script execution interrupted.') {
// The stack trace for this case is not very useful anyway.
Object.defineProperty(err, 'stack', { value: '' });
}

unpause();
if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
finishExecution(err);
});
}
}

cb(err, result);
if (!awaitPromise || err) {
finishExecution(err, result);
}
}

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

var sawSIGINT = false;
var sawCtrlD = false;
const prioritizedSigintQueue = new Set();
self.on('SIGINT', function onSigInt() {
if (prioritizedSigintQueue.size > 0) {
for (const task of prioritizedSigintQueue) {
task();
}
return;
}

var empty = self.line.length === 0;
self.clearLine();
_turnOffEditorMode(self);
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
'lib/internal/process/write-coverage.js',
'lib/internal/readline.js',
'lib/internal/repl.js',
'lib/internal/repl/await.js',
'lib/internal/socket_list.js',
'lib/internal/test/unicode.js',
'lib/internal/tls.js',
Expand Down
68 changes: 68 additions & 0 deletions test/parallel/test-repl-preprocess-top-level-await.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';

require('../common');
const assert = require('assert');
const { processTopLevelAwait } = require('internal/repl/await');

// Flags: --expose-internals

// This test was created based on
// https://cs.chromium.org/chromium/src/third_party/WebKit/LayoutTests/http/tests/inspector-unit/preprocess-top-level-awaits.js?rcl=358caaba5e763e71c4abb9ada2d9cd8b1188cac9

const testCases = [
[ '0',
null ],
[ 'await 0',
'(async () => { return (await 0) })()' ],
[ 'await 0;',
'(async () => { return (await 0); })()' ],
[ '(await 0)',
'(async () => { return ((await 0)) })()' ],
[ '(await 0);',
'(async () => { return ((await 0)); })()' ],
[ 'async function foo() { await 0; }',
null ],
[ 'async () => await 0',
null ],
[ 'class A { async method() { await 0 } }',
null ],
[ 'await 0; return 0;',
null ],
[ 'var a = await 1',
'(async () => { void (a = await 1) })()' ],
[ 'let a = await 1',
'(async () => { void (a = await 1) })()' ],
[ 'const a = await 1',
'(async () => { void (a = await 1) })()' ],
[ 'for (var i = 0; i < 1; ++i) { await i }',
'(async () => { for (void (i = 0); i < 1; ++i) { await i } })()' ],
[ 'for (let i = 0; i < 1; ++i) { await i }',
'(async () => { for (let i = 0; i < 1; ++i) { await i } })()' ],
[ 'var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
'(async () => { void ( ({a} = {a:1}), ([b] = [1]), ' +
'({c:{d}} = {c:{d: await 1}})) })()' ],
/* eslint-disable no-template-curly-in-string */
[ 'console.log(`${(await { a: 1 }).a}`)',
'(async () => { return (console.log(`${(await { a: 1 }).a}`)) })()' ],
/* eslint-enable no-template-curly-in-string */
[ 'await 0; function foo() {}',
'(async () => { await 0; foo=function foo() {} })()' ],
[ 'await 0; class Foo {}',
'(async () => { await 0; Foo=class Foo {} })()' ],
[ 'if (await true) { function foo() {} }',
'(async () => { if (await true) { foo=function foo() {} } })()' ],
[ 'if (await true) { class Foo{} }',
'(async () => { if (await true) { class Foo{} } })()' ],
[ 'if (await true) { var a = 1; }',
'(async () => { if (await true) { void (a = 1); } })()' ],
[ 'if (await true) { let a = 1; }',
'(async () => { if (await true) { let a = 1; } })()' ],
[ 'var a = await 1; let b = 2; const c = 3;',
'(async () => { void (a = await 1); void (b = 2); void (c = 3); })()' ],
[ 'let o = await 1, p',
'(async () => { void ( (o = await 1), (p=undefined)) })()' ]
];

for (const [input, expected] of testCases) {
assert.strictEqual(processTopLevelAwait(input), expected);
}

0 comments on commit eeab7bc

Please sign in to comment.