279 lines
6.6 KiB
JavaScript
279 lines
6.6 KiB
JavaScript
import evaluate from '@/astride/evaluate.js';
|
|
import Scope from '@/astride/scope.js';
|
|
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js';
|
|
import {
|
|
isArrayPattern,
|
|
isBlockStatement,
|
|
isDoWhileStatement,
|
|
isExpressionStatement,
|
|
isForStatement,
|
|
isIdentifier,
|
|
isIfStatement,
|
|
isObjectPattern,
|
|
isReturnStatement,
|
|
isVariableDeclarator,
|
|
isWhileStatement,
|
|
} from '@/astride/types.js';
|
|
|
|
export default class Sandbox {
|
|
|
|
ast;
|
|
generator;
|
|
scopes;
|
|
|
|
constructor(ast, context = {}) {
|
|
this.ast = ast;
|
|
this.$$context = context;
|
|
this.compile();
|
|
}
|
|
|
|
compile() {
|
|
let scope = new Scope();
|
|
scope.context = this.$$context;
|
|
this.scopes = new WeakMap([[this.ast, scope]]);
|
|
traverse(
|
|
this.ast,
|
|
(node, verb) => {
|
|
if (
|
|
isBlockStatement(node)
|
|
|| isForStatement(node)
|
|
) {
|
|
switch (verb) {
|
|
case 'enter': {
|
|
scope = new Scope(scope);
|
|
break;
|
|
}
|
|
case 'exit': {
|
|
scope = scope.parent;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ('enter' === verb) {
|
|
this.scopes.set(node, scope);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
get context() {
|
|
return this.$$context;
|
|
}
|
|
|
|
destructureArray(id, init) {
|
|
const scope = this.scopes.get(id);
|
|
const {elements} = id;
|
|
for (let i = 0; i < elements.length; ++i) {
|
|
const element = elements[i];
|
|
if (null === element) {
|
|
continue;
|
|
}
|
|
if (isIdentifier(element)) {
|
|
scope.allocate(element.name, init[i]);
|
|
}
|
|
/* v8 ignore next 3 */
|
|
else {
|
|
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
destructureObject(id, init) {
|
|
const scope = this.scopes.get(id);
|
|
const {properties} = id;
|
|
for (let i = 0; i < properties.length; ++i) {
|
|
const property = properties[i];
|
|
if (isObjectPattern(property.value)) {
|
|
this.destructureObject(property.value, init[property.key.name], scope);
|
|
}
|
|
else {
|
|
scope.allocate(
|
|
property.value.name,
|
|
init[property.key.name],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
evaluate(node) {
|
|
return evaluate(
|
|
node,
|
|
{scope: this.scopes.get(node)},
|
|
);
|
|
}
|
|
|
|
*execute(node, parent) {
|
|
let keys = TRAVERSAL_PATH[node.type];
|
|
if (isVariableDeclarator(node)) {
|
|
const {id} = node;
|
|
const scope = this.scopes.get(node);
|
|
if (null === node.init) {
|
|
scope.allocate(id.name, undefined);
|
|
}
|
|
else {
|
|
const init = this.evaluate(node.init);
|
|
if (isIdentifier(id)) {
|
|
if (init.async) {
|
|
yield {
|
|
async: true,
|
|
value: Promise.resolve(init.value).then((value) => {
|
|
scope.allocate(id.name, value);
|
|
}),
|
|
};
|
|
}
|
|
else {
|
|
scope.allocate(id.name, init.value);
|
|
}
|
|
}
|
|
else if (isArrayPattern(id)) {
|
|
const promiseOrVoid = init.async
|
|
? Promise.resolve(init.value).then((init) => this.destructureArray(id, init))
|
|
: this.destructureArray(id, init.value);
|
|
if (promiseOrVoid) {
|
|
yield {
|
|
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...
|
|
if (isIfStatement(node)) {
|
|
const {async, value} = this.evaluate(node.test);
|
|
const branch = (value) => {
|
|
keys = [value ? 'consequent' : 'alternate'];
|
|
};
|
|
if (async) {
|
|
yield {
|
|
async: true,
|
|
value: Promise.resolve(value).then(branch),
|
|
};
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
// Loops...
|
|
if (isForStatement(node)) {
|
|
const result = this.execute(node.update, node).next();
|
|
if (result.value?.async) {
|
|
yield 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 {
|
|
loop = test.value;
|
|
}
|
|
}
|
|
} while (loop);
|
|
// Top-level return.
|
|
if (isReturnStatement(node)) {
|
|
return !node.argument ? {value: undefined} : this.evaluate(node.argument);
|
|
}
|
|
if (isExpressionStatement(node)) {
|
|
yield this.evaluate(node.expression);
|
|
}
|
|
// yield ForStatement afterthought.
|
|
if (isForStatement(parent) && node === parent.update) {
|
|
yield this.evaluate(node);
|
|
/* v8 ignore next */
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.generator = undefined;
|
|
this.compile();
|
|
}
|
|
|
|
run(ops = 1000) {
|
|
let result;
|
|
for (let i = 0; i < ops; ++i) {
|
|
result = this.step();
|
|
if (result.done || result.value?.async) {
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
step() {
|
|
if (!this.generator) {
|
|
this.generator = this.execute(this.ast);
|
|
}
|
|
const result = this.generator.next();
|
|
if (result.done) {
|
|
this.reset();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
}
|