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