silphius/app/astride/sandbox.js

528 lines
14 KiB
JavaScript
Raw Normal View History

import evaluate from '@/astride/evaluate.js';
import Scope from '@/astride/scope.js';
2024-06-30 10:03:50 -05:00
import traverse from '@/astride/traverse.js';
2024-06-16 08:01:01 -05:00
import {
isArrayPattern,
isBlockStatement,
isForStatement,
isIdentifier,
isObjectPattern,
} from '@/astride/types.js';
2024-06-16 08:01:01 -05:00
2024-06-30 10:03:50 -05:00
const YIELD_NONE = 0;
const YIELD_PROMISE = 1;
const YIELD_LOOP_UPDATE = 2;
const YIELD_RETURN = 3;
2024-06-16 08:01:01 -05:00
export default class Sandbox {
ast;
2024-06-30 10:03:50 -05:00
$$execution;
2024-06-16 08:01:01 -05:00
scopes;
constructor(ast, context = {}) {
this.ast = ast;
this.$$context = context;
this.compile();
}
compile() {
let scope = new Scope();
scope.context = this.$$context;
2024-06-29 12:35:54 -05:00
this.scopes = new Map([[this.ast, scope]]);
2024-06-16 08:01:01 -05:00
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() {
2024-06-22 04:05:09 -05:00
return this.$$context;
2024-06-16 08:01:01 -05:00
}
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)) {
2024-06-22 10:47:17 -05:00
scope.allocate(element.name, init[i]);
2024-06-16 08:01:01 -05:00
}
/* v8 ignore next 3 */
else {
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
}
}
2024-06-30 10:03:50 -05:00
return init;
2024-06-16 08:01:01 -05:00
}
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)) {
2024-06-30 10:03:50 -05:00
this.destructureObject(property.value, init[property.key.name]);
2024-06-16 08:01:01 -05:00
}
else {
scope.allocate(
2024-06-22 10:47:17 -05:00
property.value.name,
init[property.key.name],
2024-06-16 08:01:01 -05:00
);
}
}
2024-06-30 10:03:50 -05:00
return init;
2024-06-16 08:01:01 -05:00
}
evaluate(node) {
return evaluate(
node,
{scope: this.scopes.get(node)},
);
}
2024-06-30 10:03:50 -05:00
evaluateToResult(node) {
const {async, value} = this.evaluate(node);
return {
yield: async ? YIELD_PROMISE : YIELD_NONE,
value,
};
}
executeSync(node, depth) {
let result;
const isReplaying = depth < this.$$execution.stack.length;
const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
// console.log(
// Array(depth).fill(' ').join(''),
// node.type,
// isReplaying
// ? (['replaying', ...(isReplayingThisLevel ? ['this level'] : [])].join(' '))
// : '',
// );
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;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
}
switch (node.type) {
case 'ArrayExpression':
case 'AssignmentExpression':
case 'BinaryExpression':
case 'CallExpression':
case 'ObjectExpression':
case 'Identifier':
case 'UnaryExpression':
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 = {
yield: YIELD_PROMISE,
value: result.value,
};
}
if (result.yield) {
return result;
}
this.$$execution.deferred.delete(node.argument);
break;
}
case 'BlockStatement': {
let skipping = isReplaying;
for (const child of node.body) {
if (skipping && child === this.$$execution.stack[depth + 1]) {
skipping = false;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
if (skipping) {
continue;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
result = this.executeSync(child, depth + 1);
if (result.yield) {
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;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
this.$$execution.deferred.set(node.body, body);
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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;
2024-06-16 08:01:01 -05:00
}
}
2024-06-30 10:03:50 -05:00
// Yield
this.$$execution.stack.push(undefined);
return {
value: undefined,
yield: YIELD_LOOP_UPDATE,
2024-06-16 08:01:01 -05:00
};
}
2024-06-30 10:03:50 -05:00
case 'ExpressionStatement': {
result = this.executeSync(node.expression, depth + 1);
if (result.yield) {
return result;
}
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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,
};
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
case 'IfStatement': {
const test = this.executeSync(node.test, depth + 1);
if (test.yield) {
return test;
}
2024-06-30 11:27:42 -05:00
result = {
yield: YIELD_NONE,
value: undefined,
};
2024-06-30 10:03:50 -05:00
if (test.value) {
result = this.executeSync(node.consequent, depth + 1);
2024-06-16 08:01:01 -05:00
}
2024-06-30 11:27:42 -05:00
else if (node.alternate) {
2024-06-30 10:03:50 -05:00
result = this.executeSync(node.alternate, depth + 1);
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
if (result.yield) {
return result;
}
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
case 'Literal': {
result = {value: node.value, yield: YIELD_NONE};
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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;
}
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
result = {yield: YIELD_RETURN, value: result.value};
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
case 'ReturnStatement': {
if (!node.argument) {
return {
value: undefined,
yield: YIELD_RETURN,
};
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
const argument = this.executeSync(node.argument, depth + 1);
if (argument.yield) {
return argument;
2024-06-18 22:29:40 -05:00
}
2024-06-30 10:03:50 -05:00
return {
value: argument.value,
yield: YIELD_RETURN,
};
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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;
}
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
result = {value: undefined, yield: YIELD_NONE};
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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};
2024-06-16 08:01:01 -05:00
}
else {
2024-06-30 10:03:50 -05:00
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,
};
}
}
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
break;
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
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');
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
this.$$execution.stack.pop();
return result;
2024-06-16 08:01:01 -05:00
}
2024-06-27 13:56:43 -05:00
reset() {
2024-06-30 10:03:50 -05:00
this.stack = [];
2024-06-29 12:35:54 -05:00
for (const scope of this.scopes.values()) {
scope.context = {};
}
this.scopes.get(this.ast).context = this.$$context;
2024-06-27 13:56:43 -05:00
}
2024-06-16 08:01:01 -05:00
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() {
2024-06-30 10:03:50 -05:00
if (!this.$$execution) {
this.$$execution = {
deferred: new Map(),
stack: [],
};
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
let result = this.executeSync(this.ast, 0);
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) {
2024-06-27 13:56:43 -05:00
this.reset();
2024-06-16 08:01:01 -05:00
}
2024-06-30 10:03:50 -05:00
return stepResult;
2024-06-16 08:01:01 -05:00
}
}