silphius/app/astride/sandbox.js
2024-06-27 13:57:02 -05:00

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;
}
}