feat: async

This commit is contained in:
cha0s 2021-04-13 05:53:58 -05:00
parent 8e12cc6af4
commit f736922f66
18 changed files with 364 additions and 148 deletions

View File

@ -24,94 +24,153 @@ export default class Sandbox {
evaluateArrayExpression(node) {
const elements = [];
let isAsync = false;
for (let i = 0; i < node.elements.length; i++) {
elements.push(this.evaluate(node.elements[i]));
const {async, value} = this.evaluate(node.elements[i]);
// eslint-disable-next-line no-bitwise
isAsync |= async;
elements.push(value);
}
return elements;
return {
async: !!isAsync,
value: isAsync ? Promise.all(elements) : elements,
};
}
evaluateAssignmentExpression(node) {
const {left, right} = node;
const scope = this.nodeScope(node);
const value = this.evaluate(right);
if (types.isMemberExpression(left)) {
const {
computed,
object,
optional,
property,
} = left;
const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : property.name;
const {Vasync, value} = this.evaluate(right);
if (!types.isMemberExpression(left)) {
if (Vasync) {
return {
async: true,
value: value.then((value) => scope.set(left.name, value)),
};
}
scope.set(left.name, value);
return {value};
}
/* eslint-disable no-param-reassign */
const assign = (O, P, value, optional) => {
// eslint-disable-next-line object-curly-newline
if (optional) {
if (O) {
O[P] = value;
}
// eslint-disable-next-line babel/no-unused-expressions
O && (O[P] = value);
}
else {
O[P] = value;
}
return value;
};
/* eslint-enable no-param-reassign */
const makeAsync = (O, P, value, optional) => (
Promise
.all([O, P, value])
.then(([O, P, value]) => assign(O, P, value, optional))
);
const {
computed,
object,
optional,
property,
} = left;
const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : {value: property.name};
// eslint-disable-next-line no-bitwise
if (Vasync | O.async | P.async) {
return {
async: true,
value: makeAsync(O.value, P.value, value, optional),
};
}
else {
scope.set(left.name, value);
}
return value;
return {value: assign(O.value, P.value, value, optional)};
}
evaluateAwaitExpression({argument}) {
return {
async: true,
value: this.evaluate(argument).value,
};
}
evaluateBinaryExpression(node) {
const left = this.evaluate(node.left);
const right = this.evaluate(node.right);
switch (node.operator) {
/* eslint-disable no-multi-spaces, switch-colon-spacing */
case '+' : return left + right;
case '-' : return left - right;
case '/' : return left / right;
case '%' : return left % right;
case '*' : return left * right;
case '>' : return left > right;
case '<' : return left < right;
case 'in' : return left in right;
case '>=' : return left >= right;
case '<=' : return left <= right;
case '**' : return left ** right;
case '===': return left === right;
case '!==': return left !== right;
/* eslint-disable no-bitwise */
case '^' : return left ^ right;
case '&' : return left & right;
case '|' : return left | right;
case '>>' : return left >> right;
case '<<' : return left << right;
case '>>>': return left >>> right;
/* eslint-enable no-bitwise */
/* eslint-disable eqeqeq */
case '==' : return left == right;
case '!=' : return left != right;
/* eslint-enable eqeqeq, no-multi-spaces, switch-colon-spacing */
case 'instanceof': return left instanceof right;
default:
// eslint-disable-next-line no-console
console.error("evaluateBinaryExpression(): Can't handle operator", node.operator);
return undefined;
const binary = (left, right) => {
switch (node.operator) {
/* eslint-disable no-multi-spaces, switch-colon-spacing */
case '+' : return left + right;
case '-' : return left - right;
case '/' : return left / right;
case '%' : return left % right;
case '*' : return left * right;
case '>' : return left > right;
case '<' : return left < right;
case 'in' : return left in right;
case '>=' : return left >= right;
case '<=' : return left <= right;
case '**' : return left ** right;
case '===': return left === right;
case '!==': return left !== right;
/* eslint-disable no-bitwise */
case '^' : return left ^ right;
case '&' : return left & right;
case '|' : return left | right;
case '>>' : return left >> right;
case '<<' : return left << right;
case '>>>': return left >>> right;
/* eslint-enable no-bitwise */
/* eslint-disable eqeqeq */
case '==' : return left == right;
case '!=' : return left != right;
/* eslint-enable eqeqeq, no-multi-spaces, switch-colon-spacing */
case 'instanceof': return left instanceof right;
default:
// eslint-disable-next-line no-console
console.error("evaluateBinaryExpression(): Can't handle operator", node.operator);
return undefined;
}
};
const {async: lasync, value: left} = this.evaluate(node.left);
const {async: rasync, value: right} = this.evaluate(node.right);
if (lasync || rasync) {
return {
async: true,
value: Promise.all([left, right]).then(([left, right]) => binary(left, right)),
};
}
return {value: binary(left, right)};
}
evaluateCallExpression(node) {
let asyncArgs = false;
const args = [];
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i];
args.push(this.evaluate(arg));
const {async, value} = this.evaluate(arg);
// eslint-disable-next-line no-bitwise
asyncArgs |= async;
args.push(value);
}
const invoke = (callee, args, optional) => {
if (optional) {
return callee?.(...args);
}
return callee(...args);
};
const {callee, optional: callOptional} = node;
if (types.isMemberExpression(callee)) {
const {
computed,
object,
optional: memberOptional,
property,
} = callee;
const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : property.name;
if (!types.isMemberExpression(callee)) {
const {async, value} = this.evaluate(callee);
if (asyncArgs || async) {
return {
async: true,
value: Promise
.all([value, Promise.all(args)])
.then(([callee, args]) => invoke(callee, args, callOptional)),
};
}
return {value: invoke(value, args, callOptional)};
}
const invokeMember = (O, P, args, callOptional, memberOptional) => {
if (callOptional) {
if (memberOptional) {
return O?.[P]?.(...args);
@ -122,25 +181,38 @@ export default class Sandbox {
return O?.[P](...args);
}
return O[P](...args);
};
const {
computed,
object,
optional: memberOptional,
property,
} = callee;
const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : {value: property.name};
if (asyncArgs || O.async || P.async) {
return {
async: true,
value: Promise
.all([O.value, P.value, Promise.all(args)])
.then(([O, P, args]) => invokeMember(O, P, args, callOptional, memberOptional)),
};
}
if (callOptional) {
return this.evaluate(callee)?.(...args);
}
return this.evaluate(callee)(...args);
return {value: invokeMember(O.value, P.value, args, callOptional, memberOptional)};
}
// eslint-disable-next-line class-methods-use-this
evaluateDirectiveLiteral({value}) {
return value;
return {value};
}
evaluateIdentifier(node) {
return this.nodeScope(node).get(node.name);
return {value: this.nodeScope(node).get(node.name)};
}
// eslint-disable-next-line class-methods-use-this
evaluateLiteral({value}) {
return value;
return {value};
}
evaluateMemberExpression({
@ -149,45 +221,76 @@ export default class Sandbox {
optional,
property,
}) {
const member = (O, P) => (optional ? O?.[P] : O[P]);
const O = this.evaluate(object);
const P = computed ? this.evaluate(property) : property.name;
return optional ? O?.[P] : O[P];
const P = computed ? this.evaluate(property) : {value: property.name};
if (O.async || P.async) {
return {
async: true,
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
};
}
return {value: member(O.value, P.value)};
}
evaluateObjectExpression({properties}) {
let isAsync = false;
const entries = [];
for (let i = 0; i < properties.length; i++) {
const {computed, key, value} = properties[i];
entries.push([computed ? this.evaluate(key) : key.name, this.evaluate(value)]);
const k = computed ? this.evaluate(key) : {value: key.name};
const v = this.evaluate(value);
// eslint-disable-next-line no-bitwise
isAsync |= k.async | v.async;
if (k.async || v.async) {
entries.push(Promise.all([k.value, v.value]));
}
else {
entries.push([k.value, v.value]);
}
}
return Object.fromEntries(entries);
return {
async: !!isAsync,
value: isAsync
? Promise.all(entries).then(Object.fromEntries)
: Object.fromEntries(entries),
};
}
evaluateUpdateExpression(node) {
const {argument, operator, prefix} = node;
let value = this.evaluate(argument);
const {async, value} = this.evaluate(argument);
const scope = this.nodeScope(node);
if (prefix) {
const update = (value) => {
if (prefix) {
switch (operator) {
case '++':
return scope.set(argument.name, value + 1);
case '--':
return scope.set(argument.name, value - 1);
default:
}
}
switch (operator) {
case '++':
scope.set(argument.name, ++value);
scope.set(argument.name, value + 1);
return value;
case '--':
scope.set(argument.name, --value);
scope.set(argument.name, value - 1);
return value;
default:
}
// eslint-disable-next-line no-console
console.error("evaluateUpdateExpression(): Can't handle", operator);
return undefined;
};
if (async) {
return {
async: true,
value: Promise.resolve(value).then((value) => update(value)),
};
}
switch (operator) {
case '++':
scope.set(argument.name, value + 1);
return value;
case '--':
scope.set(argument.name, value - 1);
return value;
default:
}
return undefined;
return {value: update(value)};
}
next() {
@ -242,12 +345,16 @@ export default class Sandbox {
return this.nodeScope(this.ast);
}
run(max = 10000) {
async run(max = 10000) {
for (
let current = this.runner.next();
let {done, value: {async, value}} = this.runner.next();
// eslint-disable-next-line no-param-reassign
--max > 0 && current.done !== true;
current = this.runner.next()
--max > 0
&& done !== true
&& async
// eslint-disable-next-line no-await-in-loop
&& await value;
{done, value: {async, value}} = this.runner.next()
// eslint-disable-next-line no-empty
) {}
return this;
@ -257,6 +364,7 @@ export default class Sandbox {
const flat = [
'ArrayExpression',
'AssignmentExpression',
'AwaitExpression',
'BinaryExpression',
'CallExpression',
'DirectiveLiteral',
@ -310,26 +418,63 @@ export default class Sandbox {
if (types.isForStatement(node)) {
scope = scope.push();
}
this.setNextScope(node, scope);
if (types.isVariableDeclarator(node)) {
const {id, init} = node;
scope.allocate(id.name, this.evaluate(init));
const {async, value} = this.evaluate(init);
if (async) {
yield {
async: true,
value: Promise.resolve(value).then((value) => {
scope.allocate(id.name, value);
}),
};
}
else {
scope.allocate(id.name, value);
}
}
this.setNextScope(node, scope);
// Blocks...
if (types.isIfStatement(node)) {
keys = [this.evaluate(node.test) ? 'consequent' : 'alternate'];
const {async, value} = this.evaluate(node.test);
if (async) {
yield {
async: true,
value: Promise.resolve(value).then((value) => {
keys = [value ? 'consequent' : 'alternate'];
}),
};
}
else {
keys = [value ? 'consequent' : 'alternate'];
}
}
// Loops...
let loop = false;
if (types.isForStatement(node)) {
this.traverse(node.init).next();
const {value} = this.traverse(node.init).next();
if (value?.async) {
yield value;
}
}
do {
if (
types.isForStatement(node)
|| types.isWhileStatement(node)
) {
keys = this.evaluate(node.test) ? ['body'] : [];
const {async, value} = this.evaluate(node.test);
if (async) {
yield {
async: true,
// eslint-disable-next-line no-loop-func
value: Promise.resolve(value).then((value) => {
keys = value ? ['body'] : [];
}),
};
}
else {
keys = value ? ['body'] : [];
}
loop = keys.length > 0;
}
// Recur...
@ -339,17 +484,32 @@ export default class Sandbox {
}
// Loops...
if (types.isForStatement(node)) {
this.traverse(node.update).next();
const {value} = this.traverse(node.update).next();
if (value?.async) {
yield value;
}
if (loop) {
yield;
yield {loop: 'for', value: undefined};
}
}
if (types.isDoWhileStatement(node)) {
loop = this.evaluate(node.test);
yield;
const test = this.evaluate(node.test);
if (test.async) {
yield {
async: true,
// eslint-disable-next-line no-loop-func
value: Promise.resolve(test.value).then((value) => {
loop = value;
}),
};
}
else {
loop = test.value;
}
yield {loop: 'doWhile', value: undefined};
}
if (types.isWhileStatement(node) && loop) {
yield;
yield {loop: 'while', value: undefined};
}
} while (loop);
// Scope...

View File

@ -67,10 +67,11 @@ export default class Scope {
while (walk) {
if (walk.context[key]) {
walk.context[key].value = value;
return;
return value;
}
walk = walk.parent;
}
return undefined;
}
}

View File

@ -4,6 +4,15 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates ArrayExpression', () => {
expect(new Sandbox(parse('[1, 2, 3]')).next())
expect(new Sandbox(parse('[1, 2, 3]')).next().value)
.to.deep.include({value: [1, 2, 3]});
});
it('evaluates async ArrayExpression', async () => {
const o = {allowAwaitOutsideFunction: true};
const {async, value} = new Sandbox(parse('[await 1, 2, 3]', o)).next().value;
expect(async)
.to.be.true;
expect(await value)
.to.deep.equal([1, 2, 3]);
});

View File

@ -4,6 +4,6 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates AssignmentExpression', () => {
expect(new Sandbox(parse('let foo = 69; foo = 420')).next())
expect(new Sandbox(parse('let foo = 69; foo = 420')).next().value)
.to.deep.include({value: 420});
});

View File

@ -0,0 +1,18 @@
import {parse} from '@babel/parser';
import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates AwaitExpression', async () => {
const context = {
wait: () => new Promise((resolve) => setTimeout(() => resolve(420), 0)),
};
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('const test = await wait(); test', o), context);
const {value} = sandbox.next();
expect(value.async)
.to.be.true;
await value.value;
expect(sandbox.next().value.value)
.to.equal(420);
});

View File

@ -4,48 +4,48 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates BinaryExpression', () => {
expect(new Sandbox(parse('1 + 2')).next())
expect(new Sandbox(parse('1 + 2')).next().value)
.to.deep.include({value: 3});
expect(new Sandbox(parse('1 - 2')).next())
expect(new Sandbox(parse('1 - 2')).next().value)
.to.deep.include({value: -1});
expect(new Sandbox(parse('4 / 2')).next())
expect(new Sandbox(parse('4 / 2')).next().value)
.to.deep.include({value: 2});
expect(new Sandbox(parse('10 % 3')).next())
expect(new Sandbox(parse('10 % 3')).next().value)
.to.deep.include({value: 1});
expect(new Sandbox(parse('1 * 2')).next())
expect(new Sandbox(parse('1 * 2')).next().value)
.to.deep.include({value: 2});
expect(new Sandbox(parse('1 > 2')).next())
expect(new Sandbox(parse('1 > 2')).next().value)
.to.deep.include({value: false});
expect(new Sandbox(parse('1 < 2')).next())
expect(new Sandbox(parse('1 < 2')).next().value)
.to.deep.include({value: true});
expect(new Sandbox(parse('const foo = {a: 69}; "a" in foo')).next())
expect(new Sandbox(parse('const foo = {a: 69}; "a" in foo')).next().value)
.to.deep.include({value: true});
expect(new Sandbox(parse('const foo = {a: 69}; "b" in foo')).next())
expect(new Sandbox(parse('const foo = {a: 69}; "b" in foo')).next().value)
.to.deep.include({value: false});
expect(new Sandbox(parse('1 >= 2')).next())
expect(new Sandbox(parse('1 >= 2')).next().value)
.to.deep.include({value: false});
expect(new Sandbox(parse('1 <= 2')).next())
expect(new Sandbox(parse('1 <= 2')).next().value)
.to.deep.include({value: true});
expect(new Sandbox(parse('2 ** 3')).next())
expect(new Sandbox(parse('2 ** 3')).next().value)
.to.deep.include({value: 8});
expect(new Sandbox(parse('1 === 2')).next())
expect(new Sandbox(parse('1 === 2')).next().value)
.to.deep.include({value: false});
expect(new Sandbox(parse('1 !== 2')).next())
expect(new Sandbox(parse('1 !== 2')).next().value)
.to.deep.include({value: true});
expect(new Sandbox(parse('7 & 3')).next())
expect(new Sandbox(parse('7 & 3')).next().value)
.to.deep.include({value: 3});
expect(new Sandbox(parse('1 | 2')).next())
expect(new Sandbox(parse('1 | 2')).next().value)
.to.deep.include({value: 3});
expect(new Sandbox(parse('16 >> 2')).next())
expect(new Sandbox(parse('16 >> 2')).next().value)
.to.deep.include({value: 4});
expect(new Sandbox(parse('16 >>> 5')).next())
expect(new Sandbox(parse('16 >>> 5')).next().value)
.to.deep.include({value: 0});
expect(new Sandbox(parse('1 << 2')).next())
expect(new Sandbox(parse('1 << 2')).next().value)
.to.deep.include({value: 4});
expect(new Sandbox(parse('1 ^ 2')).next())
expect(new Sandbox(parse('1 ^ 2')).next().value)
.to.deep.include({value: 3});
expect(new Sandbox(parse('1 == 2')).next())
expect(new Sandbox(parse('1 == 2')).next().value)
.to.deep.include({value: false});
expect(new Sandbox(parse('1 != 2')).next())
expect(new Sandbox(parse('1 != 2')).next().value)
.to.deep.include({value: true});
});

View File

@ -4,6 +4,6 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates CallExpression', () => {
expect(new Sandbox(parse('test()'), {test: () => 69}).next())
expect(new Sandbox(parse('test()'), {test: () => 69}).next().value)
.to.deep.include({value: 69});
});

View File

@ -5,7 +5,7 @@ import Sandbox from '../src/sandbox';
it('evaluates DoWhileStatement', () => {
const sandbox = new Sandbox(parse('let i = 0; do { i++; } while (false); i'));
expect(sandbox.next()).to.deep.include({value: 0});
expect(sandbox.next()).to.deep.include({value: undefined});
expect(sandbox.next()).to.deep.include({value: 1});
expect(sandbox.next().value).to.deep.include({value: 0});
expect(sandbox.next().value).to.deep.include({value: undefined});
expect(sandbox.next().value).to.deep.include({value: 1});
});

View File

@ -6,7 +6,7 @@ import Sandbox from '../src/sandbox';
it('evaluates ForStatement', () => {
const sandbox = new Sandbox(parse('for (let i = 0; i < 5; ++i) { i; }'));
for (let i = 0; i < 5; ++i) {
expect(sandbox.next()).to.deep.include({value: i});
expect(sandbox.next().value).to.deep.include({value: i});
// Loop...
sandbox.next();
}

View File

@ -4,6 +4,6 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates Identifier', () => {
expect(new Sandbox(parse('const test = 69; test')).next())
expect(new Sandbox(parse('const test = 69; test')).next().value)
.to.deep.include({value: 69});
});

View File

@ -5,7 +5,7 @@ import Sandbox from '../src/sandbox';
it('evaluates IfStatement', () => {
expect(new Sandbox(parse('if (false) { 69; }')).next())
.to.deep.include({value: undefined});
expect(new Sandbox(parse('if (true) { 69; }')).next())
.to.deep.include({done: true});
expect(new Sandbox(parse('if (true) { 69; }')).next().value)
.to.deep.include({value: 69});
});

View File

@ -4,10 +4,10 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates Literal', () => {
expect(new Sandbox(parse('69')).next())
expect(new Sandbox(parse('69')).next().value)
.to.deep.include({value: 69});
expect(new Sandbox(parse('"420"')).next())
expect(new Sandbox(parse('"420"')).next().value)
.to.deep.include({value: '420'});
expect(new Sandbox(parse('420.69')).next())
expect(new Sandbox(parse('420.69')).next().value)
.to.deep.include({value: 420.69});
});

View File

@ -4,6 +4,21 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates MemberExpression', () => {
expect(new Sandbox(parse('a.b.c'), {a: {b: {c: 69}}}).next())
expect(new Sandbox(parse('a.b.c'), {a: {b: {c: 69}}}).next().value)
.to.deep.include({value: 69});
});
it('evaluates async MemberExpression', async () => {
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse('const aa = await a; aa.b.c', o), {a: {b: {c: 69}}});
let {async, value} = sandbox.next().value;
expect(async)
.to.be.true;
expect(await value)
.to.equal(undefined);
({async, value} = sandbox.next().value);
expect(async)
.to.be.undefined;
expect(value)
.to.equal(69);
});

View File

@ -4,6 +4,6 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates ObjectExpression', () => {
expect(new Sandbox(parse('({a: 1, b: 2})')).next())
expect(new Sandbox(parse('({a: 1, b: 2})')).next().value)
.to.deep.include({value: {a: 1, b: 2}});
});

View File

@ -0,0 +1,13 @@
import {parse} from '@babel/parser';
import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('runs', async () => {
const context = {
wait: () => new Promise((resolve) => setTimeout(() => resolve(), 0)),
}
const o = {allowAwaitOutsideFunction: true};
const sandbox = new Sandbox(parse(`await wait(); 1 + 3 * 2;`, o), context);
await sandbox.run();
});

View File

@ -5,13 +5,13 @@ import Sandbox from '../src/sandbox';
it('scopes BlockStatement', () => {
const sandbox = new Sandbox(parse('if (true) { const foo = 69; foo; } foo;'));
expect(sandbox.next())
expect(sandbox.next().value)
.to.deep.include({value: 69});
expect(sandbox.next())
expect(sandbox.next().value)
.to.deep.include({value: undefined});
});
it('scopes ForStatement', () => {
expect(new Sandbox(parse('for (let i = 0; i < 5; ++i) {} i;')).next())
expect(new Sandbox(parse('for (let i = 0; i < 5; ++i) {} i;')).next().value)
.to.deep.include({value: undefined});
});

View File

@ -4,12 +4,12 @@ import {expect} from 'chai';
import Sandbox from '../src/sandbox';
it('evaluates UpdateExpression', () => {
expect(new Sandbox(parse('a++'), {a: 1}).next())
expect(new Sandbox(parse('a++'), {a: 1}).next().value)
.to.deep.include({value: 1});
expect(new Sandbox(parse('++a'), {a: 1}).next())
expect(new Sandbox(parse('++a'), {a: 1}).next().value)
.to.deep.include({value: 2});
expect(new Sandbox(parse('a--'), {a: 1}).next())
expect(new Sandbox(parse('a--'), {a: 1}).next().value)
.to.deep.include({value: 1});
expect(new Sandbox(parse('--a'), {a: 1}).next())
expect(new Sandbox(parse('--a'), {a: 1}).next().value)
.to.deep.include({value: 0});
});

View File

@ -6,7 +6,7 @@ import Sandbox from '../src/sandbox';
it('evaluates WhileStatement', () => {
const sandbox = new Sandbox(parse('let i = 0; while (i < 5) { i++; }'));
for (let i = 0; i < 5; ++i) {
expect(sandbox.next()).to.deep.include({value: i});
expect(sandbox.next().value).to.deep.include({value: i});
// Loop...
sandbox.next();
}