refactor!: sandbox
This commit is contained in:
parent
31722ab4a0
commit
398dd66594
|
@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) {
|
||||||
.then(([left, right]) => binary(left, right)),
|
.then(([left, right]) => binary(left, right)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {value: binary(left.value, right.value)};
|
return {
|
||||||
|
async: false,
|
||||||
|
value: binary(left.value, right.value),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,8 @@ export default function(node, {evaluate, scope}) {
|
||||||
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
|
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args)};
|
return {
|
||||||
|
async: false,
|
||||||
|
value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export default function(node, {scope}) {
|
export default function(node, {scope}) {
|
||||||
return {value: scope.get(node.name)};
|
return {async: false, value: scope.get(node.name)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export default function({value}) {
|
export default function({value}) {
|
||||||
return {value};
|
return {async: false, value};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) {
|
||||||
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
|
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {value: member(O.value, P.value)};
|
return {async: false, value: member(O.value, P.value)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) {
|
||||||
value: Promise.resolve(arg.value).then(unary),
|
value: Promise.resolve(arg.value).then(unary),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {value: unary(arg.value)};
|
return {async: false, value: unary(arg.value)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,5 @@ export default function(node, {evaluate, scope}) {
|
||||||
/* v8 ignore next */
|
/* v8 ignore next */
|
||||||
throw new Error(`operator not implemented: ${operator}`);
|
throw new Error(`operator not implemented: ${operator}`);
|
||||||
};
|
};
|
||||||
return {value: update(value)};
|
return {async: false, value: update(value)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import evaluate from '@/astride/evaluate.js';
|
import evaluate from '@/astride/evaluate.js';
|
||||||
import Scope from '@/astride/scope.js';
|
import Scope from '@/astride/scope.js';
|
||||||
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js';
|
import traverse from '@/astride/traverse.js';
|
||||||
import {
|
import {
|
||||||
isArrayPattern,
|
isArrayPattern,
|
||||||
isBlockStatement,
|
isBlockStatement,
|
||||||
isDoWhileStatement,
|
|
||||||
isExpressionStatement,
|
|
||||||
isForStatement,
|
isForStatement,
|
||||||
isIdentifier,
|
isIdentifier,
|
||||||
isIfStatement,
|
|
||||||
isObjectPattern,
|
isObjectPattern,
|
||||||
isReturnStatement,
|
|
||||||
isVariableDeclarator,
|
|
||||||
isWhileStatement,
|
|
||||||
} from '@/astride/types.js';
|
} from '@/astride/types.js';
|
||||||
|
|
||||||
|
const YIELD_NONE = 0;
|
||||||
|
const YIELD_PROMISE = 1;
|
||||||
|
const YIELD_LOOP_UPDATE = 2;
|
||||||
|
const YIELD_RETURN = 3;
|
||||||
|
|
||||||
export default class Sandbox {
|
export default class Sandbox {
|
||||||
|
|
||||||
ast;
|
ast;
|
||||||
generator;
|
$$execution;
|
||||||
scopes;
|
scopes;
|
||||||
|
|
||||||
constructor(ast, context = {}) {
|
constructor(ast, context = {}) {
|
||||||
|
@ -76,7 +75,7 @@ export default class Sandbox {
|
||||||
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return init;
|
||||||
}
|
}
|
||||||
|
|
||||||
destructureObject(id, init) {
|
destructureObject(id, init) {
|
||||||
|
@ -85,7 +84,7 @@ export default class Sandbox {
|
||||||
for (let i = 0; i < properties.length; ++i) {
|
for (let i = 0; i < properties.length; ++i) {
|
||||||
const property = properties[i];
|
const property = properties[i];
|
||||||
if (isObjectPattern(property.value)) {
|
if (isObjectPattern(property.value)) {
|
||||||
this.destructureObject(property.value, init[property.key.name], scope);
|
this.destructureObject(property.value, init[property.key.name]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.allocate(
|
scope.allocate(
|
||||||
|
@ -94,6 +93,7 @@ export default class Sandbox {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return init;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate(node) {
|
evaluate(node) {
|
||||||
|
@ -103,153 +103,370 @@ export default class Sandbox {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
*execute(node, parent) {
|
evaluateToResult(node) {
|
||||||
let keys = TRAVERSAL_PATH[node.type];
|
const {async, value} = this.evaluate(node);
|
||||||
if (isVariableDeclarator(node)) {
|
return {
|
||||||
const {id} = node;
|
yield: async ? YIELD_PROMISE : YIELD_NONE,
|
||||||
const scope = this.scopes.get(node);
|
value,
|
||||||
if (null === node.init) {
|
};
|
||||||
scope.allocate(id.name, undefined);
|
}
|
||||||
}
|
|
||||||
else {
|
executeSync(node, depth) {
|
||||||
const init = this.evaluate(node.init);
|
let result;
|
||||||
if (isIdentifier(id)) {
|
const isReplaying = depth < this.$$execution.stack.length;
|
||||||
if (init.async) {
|
const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
|
||||||
yield {
|
// console.log(
|
||||||
async: true,
|
// Array(depth).fill(' ').join(''),
|
||||||
value: Promise.resolve(init.value).then((value) => {
|
// node.type,
|
||||||
scope.allocate(id.name, value);
|
// isReplaying
|
||||||
}),
|
// ? (['replaying', ...(isReplayingThisLevel ? ['this level'] : [])].join(' '))
|
||||||
};
|
// : '',
|
||||||
}
|
// );
|
||||||
else {
|
if (!isReplaying) {
|
||||||
scope.allocate(id.name, init.value);
|
this.$$execution.stack.push(node);
|
||||||
}
|
}
|
||||||
}
|
// Substitute executing the node for its deferred result.
|
||||||
else if (isArrayPattern(id)) {
|
else if (isReplayingThisLevel) {
|
||||||
const promiseOrVoid = init.async
|
if (this.$$execution.stack[depth] === node) {
|
||||||
? Promise.resolve(init.value).then((init) => this.destructureArray(id, init))
|
result = this.$$execution.deferred.get(node);
|
||||||
: this.destructureArray(id, init.value);
|
this.$$execution.deferred.delete(node);
|
||||||
if (promiseOrVoid) {
|
this.$$execution.stack.pop();
|
||||||
yield {
|
return result;
|
||||||
async: true,
|
|
||||||
value: promiseOrVoid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (isObjectPattern(id)) {
|
|
||||||
const promiseOrVoid = init.async
|
|
||||||
? Promise.resolve(init.value).then((init) => this.destructureObject(id, init))
|
|
||||||
: this.destructureObject(id, init.value);
|
|
||||||
if (promiseOrVoid) {
|
|
||||||
yield {
|
|
||||||
async: true,
|
|
||||||
value: promiseOrVoid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Blocks...
|
switch (node.type) {
|
||||||
if (isIfStatement(node)) {
|
case 'ArrayExpression':
|
||||||
const {async, value} = this.evaluate(node.test);
|
case 'AssignmentExpression':
|
||||||
const branch = (value) => {
|
case 'BinaryExpression':
|
||||||
keys = [value ? 'consequent' : 'alternate'];
|
case 'CallExpression':
|
||||||
};
|
case 'ObjectExpression':
|
||||||
if (async) {
|
case 'Identifier':
|
||||||
yield {
|
case 'UnaryExpression':
|
||||||
async: true,
|
case 'UpdateExpression': {
|
||||||
value: Promise.resolve(value).then(branch),
|
result = this.evaluateToResult(node);
|
||||||
};
|
if (result.yield) {
|
||||||
}
|
|
||||||
else {
|
|
||||||
branch(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Loops...
|
|
||||||
let loop = false;
|
|
||||||
if (isForStatement(node)) {
|
|
||||||
const {value} = this.execute(node.init, node).next();
|
|
||||||
if (value?.async) {
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
if (
|
|
||||||
isForStatement(node)
|
|
||||||
|| isWhileStatement(node)
|
|
||||||
) {
|
|
||||||
const {async, value} = this.evaluate(node.test);
|
|
||||||
if (async) {
|
|
||||||
yield {
|
|
||||||
async: true,
|
|
||||||
value: Promise.resolve(value).then((value) => {
|
|
||||||
keys = value ? ['body'] : [];
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
keys = value ? ['body'] : [];
|
|
||||||
}
|
|
||||||
loop = keys.length > 0;
|
|
||||||
}
|
|
||||||
// Recur...
|
|
||||||
let children = [];
|
|
||||||
if (keys instanceof Function) {
|
|
||||||
children = keys(node);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (const key of keys) {
|
|
||||||
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const child of children) {
|
|
||||||
if (!child) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const result = yield* this.execute(child, node);
|
|
||||||
if (result) {
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Loops...
|
case 'AwaitExpression': {
|
||||||
if (isForStatement(node)) {
|
const coerce = !this.$$execution.deferred.has(node.argument);
|
||||||
const result = this.execute(node.update, node).next();
|
result = this.executeSync(node.argument, depth + 1);
|
||||||
if (result.value?.async) {
|
if (coerce) {
|
||||||
yield result.value;
|
result = {
|
||||||
}
|
yield: YIELD_PROMISE,
|
||||||
}
|
value: result.value,
|
||||||
if (isDoWhileStatement(node)) {
|
|
||||||
const test = this.evaluate(node.test);
|
|
||||||
if (test.async) {
|
|
||||||
yield {
|
|
||||||
async: true,
|
|
||||||
value: Promise.resolve(test.value).then((value) => {
|
|
||||||
loop = value;
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
if (result.yield) {
|
||||||
loop = test.value;
|
return result;
|
||||||
}
|
}
|
||||||
|
this.$$execution.deferred.delete(node.argument);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} while (loop);
|
case 'BlockStatement': {
|
||||||
// Top-level return.
|
let skipping = isReplaying;
|
||||||
if (isReturnStatement(node)) {
|
for (const child of node.body) {
|
||||||
return !node.argument ? {value: undefined} : this.evaluate(node.argument);
|
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||||
}
|
skipping = false;
|
||||||
if (isExpressionStatement(node)) {
|
}
|
||||||
yield this.evaluate(node.expression);
|
if (skipping) {
|
||||||
}
|
continue;
|
||||||
// yield ForStatement afterthought.
|
}
|
||||||
if (isForStatement(parent) && node === parent.update) {
|
result = this.executeSync(child, depth + 1);
|
||||||
yield this.evaluate(node);
|
if (result.yield) {
|
||||||
/* v8 ignore next */
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ConditionalExpression': {
|
||||||
|
const test = this.executeSync(node.test, depth + 1);
|
||||||
|
if (test.yield) {
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
if (test.value) {
|
||||||
|
result = this.executeSync(node.consequent, depth + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = this.executeSync(node.alternate, depth + 1);
|
||||||
|
}
|
||||||
|
if (result.yield) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'DoWhileStatement': {
|
||||||
|
const shouldVisitChild = (child) => (
|
||||||
|
isReplaying
|
||||||
|
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) {
|
||||||
|
this.$$execution.stack.pop();
|
||||||
|
}
|
||||||
|
result = this.$$execution.deferred.get(node.body);
|
||||||
|
if (shouldVisitChild(node.body)) {
|
||||||
|
const body = this.executeSync(node.body, depth + 1);
|
||||||
|
if (body.yield) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
this.$$execution.deferred.set(node.body, body);
|
||||||
|
}
|
||||||
|
if (shouldVisitChild(node.test)) {
|
||||||
|
const test = this.executeSync(node.test, depth + 1);
|
||||||
|
if (test.yield) {
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
if (!test.value) {
|
||||||
|
this.$$execution.deferred.delete(node.body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Yield
|
||||||
|
this.$$execution.stack.push(undefined);
|
||||||
|
return {
|
||||||
|
value: undefined,
|
||||||
|
yield: YIELD_LOOP_UPDATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'ExpressionStatement': {
|
||||||
|
result = this.executeSync(node.expression, depth + 1);
|
||||||
|
if (result.yield) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ForStatement': {
|
||||||
|
const shouldVisitChild = (child) => (
|
||||||
|
isReplaying
|
||||||
|
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
if (shouldVisitChild(node.init)) {
|
||||||
|
const init = this.executeSync(node.init, depth + 1);
|
||||||
|
if (init.yield) {
|
||||||
|
return init;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) {
|
||||||
|
this.$$execution.stack.pop();
|
||||||
|
}
|
||||||
|
result = this.$$execution.deferred.get(node.body) || {
|
||||||
|
value: undefined,
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
};
|
||||||
|
if (shouldVisitChild(node.test)) {
|
||||||
|
const test = this.executeSync(node.test, depth + 1);
|
||||||
|
if (test.yield) {
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
if (!test.value) {
|
||||||
|
this.$$execution.deferred.delete(node.body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldVisitChild(node.body)) {
|
||||||
|
const body = this.executeSync(node.body, depth + 1);
|
||||||
|
if (body.yield) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
this.$$execution.deferred.set(node.body, body);
|
||||||
|
}
|
||||||
|
if (shouldVisitChild(node.update)) {
|
||||||
|
const update = this.executeSync(node.update, depth + 1);
|
||||||
|
if (update.yield) {
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
this.$$execution.deferred.set(node.update, update);
|
||||||
|
}
|
||||||
|
// Yield
|
||||||
|
this.$$execution.stack.push(undefined);
|
||||||
|
const update = this.$$execution.deferred.get(node.update);
|
||||||
|
this.$$execution.deferred.delete(node.update);
|
||||||
|
return {
|
||||||
|
value: update.value,
|
||||||
|
yield: YIELD_LOOP_UPDATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'IfStatement': {
|
||||||
|
const test = this.executeSync(node.test, depth + 1);
|
||||||
|
if (test.yield) {
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
if (test.value) {
|
||||||
|
result = this.executeSync(node.consequent, depth + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = this.executeSync(node.alternate, depth + 1);
|
||||||
|
}
|
||||||
|
if (result.yield) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Literal': {
|
||||||
|
result = {value: node.value, yield: YIELD_NONE};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Program': {
|
||||||
|
let skipping = isReplaying;
|
||||||
|
for (const child of node.body) {
|
||||||
|
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
if (skipping) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result = this.executeSync(child, depth + 1);
|
||||||
|
if (result.yield) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = {yield: YIELD_RETURN, value: result.value};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ReturnStatement': {
|
||||||
|
if (!node.argument) {
|
||||||
|
return {
|
||||||
|
value: undefined,
|
||||||
|
yield: YIELD_RETURN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const argument = this.executeSync(node.argument, depth + 1);
|
||||||
|
if (argument.yield) {
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: argument.value,
|
||||||
|
yield: YIELD_RETURN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'VariableDeclaration': {
|
||||||
|
let skipping = isReplaying;
|
||||||
|
for (const child of node.declarations) {
|
||||||
|
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||||
|
skipping = false;
|
||||||
|
}
|
||||||
|
if (skipping) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const result = this.executeSync(child, depth + 1);
|
||||||
|
if (result.yield) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = {value: undefined, yield: YIELD_NONE};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'VariableDeclarator': {
|
||||||
|
const {id} = node;
|
||||||
|
const scope = this.scopes.get(node);
|
||||||
|
if (null === node.init) {
|
||||||
|
scope.allocate(id.name, undefined);
|
||||||
|
result = {value: undefined, yield: YIELD_NONE};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const init = this.executeSync(node.init, depth + 1);
|
||||||
|
if (isIdentifier(id)) {
|
||||||
|
if (init.yield) {
|
||||||
|
return {
|
||||||
|
value: Promise.resolve(init.value)
|
||||||
|
.then((value) => scope.allocate(id.name, value)),
|
||||||
|
yield: init.yield,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = {
|
||||||
|
value: scope.allocate(id.name, init.value),
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isArrayPattern(id)) {
|
||||||
|
if (init.yield) {
|
||||||
|
return {
|
||||||
|
value: Promise.resolve(init.value)
|
||||||
|
.then((value) => this.destructureArray(id, value)),
|
||||||
|
yield: init.yield,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = {
|
||||||
|
value: this.destructureArray(id, init.value),
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isObjectPattern(id)) {
|
||||||
|
if (init.yield) {
|
||||||
|
return {
|
||||||
|
value: Promise.resolve(init.value)
|
||||||
|
.then((value) => this.destructureObject(id, value)),
|
||||||
|
yield: init.yield,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = {
|
||||||
|
value: this.destructureObject(id, init.value),
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'WhileStatement': {
|
||||||
|
const shouldVisitChild = (child) => (
|
||||||
|
isReplaying
|
||||||
|
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
if (this.$$execution.stack[this.$$execution.stack.length - 1] === undefined) {
|
||||||
|
this.$$execution.stack.pop();
|
||||||
|
}
|
||||||
|
result = this.$$execution.deferred.get(node.body) || {
|
||||||
|
value: undefined,
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
};
|
||||||
|
if (shouldVisitChild(node.test)) {
|
||||||
|
const test = this.executeSync(node.test, depth + 1);
|
||||||
|
if (test.yield) {
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
if (!test.value) {
|
||||||
|
this.$$execution.deferred.delete(node.body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldVisitChild(node.body)) {
|
||||||
|
const body = this.executeSync(node.body, depth + 1);
|
||||||
|
if (body.yield) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
this.$$execution.deferred.set(node.body, body);
|
||||||
|
}
|
||||||
|
// Yield
|
||||||
|
this.$$execution.stack.push(undefined);
|
||||||
|
return {
|
||||||
|
value: undefined,
|
||||||
|
yield: YIELD_LOOP_UPDATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.log(
|
||||||
|
node.type,
|
||||||
|
Object.keys(node)
|
||||||
|
.filter((key) => !['start', 'end'].includes(key)),
|
||||||
|
);
|
||||||
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
this.$$execution.stack.pop();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.generator = undefined;
|
this.stack = [];
|
||||||
for (const scope of this.scopes.values()) {
|
for (const scope of this.scopes.values()) {
|
||||||
scope.context = {};
|
scope.context = {};
|
||||||
}
|
}
|
||||||
|
@ -268,14 +485,39 @@ export default class Sandbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
step() {
|
||||||
if (!this.generator) {
|
if (!this.$$execution) {
|
||||||
this.generator = this.execute(this.ast);
|
this.$$execution = {
|
||||||
|
deferred: new Map(),
|
||||||
|
stack: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const result = this.generator.next();
|
let result = this.executeSync(this.ast, 0);
|
||||||
if (result.done) {
|
const stepResult = {async: false, done: false, value: undefined};
|
||||||
|
switch (result.yield) {
|
||||||
|
case YIELD_PROMISE: {
|
||||||
|
stepResult.async = true;
|
||||||
|
stepResult.value = Promise.resolve(result.value).then((value) => {
|
||||||
|
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
|
||||||
|
this.$$execution.deferred.set(top, {
|
||||||
|
value,
|
||||||
|
yield: YIELD_NONE,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case YIELD_LOOP_UPDATE: {
|
||||||
|
stepResult.value = result.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case YIELD_RETURN: {
|
||||||
|
stepResult.done = true;
|
||||||
|
stepResult.value = result.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stepResult.done) {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
return result;
|
return stepResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
159
app/astride/sandbox.perf.test.js
Normal file
159
app/astride/sandbox.perf.test.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import {parse as acornParse} from 'acorn';
|
||||||
|
import {expect, test} from 'vitest';
|
||||||
|
|
||||||
|
import Sandbox from '@/astride/sandbox.js';
|
||||||
|
|
||||||
|
function parse(code, options = {}) {
|
||||||
|
return acornParse(code, {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
// [
|
||||||
|
// `
|
||||||
|
// const {one, two: {three, four: {five, six: {seven}}}} = value;
|
||||||
|
// `,
|
||||||
|
// {
|
||||||
|
// value: {
|
||||||
|
// one: 1,
|
||||||
|
// two: {
|
||||||
|
// three: 3,
|
||||||
|
// four: {
|
||||||
|
// five: 5,
|
||||||
|
// six: {
|
||||||
|
// seven: 7,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// one: 1,
|
||||||
|
// three: 3,
|
||||||
|
// five: 5,
|
||||||
|
// seven: 7
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
const x = 123;
|
||||||
|
{
|
||||||
|
const x = 234;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
x: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
let x = [];
|
||||||
|
for (let i = 0; i < 100; ++i) {
|
||||||
|
x[i] = i;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
x: Array(100).fill(0).map((n, i) => i),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
let x = 0, y;
|
||||||
|
for (let i = 0; i < 4; ++i) {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
if (x % 2) {
|
||||||
|
y = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
y = true
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
y: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
let x = 0;
|
||||||
|
while (x < 100) {
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
x: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
let x = 0;
|
||||||
|
do {
|
||||||
|
x += 1;
|
||||||
|
} while (x < 100);
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
x: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
`
|
||||||
|
let x = 0;
|
||||||
|
do {
|
||||||
|
x += 1;
|
||||||
|
} while (x < 100);
|
||||||
|
`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
x: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
test('performs well', async () => {
|
||||||
|
let sandbox;
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
sandbox = new Sandbox(await parse(testCase[0]));
|
||||||
|
for (const key in testCase[1]) {
|
||||||
|
sandbox.context[key] = testCase[1][key];
|
||||||
|
}
|
||||||
|
sandbox.run();
|
||||||
|
expect(sandbox.context)
|
||||||
|
.to.deep.include(testCase[2]);
|
||||||
|
const N = 1000;
|
||||||
|
let last;
|
||||||
|
for (let i = 0; i < 100000 / N; ++i) {
|
||||||
|
sandbox.run();
|
||||||
|
}
|
||||||
|
last = performance.now();
|
||||||
|
for (let i = 0; i < N; ++i) {
|
||||||
|
sandbox.run();
|
||||||
|
}
|
||||||
|
const astrideSyncTime = (performance.now() - last) / N;
|
||||||
|
const native = new Function(
|
||||||
|
Object.keys(testCase[1]).map((arg) => `{${arg}}`).join(', '),
|
||||||
|
testCase[0],
|
||||||
|
);
|
||||||
|
for (let i = 0; i < 100000 / N; ++i) {
|
||||||
|
native(testCase[1]);
|
||||||
|
}
|
||||||
|
last = performance.now();
|
||||||
|
for (let i = 0; i < N; ++i) {
|
||||||
|
native(testCase[1]);
|
||||||
|
}
|
||||||
|
const nativeTime = (performance.now() - last) / N;
|
||||||
|
// console.log(
|
||||||
|
// testCase[0],
|
||||||
|
// `${Math.round(astrideSyncTime / nativeTime)}x slower`,
|
||||||
|
// );
|
||||||
|
expect(astrideSyncTime)
|
||||||
|
.to.be.lessThan(nativeTime * 500);
|
||||||
|
}
|
||||||
|
});
|
|
@ -11,6 +11,18 @@ function parse(code, options = {}) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function finish(sandbox) {
|
||||||
|
let result;
|
||||||
|
let i = 0;
|
||||||
|
do {
|
||||||
|
result = sandbox.step();
|
||||||
|
if (result.async) {
|
||||||
|
await result.value;
|
||||||
|
}
|
||||||
|
} while (!result.done && ++i < 1000);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
test('declares variables', async () => {
|
test('declares variables', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
|
@ -22,13 +34,7 @@ test('declares variables', async () => {
|
||||||
const asyncObject = await {11: '12'};
|
const asyncObject = await {11: '12'};
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.async;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
scalar: 1,
|
scalar: 1,
|
||||||
|
@ -53,17 +59,25 @@ test('scopes variables', async () => {
|
||||||
result.push(scalar);
|
result.push(scalar);
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.async;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context.result)
|
expect(sandbox.context.result)
|
||||||
.to.deep.equal([1, 2, 1]);
|
.to.deep.equal([1, 2, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns last', async () => {
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
foo = 32;
|
||||||
|
{
|
||||||
|
bar = 64;
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
;
|
||||||
|
expect(await finish(sandbox))
|
||||||
|
.to.deep.include({done: true, value: 64});
|
||||||
|
});
|
||||||
|
|
||||||
test('destructures variables', async () => {
|
test('destructures variables', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
|
@ -73,13 +87,7 @@ test('destructures variables', async () => {
|
||||||
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
a: 1,
|
a: 1,
|
||||||
|
@ -98,19 +106,17 @@ test('runs arbitrary number of ops', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
const foo = [];
|
const foo = [];
|
||||||
for (let i = 0; i < 1500; ++i) {
|
for (let i = 0; i < 150; ++i) {
|
||||||
foo.push(i);
|
foo.push(i);
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
sandbox.run(1000);
|
sandbox.run(100);
|
||||||
expect(sandbox.context.foo.length)
|
expect(sandbox.context.foo.length)
|
||||||
.to.equal(1000);
|
.to.equal(100);
|
||||||
sandbox.run(1000);
|
sandbox.run(100);
|
||||||
expect(sandbox.context.foo.length)
|
expect(sandbox.context.foo.length)
|
||||||
.to.equal(1500);
|
.to.equal(150);
|
||||||
expect(true)
|
|
||||||
.to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('evaluates conditional branches', async () => {
|
test('evaluates conditional branches', async () => {
|
||||||
|
@ -131,13 +137,7 @@ test('evaluates conditional branches', async () => {
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
foo: 1,
|
foo: 1,
|
||||||
|
@ -148,7 +148,11 @@ test('evaluates conditional branches', async () => {
|
||||||
test('evaluates loops', async () => {
|
test('evaluates loops', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
let x = 0, y = 0, a = 0, b = 0, c = 0;
|
x = 0
|
||||||
|
y = 0
|
||||||
|
a = 0
|
||||||
|
b = 0
|
||||||
|
c = 0
|
||||||
for (let i = 0; i < 3; ++i) {
|
for (let i = 0; i < 3; ++i) {
|
||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
|
@ -166,13 +170,7 @@ test('evaluates loops', async () => {
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
a: 3,
|
a: 3,
|
||||||
|
@ -207,7 +205,7 @@ test('returns values at the top level', async () => {
|
||||||
y = 8
|
y = 8
|
||||||
`, {allowReturnOutsideFunction: true}),
|
`, {allowReturnOutsideFunction: true}),
|
||||||
);
|
);
|
||||||
const {value: {value}} = sandbox.run();
|
const {value} = sandbox.run();
|
||||||
expect(value)
|
expect(value)
|
||||||
.to.deep.equal([48, 12]);
|
.to.deep.equal([48, 12]);
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
|
@ -257,3 +255,47 @@ test('runs arbitrary number of ops', async () => {
|
||||||
expect(true)
|
expect(true)
|
||||||
.to.be.true;
|
.to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('clears for loop deferred context', async () => {
|
||||||
|
const context = {x: 10};
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
let l = 0
|
||||||
|
for (let i = 0; i < x; ++i) {
|
||||||
|
l += 1
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({
|
||||||
|
value: 10,
|
||||||
|
});
|
||||||
|
context.x = 0
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears while loop deferred context', async () => {
|
||||||
|
const context = {x: 10};
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
let l = 0
|
||||||
|
while (l < x) {
|
||||||
|
l += 1
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({
|
||||||
|
value: 10,
|
||||||
|
});
|
||||||
|
context.x = 0
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default class Scope {
|
||||||
}
|
}
|
||||||
|
|
||||||
allocate(key, value) {
|
allocate(key, value) {
|
||||||
this.context[key] = value;
|
return this.context[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user