641 lines
18 KiB
JavaScript
641 lines
18 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;
|
|
|
|
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 '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 '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.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) {
|
|
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 '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;
|
|
}
|
|
|
|
}
|