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; 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 ) { 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 ('Identifier' === element.type) { scope.allocate(element.name, init[i]); } /* v8 ignore next 3 */ else { 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 'LogicalExpression': case 'MemberExpression': 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 = { value: result.value, yield: YIELD_PROMISE, }; } if (result.yield) { return result; } /* v8 ignore next 2 */ 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 '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) { 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 '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 '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: [], }; } 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) { this.reset(); } return stepResult; } }