refactor!: sandbox
This commit is contained in:
parent
31722ab4a0
commit
398dd66594
|
@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) {
|
|||
.then(([left, right]) => binary(left, right)),
|
||||
};
|
||||
}
|
||||
return {value: binary(left.value, right.value)};
|
||||
return {
|
||||
async: false,
|
||||
value: binary(left.value, right.value),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,5 +49,8 @@ export default function(node, {evaluate, scope}) {
|
|||
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
|
||||
};
|
||||
}
|
||||
return {value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args)};
|
||||
return {
|
||||
async: false,
|
||||
value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function(node, {scope}) {
|
||||
return {value: scope.get(node.name)};
|
||||
return {async: false, value: scope.get(node.name)};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export default function({value}) {
|
||||
return {value};
|
||||
return {async: false, value};
|
||||
}
|
||||
|
|
|
@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) {
|
|||
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
|
||||
};
|
||||
}
|
||||
return {value: member(O.value, P.value)};
|
||||
return {async: false, value: member(O.value, P.value)};
|
||||
}
|
||||
|
|
|
@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) {
|
|||
value: Promise.resolve(arg.value).then(unary),
|
||||
};
|
||||
}
|
||||
return {value: unary(arg.value)};
|
||||
return {async: false, value: unary(arg.value)};
|
||||
}
|
||||
|
|
|
@ -19,5 +19,5 @@ export default function(node, {evaluate, scope}) {
|
|||
/* v8 ignore next */
|
||||
throw new Error(`operator not implemented: ${operator}`);
|
||||
};
|
||||
return {value: update(value)};
|
||||
return {async: false, value: update(value)};
|
||||
}
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import evaluate from '@/astride/evaluate.js';
|
||||
import Scope from '@/astride/scope.js';
|
||||
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js';
|
||||
import traverse from '@/astride/traverse.js';
|
||||
import {
|
||||
isArrayPattern,
|
||||
isBlockStatement,
|
||||
isDoWhileStatement,
|
||||
isExpressionStatement,
|
||||
isForStatement,
|
||||
isIdentifier,
|
||||
isIfStatement,
|
||||
isObjectPattern,
|
||||
isReturnStatement,
|
||||
isVariableDeclarator,
|
||||
isWhileStatement,
|
||||
} from '@/astride/types.js';
|
||||
|
||||
const YIELD_NONE = 0;
|
||||
const YIELD_PROMISE = 1;
|
||||
const YIELD_LOOP_UPDATE = 2;
|
||||
const YIELD_RETURN = 3;
|
||||
|
||||
export default class Sandbox {
|
||||
|
||||
ast;
|
||||
generator;
|
||||
$$execution;
|
||||
scopes;
|
||||
|
||||
constructor(ast, context = {}) {
|
||||
|
@ -76,7 +75,7 @@ export default class Sandbox {
|
|||
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return init;
|
||||
}
|
||||
|
||||
destructureObject(id, init) {
|
||||
|
@ -85,7 +84,7 @@ export default class Sandbox {
|
|||
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);
|
||||
this.destructureObject(property.value, init[property.key.name]);
|
||||
}
|
||||
else {
|
||||
scope.allocate(
|
||||
|
@ -94,6 +93,7 @@ export default class Sandbox {
|
|||
);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
evaluate(node) {
|
||||
|
@ -103,153 +103,370 @@ export default class Sandbox {
|
|||
);
|
||||
}
|
||||
|
||||
*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,
|
||||
};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
}),
|
||||
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,
|
||||
};
|
||||
}
|
||||
else {
|
||||
loop = test.value;
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
this.$$execution.deferred.delete(node.argument);
|
||||
break;
|
||||
}
|
||||
} 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 */
|
||||
case 'BlockStatement': {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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(undefined);
|
||||
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[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,
|
||||
};
|
||||
}
|
||||
case 'IfStatement': {
|
||||
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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Literal': {
|
||||
result = {value: node.value, yield: YIELD_NONE};
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
result = {yield: YIELD_RETURN, value: result.value};
|
||||
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;
|
||||
}
|
||||
return {
|
||||
value: argument.value,
|
||||
yield: YIELD_RETURN,
|
||||
};
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 (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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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');
|
||||
}
|
||||
this.$$execution.stack.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.generator = undefined;
|
||||
this.stack = [];
|
||||
for (const scope of this.scopes.values()) {
|
||||
scope.context = {};
|
||||
}
|
||||
|
@ -268,14 +485,39 @@ export default class Sandbox {
|
|||
}
|
||||
|
||||
step() {
|
||||
if (!this.generator) {
|
||||
this.generator = this.execute(this.ast);
|
||||
if (!this.$$execution) {
|
||||
this.$$execution = {
|
||||
deferred: new Map(),
|
||||
stack: [],
|
||||
};
|
||||
}
|
||||
const result = this.generator.next();
|
||||
if (result.done) {
|
||||
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 result;
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
159
app/astride/sandbox.perf.test.js
Normal file
159
app/astride/sandbox.perf.test.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import {parse as acornParse} from 'acorn';
|
||||
import {expect, test} from 'vitest';
|
||||
|
||||
import Sandbox from '@/astride/sandbox.js';
|
||||
|
||||
function parse(code, options = {}) {
|
||||
return acornParse(code, {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
const testCases = [
|
||||
// [
|
||||
// `
|
||||
// const {one, two: {three, four: {five, six: {seven}}}} = value;
|
||||
// `,
|
||||
// {
|
||||
// value: {
|
||||
// one: 1,
|
||||
// two: {
|
||||
// three: 3,
|
||||
// four: {
|
||||
// five: 5,
|
||||
// six: {
|
||||
// seven: 7,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// one: 1,
|
||||
// three: 3,
|
||||
// five: 5,
|
||||
// seven: 7
|
||||
// },
|
||||
// ],
|
||||
[
|
||||
`
|
||||
const x = 123;
|
||||
{
|
||||
const x = 234;
|
||||
}
|
||||
`,
|
||||
{},
|
||||
{
|
||||
x: 123,
|
||||
},
|
||||
],
|
||||
[
|
||||
`
|
||||
let x = [];
|
||||
for (let i = 0; i < 100; ++i) {
|
||||
x[i] = i;
|
||||
}
|
||||
`,
|
||||
{},
|
||||
{
|
||||
x: Array(100).fill(0).map((n, i) => i),
|
||||
},
|
||||
],
|
||||
[
|
||||
`
|
||||
let x = 0, y;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
x += 1;
|
||||
}
|
||||
if (x % 2) {
|
||||
y = false
|
||||
}
|
||||
else {
|
||||
y = true
|
||||
}
|
||||
`,
|
||||
{},
|
||||
{
|
||||
y: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
`
|
||||
let x = 0;
|
||||
while (x < 100) {
|
||||
x += 1;
|
||||
}
|
||||
`,
|
||||
{},
|
||||
{
|
||||
x: 100,
|
||||
},
|
||||
],
|
||||
[
|
||||
`
|
||||
let x = 0;
|
||||
do {
|
||||
x += 1;
|
||||
} while (x < 100);
|
||||
`,
|
||||
{},
|
||||
{
|
||||
x: 100,
|
||||
},
|
||||
],
|
||||
[
|
||||
`
|
||||
let x = 0;
|
||||
do {
|
||||
x += 1;
|
||||
} while (x < 100);
|
||||
`,
|
||||
{},
|
||||
{
|
||||
x: 100,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
test('performs well', async () => {
|
||||
let sandbox;
|
||||
for (const testCase of testCases) {
|
||||
sandbox = new Sandbox(await parse(testCase[0]));
|
||||
for (const key in testCase[1]) {
|
||||
sandbox.context[key] = testCase[1][key];
|
||||
}
|
||||
sandbox.run();
|
||||
expect(sandbox.context)
|
||||
.to.deep.include(testCase[2]);
|
||||
const N = 1000;
|
||||
let last;
|
||||
for (let i = 0; i < 100000 / N; ++i) {
|
||||
sandbox.run();
|
||||
}
|
||||
last = performance.now();
|
||||
for (let i = 0; i < N; ++i) {
|
||||
sandbox.run();
|
||||
}
|
||||
const astrideSyncTime = (performance.now() - last) / N;
|
||||
const native = new Function(
|
||||
Object.keys(testCase[1]).map((arg) => `{${arg}}`).join(', '),
|
||||
testCase[0],
|
||||
);
|
||||
for (let i = 0; i < 100000 / N; ++i) {
|
||||
native(testCase[1]);
|
||||
}
|
||||
last = performance.now();
|
||||
for (let i = 0; i < N; ++i) {
|
||||
native(testCase[1]);
|
||||
}
|
||||
const nativeTime = (performance.now() - last) / N;
|
||||
// console.log(
|
||||
// testCase[0],
|
||||
// `${Math.round(astrideSyncTime / nativeTime)}x slower`,
|
||||
// );
|
||||
expect(astrideSyncTime)
|
||||
.to.be.lessThan(nativeTime * 500);
|
||||
}
|
||||
});
|
|
@ -11,6 +11,18 @@ function parse(code, options = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
async function finish(sandbox) {
|
||||
let result;
|
||||
let i = 0;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.async) {
|
||||
await result.value;
|
||||
}
|
||||
} while (!result.done && ++i < 1000);
|
||||
return result;
|
||||
}
|
||||
|
||||
test('declares variables', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
|
@ -22,13 +34,7 @@ test('declares variables', async () => {
|
|||
const asyncObject = await {11: '12'};
|
||||
`),
|
||||
);
|
||||
let result;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.value?.async) {
|
||||
await result.value.async;
|
||||
}
|
||||
} while (!result.done);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
scalar: 1,
|
||||
|
@ -53,17 +59,25 @@ test('scopes variables', async () => {
|
|||
result.push(scalar);
|
||||
`),
|
||||
);
|
||||
let result;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.value?.async) {
|
||||
await result.value.async;
|
||||
}
|
||||
} while (!result.done);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context.result)
|
||||
.to.deep.equal([1, 2, 1]);
|
||||
});
|
||||
|
||||
test('returns last', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
foo = 32;
|
||||
{
|
||||
bar = 64;
|
||||
}
|
||||
`),
|
||||
);
|
||||
;
|
||||
expect(await finish(sandbox))
|
||||
.to.deep.include({done: true, value: 64});
|
||||
});
|
||||
|
||||
test('destructures variables', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
|
@ -73,13 +87,7 @@ test('destructures variables', async () => {
|
|||
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
||||
`),
|
||||
);
|
||||
let result;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.value?.async) {
|
||||
await result.value.value;
|
||||
}
|
||||
} while (!result.done);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
a: 1,
|
||||
|
@ -98,19 +106,17 @@ test('runs arbitrary number of ops', async () => {
|
|||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const foo = [];
|
||||
for (let i = 0; i < 1500; ++i) {
|
||||
for (let i = 0; i < 150; ++i) {
|
||||
foo.push(i);
|
||||
}
|
||||
`),
|
||||
);
|
||||
sandbox.run(1000);
|
||||
sandbox.run(100);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(1000);
|
||||
sandbox.run(1000);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(1500);
|
||||
expect(true)
|
||||
.to.be.true;
|
||||
.to.equal(100);
|
||||
sandbox.run(100);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(150);
|
||||
});
|
||||
|
||||
test('evaluates conditional branches', async () => {
|
||||
|
@ -131,13 +137,7 @@ test('evaluates conditional branches', async () => {
|
|||
}
|
||||
`),
|
||||
);
|
||||
let result;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.value?.async) {
|
||||
await result.value.value;
|
||||
}
|
||||
} while (!result.done);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
foo: 1,
|
||||
|
@ -148,7 +148,11 @@ test('evaluates conditional branches', async () => {
|
|||
test('evaluates loops', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
let x = 0, y = 0, a = 0, b = 0, c = 0;
|
||||
x = 0
|
||||
y = 0
|
||||
a = 0
|
||||
b = 0
|
||||
c = 0
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
x += 1;
|
||||
}
|
||||
|
@ -166,13 +170,7 @@ test('evaluates loops', async () => {
|
|||
}
|
||||
`),
|
||||
);
|
||||
let result;
|
||||
do {
|
||||
result = sandbox.step();
|
||||
if (result.value?.async) {
|
||||
await result.value.value;
|
||||
}
|
||||
} while (!result.done);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
a: 3,
|
||||
|
@ -207,7 +205,7 @@ test('returns values at the top level', async () => {
|
|||
y = 8
|
||||
`, {allowReturnOutsideFunction: true}),
|
||||
);
|
||||
const {value: {value}} = sandbox.run();
|
||||
const {value} = sandbox.run();
|
||||
expect(value)
|
||||
.to.deep.equal([48, 12]);
|
||||
expect(sandbox.context)
|
||||
|
@ -257,3 +255,47 @@ test('runs arbitrary number of ops', async () => {
|
|||
expect(true)
|
||||
.to.be.true;
|
||||
});
|
||||
|
||||
test('clears for loop deferred context', async () => {
|
||||
const context = {x: 10};
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
let l = 0
|
||||
for (let i = 0; i < x; ++i) {
|
||||
l += 1
|
||||
}
|
||||
`),
|
||||
context,
|
||||
);
|
||||
expect(sandbox.run())
|
||||
.to.deep.include({
|
||||
value: 10,
|
||||
});
|
||||
context.x = 0
|
||||
expect(sandbox.run())
|
||||
.to.deep.include({
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('clears while loop deferred context', async () => {
|
||||
const context = {x: 10};
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
let l = 0
|
||||
while (l < x) {
|
||||
l += 1
|
||||
}
|
||||
`),
|
||||
context,
|
||||
);
|
||||
expect(sandbox.run())
|
||||
.to.deep.include({
|
||||
value: 10,
|
||||
});
|
||||
context.x = 0
|
||||
expect(sandbox.run())
|
||||
.to.deep.include({
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ export default class Scope {
|
|||
}
|
||||
|
||||
allocate(key, value) {
|
||||
this.context[key] = value;
|
||||
return this.context[key] = value;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user