silphius/app/astride/sandbox.js
2024-08-01 13:00:21 -05:00

737 lines
21 KiB
JavaScript

import evaluate from '@/astride/evaluate.js';
import Scope from '@/astride/scope.js';
import traverse from '@/astride/traverse.js';
const YIELD_NONE = 0;
const YIELD_PROMISE = 1;
const YIELD_LOOP_UPDATE = 2;
const YIELD_RETURN = 3;
const YIELD_BREAK = 4;
export default class Sandbox {
ast;
$$execution;
scopes;
constructor(ast, context = {}) {
this.ast = ast;
this.$$context = context;
this.compile();
}
clone() {
return new this.constructor(
this.ast,
{
...this.$$context,
}
);
}
compile() {
let scope = new Scope();
scope.context = this.$$context;
this.scopes = new Map([[this.ast, scope]]);
traverse(
this.ast,
(node, verb) => {
if (
'BlockStatement' === node.type
|| 'ForStatement' === node.type
|| 'ForOfStatement' === node.type
) {
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;
}
switch (element.type) {
case 'ArrayPattern': {
this.destructureArray(element, init[i]);
break;
}
case 'ObjectPattern': {
this.destructureObject(element, init[i]);
break;
}
case 'Identifier': {
scope.allocate(element.name, init[i]);
break;
}
/* v8 ignore next 2 */
default:
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
}
}
return init;
}
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 ('ObjectPattern' === property.value.type) {
this.destructureObject(property.value, init[property.key.name]);
}
else {
scope.allocate(
property.value.name,
init[property.key.name],
);
}
}
return init;
}
evaluate(node) {
return evaluate(
node,
{scope: this.scopes.get(node)},
);
}
evaluateToResult(node) {
const {async, value} = this.evaluate(node);
return {
value,
yield: async ? YIELD_PROMISE : YIELD_NONE,
};
}
executeSync(node, depth) {
let result;
const isReplaying = depth < this.$$execution.stack.length;
const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
if (!isReplaying) {
this.$$execution.stack.push(node);
}
// Substitute executing the node for its deferred result.
else if (isReplayingThisLevel) {
if (this.$$execution.stack[depth] === node) {
result = this.$$execution.deferred.get(node);
this.$$execution.deferred.delete(node);
this.$$execution.stack.pop();
return result;
}
}
switch (node.type) {
case 'ArrayExpression':
case 'AssignmentExpression':
case 'BinaryExpression':
case 'CallExpression':
case 'ChainExpression':
case 'ObjectExpression':
case 'Identifier':
case 'MemberExpression':
case 'NewExpression':
case 'UpdateExpression': {
result = this.evaluateToResult(node);
if (result.yield) {
return result;
}
break;
}
case 'AwaitExpression': {
const coerce = !this.$$execution.deferred.has(node.argument);
result = this.executeSync(node.argument, depth + 1);
if (coerce) {
result = {
value: result.value,
yield: YIELD_PROMISE,
};
}
if (result.yield) {
return result;
}
/* v8 ignore next 2 */
break;
}
case 'LogicalExpression': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (shouldVisitChild(node.left)) {
const left = this.executeSync(node.left, depth + 1);
if (left.yield) {
return left;
}
this.$$execution.deferred.set(node.left, left);
}
const left = this.$$execution.deferred.get(node.left);
this.$$execution.deferred.delete(node.left);
if ('||' === node.operator && left.value) {
result = {
value: true,
yield: YIELD_NONE,
};
break;
}
if ('&&' === node.operator && !left.value) {
result = {
value: false,
yield: YIELD_NONE,
};
break;
}
const right = this.executeSync(node.right, depth + 1);
if (right.yield) {
return right;
}
result = {
value: !!right.value,
yield: YIELD_NONE,
};
break;
}
case 'BlockStatement': {
result = {
value: undefined,
yield: YIELD_NONE,
};
let skipping = isReplaying;
for (const child of node.body) {
if (skipping && child === this.$$execution.stack[depth + 1]) {
skipping = false;
}
/* v8 ignore next 3 */
if (skipping) {
continue;
}
result = this.executeSync(child, depth + 1);
if (result.yield) {
return result;
}
}
break;
}
case 'BreakStatement': {
walkUp: while (this.$$execution.stack.length > 0) {
const frame = this.$$execution.stack[this.$$execution.stack.length - 1];
switch (frame.type) {
case 'ForStatement': {
break walkUp;
}
case 'ForOfStatement': {
break walkUp;
}
default: this.$$execution.stack.pop();
}
}
return {
value: undefined,
yield: YIELD_BREAK,
};
}
case 'ConditionalExpression': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
}
const test = this.$$execution.deferred.get(node.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[depth + 1] === node) {
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(node);
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[depth + 1] === node) {
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) {
if (YIELD_BREAK === body.yield) {
this.$$execution.deferred.delete(node.body);
break;
}
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(node);
const update = this.$$execution.deferred.get(node.update);
this.$$execution.deferred.delete(node.update);
return {
value: update.value,
yield: YIELD_LOOP_UPDATE,
};
}
case 'ForOfStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
const scope = this.scopes.get(node);
if (shouldVisitChild(node.right)) {
const right = this.executeSync(node.right, depth + 1);
if (right.yield) {
return right;
}
scope.allocate('@@iterator', right.value[Symbol.iterator]());
}
result = this.$$execution.deferred.get(node.body) || {
value: undefined,
yield: YIELD_NONE,
};
if (shouldVisitChild(node)) {
this.$$execution.deferred.set(node.left, scope.get('@@iterator').next());
}
if (this.$$execution.stack[depth + 1] === node) {
this.$$execution.stack.pop();
}
const {done, value} = this.$$execution.deferred.get(node.left);
if (done) {
this.$$execution.deferred.delete(node.left);
this.$$execution.deferred.delete(node.body);
break;
}
switch (node.left.type) {
case 'ArrayPattern': {
this.destructureArray(node.left, value)
break;
}
case 'ObjectPattern': {
this.destructureObject(node.left, value)
break;
}
case 'VariableDeclaration': {
const [declaration] = node.left.declarations;
switch (declaration.id.type) {
case 'Identifier': {
scope.set(declaration.id.name, value);
break;
}
case 'ArrayPattern': {
this.destructureArray(declaration.id, value)
break;
}
case 'ObjectPattern': {
this.destructureObject(declaration.id, value)
break;
}
}
break;
}
case 'Identifier': {
scope.set(node.left.name, value);
break;
}
}
if (shouldVisitChild(node.body)) {
const body = this.executeSync(node.body, depth + 1);
if (body.yield) {
if (YIELD_BREAK === body.yield) {
this.$$execution.deferred.delete(node.left);
this.$$execution.deferred.delete(node.body);
break;
}
return body;
}
this.$$execution.deferred.set(node.body, body);
}
// Yield
this.$$execution.stack.push(node);
return {
value: undefined,
yield: YIELD_LOOP_UPDATE,
};
}
case 'IfStatement': {
const shouldVisitChild = (child) => (
isReplaying
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
: true
);
result = {
value: undefined,
yield: YIELD_NONE,
};
if (shouldVisitChild(node.test)) {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
this.$$execution.deferred.set(node.test, test);
}
const test = this.$$execution.deferred.get(node.test);
if (test.value) {
result = this.executeSync(node.consequent, depth + 1);
}
else if (node.alternate) {
result = this.executeSync(node.alternate, depth + 1);
}
if (result.yield) {
return result;
}
this.$$execution.deferred.delete(node.test);
break;
}
case 'Literal': {
result = {value: node.value, yield: YIELD_NONE};
break;
}
case 'Program': {
result = {value: undefined, yield: YIELD_NONE};
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 = {value: result.value, yield: YIELD_RETURN};
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;
}
result = {
value: argument.value,
yield: YIELD_RETURN,
};
break;
}
case 'UnaryExpression': {
if ('delete' === node.operator) {
let property;
if (node.argument.computed) {
property = this.executeSync(node.argument.property, depth + 1);
if (property.yield) {
return property;
}
}
else {
property = {value: node.argument.property.name, yield: YIELD_NONE};
}
const scope = this.scopes.get(node);
const object = scope.get(node.argument.object.name, undefined);
delete object[property.value];
result = {value: true, yield: YIELD_NONE};
}
else {
result = this.evaluateToResult(node);
if (result.yield) {
return result;
}
}
break;
}
case 'VariableDeclaration': {
let skipping = isReplaying;
for (const child of node.declarations) {
if (skipping && child === this.$$execution.stack[depth + 1]) {
skipping = false;
}
/* v8 ignore next 3 */
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 ('Identifier' === id.type) {
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 ('ArrayPattern' === id.type) {
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 ('ObjectPattern' === id.type) {
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[depth + 1] === node) {
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(node);
return {
value: undefined,
yield: YIELD_LOOP_UPDATE,
};
}
/* v8 ignore next 7 */
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() {
this.$$execution = undefined;
for (const scope of this.scopes.values()) {
scope.context = {};
}
this.scopes.get(this.ast).context = this.$$context;
}
run(ops = 1000) {
let result;
for (let i = 0; i < ops; ++i) {
result = this.step();
if (result.done || result.async) {
break;
}
}
return result;
}
step() {
if (!this.$$execution) {
this.$$execution = {
deferred: new Map(),
stack: [],
};
}
const result = this.executeSync(this.ast, 0);
const stepResult = {async: false, done: false, value: undefined};
switch (result.yield) {
case YIELD_PROMISE: {
stepResult.async = true;
const promise = result.value instanceof Promise
? result.value
: Promise.resolve(result.value);
promise
.then((value) => {
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
this.$$execution.deferred.set(top, {
value,
yield: YIELD_NONE,
});
});
stepResult.value = promise;
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();
}
return stepResult;
}
}