Compare commits
21 Commits
2376b5b1c3
...
2ab82d1d3e
Author | SHA1 | Date | |
---|---|---|---|
|
2ab82d1d3e | ||
|
d00bcf23f3 | ||
|
d7a629db7a | ||
|
00862f96bd | ||
|
398dd66594 | ||
|
31722ab4a0 | ||
|
bb7435e18e | ||
|
89c32a2299 | ||
|
5db2e4905c | ||
|
13f49813a8 | ||
|
aa586a98e5 | ||
|
bcbef693cc | ||
|
b3087ac4d3 | ||
|
6c7221a7f7 | ||
|
1a3a8f28d0 | ||
|
90d8ee3a35 | ||
|
a53be624d3 | ||
|
221666783d | ||
|
853f9f1fb4 | ||
|
2830764f9f | ||
|
ee68d0414e |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,4 +4,5 @@ node_modules
|
||||||
/.cache
|
/.cache
|
||||||
/build
|
/build
|
||||||
/coverage
|
/coverage
|
||||||
|
/dev
|
||||||
.env
|
.env
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default function evaluate(node, {scope} = {}) {
|
||||||
return evaluators.unary(node, {evaluate, scope});
|
return evaluators.unary(node, {evaluate, scope});
|
||||||
case 'UpdateExpression':
|
case 'UpdateExpression':
|
||||||
return evaluators.update(node, {evaluate, scope});
|
return evaluators.update(node, {evaluate, scope});
|
||||||
|
/* v8 ignore next 2 */
|
||||||
default:
|
default:
|
||||||
throw new EvalError(`astride: Can't evaluate node of type ${node.type}`)
|
throw new EvalError(`astride: Can't evaluate node of type ${node.type}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import {isSpreadElement} from '@/astride/types.js';
|
|
||||||
|
|
||||||
export default function(node, {evaluate, scope}) {
|
export default function(node, {evaluate, scope}) {
|
||||||
const elements = [];
|
const elements = [];
|
||||||
const asyncSpread = Object.create(null);
|
const asyncSpread = Object.create(null);
|
||||||
let isAsync = false;
|
let isAsync = false;
|
||||||
for (const index in node.elements) {
|
for (const index in node.elements) {
|
||||||
const element = node.elements[index];
|
const element = node.elements[index];
|
||||||
if (isSpreadElement(element)) {
|
if ('SpreadElement' === element.type) {
|
||||||
const {async, value} = evaluate(element.argument, {scope});
|
const {async, value} = evaluate(element.argument, {scope});
|
||||||
isAsync = isAsync || async;
|
isAsync = isAsync || async;
|
||||||
if (async) {
|
if (async) {
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import {
|
|
||||||
isMemberExpression,
|
|
||||||
} from '@/astride/types.js';
|
|
||||||
|
|
||||||
export default function(node, {evaluate, scope}) {
|
export default function(node, {evaluate, scope}) {
|
||||||
const {operator, left} = node;
|
const {operator, left} = node;
|
||||||
const right = evaluate(node.right, {scope});
|
const right = evaluate(node.right, {scope});
|
||||||
if (!isMemberExpression(left)) {
|
if (!('MemberExpression' === left.type)) {
|
||||||
const assign = (value) => {
|
const assign = (value) => {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case '=' : return scope.set(left.name, value);
|
case '=' : return scope.set(left.name, value);
|
||||||
|
|
|
@ -41,5 +41,8 @@ export default function(node, {evaluate, scope}) {
|
||||||
.then(([left, right]) => binary(left, right)),
|
.then(([left, right]) => binary(left, right)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {value: binary(left.value, right.value)};
|
return {
|
||||||
|
async: false,
|
||||||
|
value: binary(left.value, right.value),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import fastCall from '@/util/fast-call.js';
|
import fastCall from '@/util/fast-call.js';
|
||||||
import {
|
|
||||||
isMemberExpression,
|
|
||||||
} from '@/astride/types.js';
|
|
||||||
|
|
||||||
export default function(node, {evaluate, scope}) {
|
export default function(node, {evaluate, scope}) {
|
||||||
let asyncArgs = false;
|
let asyncArgs = false;
|
||||||
|
@ -24,7 +21,7 @@ export default function(node, {evaluate, scope}) {
|
||||||
}
|
}
|
||||||
return fastCall(fn, holder, args);
|
return fastCall(fn, holder, args);
|
||||||
};
|
};
|
||||||
if (!isMemberExpression(callee)) {
|
if (!('MemberExpression' === callee.type)) {
|
||||||
const {async, value} = evaluate(callee, {scope});
|
const {async, value} = evaluate(callee, {scope});
|
||||||
if (asyncArgs || async) {
|
if (asyncArgs || async) {
|
||||||
return {
|
return {
|
||||||
|
@ -49,5 +46,8 @@ export default function(node, {evaluate, scope}) {
|
||||||
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
|
.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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,8 @@ scopeTest('evaluates optional calls', async ({scope}) => {
|
||||||
scope.set('O', {});
|
scope.set('O', {});
|
||||||
expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value)
|
expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value)
|
||||||
.to.equal(undefined);
|
.to.equal(undefined);
|
||||||
// expect(evaluate(await expression('O?.g(1, 2, 3)'), {scope}).value)
|
expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
|
||||||
// .to.equal(undefined);
|
.to.equal(undefined);
|
||||||
// expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
|
|
||||||
// .to.equal(undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
scopeTest('evaluates async calls', async ({scope}) => {
|
scopeTest('evaluates async calls', async ({scope}) => {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export default function(node, {scope}) {
|
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}) {
|
export default function({value}) {
|
||||||
return {value};
|
return {async: false, value};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export default function(node, {evaluate, scope}) {
|
export default function(node, {evaluate, scope}) {
|
||||||
const {computed, object, property, wrapper} = node;
|
const {computed, object, optional, property} = node;
|
||||||
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]);
|
const member = (O, P) => (optional ? O?.[P] : O[P]);
|
||||||
const O = evaluate(object, {scope});
|
const O = evaluate(object, {scope});
|
||||||
const P = computed
|
const P = computed
|
||||||
? evaluate(property, {scope})
|
? evaluate(property, {scope})
|
||||||
|
@ -12,5 +12,5 @@ export default function(node, {evaluate, scope}) {
|
||||||
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
|
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)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,18 @@
|
||||||
import {
|
|
||||||
isIdentifier,
|
|
||||||
isLiteral,
|
|
||||||
isProperty,
|
|
||||||
isSpreadElement,
|
|
||||||
} from '@/astride/types.js';
|
|
||||||
|
|
||||||
export default function(node, {evaluate, scope}) {
|
export default function(node, {evaluate, scope}) {
|
||||||
const {properties} = node;
|
const {properties} = node;
|
||||||
let isAsync = false;
|
let isAsync = false;
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (let i = 0; i < properties.length; i++) {
|
for (let i = 0; i < properties.length; i++) {
|
||||||
if (isProperty(properties[i])) {
|
if ('Property' === properties[i].type) {
|
||||||
const {computed, key, value} = properties[i];
|
const {computed, key, value} = properties[i];
|
||||||
let k;
|
let k;
|
||||||
if (computed) {
|
if (computed) {
|
||||||
k = evaluate(key, {scope});
|
k = evaluate(key, {scope});
|
||||||
}
|
}
|
||||||
else if (isIdentifier(key)) {
|
else if ('Identifier' === key.type) {
|
||||||
k = {value: key.name};
|
k = {value: key.name};
|
||||||
}
|
}
|
||||||
else if (isLiteral(key)) {
|
else if ('Literal' === key.type) {
|
||||||
k = {value: key.value};
|
k = {value: key.value};
|
||||||
}
|
}
|
||||||
/* v8 ignore next 3 */
|
/* v8 ignore next 3 */
|
||||||
|
@ -35,7 +28,7 @@ export default function(node, {evaluate, scope}) {
|
||||||
entries.push([k.value, v.value]);
|
entries.push([k.value, v.value]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isSpreadElement(properties[i])) {
|
if ('SpreadElement' === properties[i].type) {
|
||||||
const {argument} = properties[i];
|
const {argument} = properties[i];
|
||||||
const spreading = evaluate(argument, {scope});
|
const spreading = evaluate(argument, {scope});
|
||||||
isAsync ||= spreading.async;
|
isAsync ||= spreading.async;
|
||||||
|
|
|
@ -20,5 +20,5 @@ export default function(node, {evaluate, scope}) {
|
||||||
value: Promise.resolve(arg.value).then(unary),
|
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 */
|
/* v8 ignore next */
|
||||||
throw new Error(`operator not implemented: ${operator}`);
|
throw new Error(`operator not implemented: ${operator}`);
|
||||||
};
|
};
|
||||||
return {value: update(value)};
|
return {async: false, value: update(value)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,16 @@
|
||||||
import evaluate from '@/astride/evaluate.js';
|
import evaluate from '@/astride/evaluate.js';
|
||||||
import Scope from '@/astride/scope.js';
|
import Scope from '@/astride/scope.js';
|
||||||
import traverse, {TRAVERSAL_PATH} from '@/astride/traverse.js';
|
import traverse from '@/astride/traverse.js';
|
||||||
import {
|
|
||||||
isArrayPattern,
|
const YIELD_NONE = 0;
|
||||||
isBlockStatement,
|
const YIELD_PROMISE = 1;
|
||||||
isDoWhileStatement,
|
const YIELD_LOOP_UPDATE = 2;
|
||||||
isExpressionStatement,
|
const YIELD_RETURN = 3;
|
||||||
isForStatement,
|
|
||||||
isIdentifier,
|
|
||||||
isIfStatement,
|
|
||||||
isObjectPattern,
|
|
||||||
isReturnStatement,
|
|
||||||
isVariableDeclarator,
|
|
||||||
isWhileStatement,
|
|
||||||
} from '@/astride/types.js';
|
|
||||||
|
|
||||||
export default class Sandbox {
|
export default class Sandbox {
|
||||||
|
|
||||||
ast;
|
ast;
|
||||||
generator;
|
$$execution;
|
||||||
scopes;
|
scopes;
|
||||||
|
|
||||||
constructor(ast, context = {}) {
|
constructor(ast, context = {}) {
|
||||||
|
@ -30,13 +22,13 @@ export default class Sandbox {
|
||||||
compile() {
|
compile() {
|
||||||
let scope = new Scope();
|
let scope = new Scope();
|
||||||
scope.context = this.$$context;
|
scope.context = this.$$context;
|
||||||
this.scopes = new WeakMap([[this.ast, scope]]);
|
this.scopes = new Map([[this.ast, scope]]);
|
||||||
traverse(
|
traverse(
|
||||||
this.ast,
|
this.ast,
|
||||||
(node, verb) => {
|
(node, verb) => {
|
||||||
if (
|
if (
|
||||||
isBlockStatement(node)
|
'BlockStatement' === node.type
|
||||||
|| isForStatement(node)
|
|| 'ForStatement' === node.type
|
||||||
) {
|
) {
|
||||||
switch (verb) {
|
switch (verb) {
|
||||||
case 'enter': {
|
case 'enter': {
|
||||||
|
@ -68,7 +60,7 @@ export default class Sandbox {
|
||||||
if (null === element) {
|
if (null === element) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isIdentifier(element)) {
|
if ('Identifier' === element.type) {
|
||||||
scope.allocate(element.name, init[i]);
|
scope.allocate(element.name, init[i]);
|
||||||
}
|
}
|
||||||
/* v8 ignore next 3 */
|
/* v8 ignore next 3 */
|
||||||
|
@ -76,7 +68,7 @@ export default class Sandbox {
|
||||||
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return init;
|
||||||
}
|
}
|
||||||
|
|
||||||
destructureObject(id, init) {
|
destructureObject(id, init) {
|
||||||
|
@ -84,8 +76,8 @@ export default class Sandbox {
|
||||||
const {properties} = id;
|
const {properties} = id;
|
||||||
for (let i = 0; i < properties.length; ++i) {
|
for (let i = 0; i < properties.length; ++i) {
|
||||||
const property = properties[i];
|
const property = properties[i];
|
||||||
if (isObjectPattern(property.value)) {
|
if ('ObjectPattern' === property.value.type) {
|
||||||
this.destructureObject(property.value, init[property.key.name], scope);
|
this.destructureObject(property.value, init[property.key.name]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.allocate(
|
scope.allocate(
|
||||||
|
@ -94,6 +86,7 @@ export default class Sandbox {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return init;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate(node) {
|
evaluate(node) {
|
||||||
|
@ -103,161 +96,408 @@ export default class Sandbox {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
*execute(node, parent) {
|
evaluateToResult(node) {
|
||||||
let keys = TRAVERSAL_PATH[node.type];
|
const {async, value} = this.evaluate(node);
|
||||||
if (isVariableDeclarator(node)) {
|
return {
|
||||||
const {id} = node;
|
value,
|
||||||
const scope = this.scopes.get(node);
|
yield: async ? YIELD_PROMISE : YIELD_NONE,
|
||||||
if (null === node.init) {
|
};
|
||||||
scope.allocate(id.name, undefined);
|
}
|
||||||
}
|
|
||||||
else {
|
executeSync(node, depth) {
|
||||||
const init = this.evaluate(node.init);
|
let result;
|
||||||
if (isIdentifier(id)) {
|
const isReplaying = depth < this.$$execution.stack.length;
|
||||||
if (init.async) {
|
const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
|
||||||
yield {
|
if (!isReplaying) {
|
||||||
async: true,
|
this.$$execution.stack.push(node);
|
||||||
value: Promise.resolve(init.value).then((value) => {
|
}
|
||||||
scope.allocate(id.name, value);
|
// Substitute executing the node for its deferred result.
|
||||||
}),
|
else if (isReplayingThisLevel) {
|
||||||
};
|
if (this.$$execution.stack[depth] === node) {
|
||||||
}
|
result = this.$$execution.deferred.get(node);
|
||||||
else {
|
this.$$execution.deferred.delete(node);
|
||||||
scope.allocate(id.name, init.value);
|
this.$$execution.stack.pop();
|
||||||
}
|
return result;
|
||||||
}
|
|
||||||
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...
|
switch (node.type) {
|
||||||
if (isIfStatement(node)) {
|
case 'ArrayExpression':
|
||||||
const {async, value} = this.evaluate(node.test);
|
case 'AssignmentExpression':
|
||||||
const branch = (value) => {
|
case 'BinaryExpression':
|
||||||
keys = [value ? 'consequent' : 'alternate'];
|
case 'CallExpression':
|
||||||
};
|
case 'ChainExpression':
|
||||||
if (async) {
|
case 'ObjectExpression':
|
||||||
yield {
|
case 'Identifier':
|
||||||
async: true,
|
case 'LogicalExpression':
|
||||||
value: Promise.resolve(value).then(branch),
|
case 'MemberExpression':
|
||||||
};
|
case 'UnaryExpression':
|
||||||
}
|
case 'UpdateExpression': {
|
||||||
else {
|
result = this.evaluateToResult(node);
|
||||||
branch(value);
|
if (result.yield) {
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// Loops...
|
case 'AwaitExpression': {
|
||||||
if (isForStatement(node)) {
|
const coerce = !this.$$execution.deferred.has(node.argument);
|
||||||
const result = this.execute(node.update, node).next();
|
result = this.executeSync(node.argument, depth + 1);
|
||||||
if (result.value?.async) {
|
if (coerce) {
|
||||||
yield result.value;
|
result = {
|
||||||
}
|
value: result.value,
|
||||||
}
|
yield: YIELD_PROMISE,
|
||||||
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 {
|
if (result.yield) {
|
||||||
loop = test.value;
|
return result;
|
||||||
}
|
}
|
||||||
|
/* v8 ignore next 2 */
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} while (loop);
|
case 'BlockStatement': {
|
||||||
// Top-level return.
|
result = {
|
||||||
if (isReturnStatement(node)) {
|
value: undefined,
|
||||||
return !node.argument ? {value: undefined} : this.evaluate(node.argument);
|
yield: YIELD_NONE,
|
||||||
}
|
};
|
||||||
if (isExpressionStatement(node)) {
|
let skipping = isReplaying;
|
||||||
yield this.evaluate(node.expression);
|
for (const child of node.body) {
|
||||||
}
|
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||||
// yield ForStatement afterthought.
|
skipping = false;
|
||||||
if (isForStatement(parent) && node === parent.update) {
|
}
|
||||||
yield this.evaluate(node);
|
/* v8 ignore next 3 */
|
||||||
/* v8 ignore next */
|
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': {
|
||||||
|
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() {
|
reset() {
|
||||||
this.generator = undefined;
|
this.$$execution = undefined;
|
||||||
this.compile();
|
for (const scope of this.scopes.values()) {
|
||||||
|
scope.context = {};
|
||||||
|
}
|
||||||
|
this.scopes.get(this.ast).context = this.$$context;
|
||||||
}
|
}
|
||||||
|
|
||||||
run(ops = 1000) {
|
run(ops = 1000) {
|
||||||
let result;
|
let result;
|
||||||
for (let i = 0; i < ops; ++i) {
|
for (let i = 0; i < ops; ++i) {
|
||||||
result = this.step();
|
result = this.step();
|
||||||
if (result.done || result.value?.async) {
|
if (result.done || result.async) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,14 +505,40 @@ export default class Sandbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
step() {
|
step() {
|
||||||
if (!this.generator) {
|
if (!this.$$execution) {
|
||||||
this.generator = this.execute(this.ast);
|
this.$$execution = {
|
||||||
|
deferred: new Map(),
|
||||||
|
stack: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const result = this.generator.next();
|
let result = this.executeSync(this.ast, 0);
|
||||||
if (result.done) {
|
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();
|
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 () => {
|
test('declares variables', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
|
@ -22,13 +34,7 @@ test('declares variables', async () => {
|
||||||
const asyncObject = await {11: '12'};
|
const asyncObject = await {11: '12'};
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.async;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
scalar: 1,
|
scalar: 1,
|
||||||
|
@ -53,17 +59,24 @@ test('scopes variables', async () => {
|
||||||
result.push(scalar);
|
result.push(scalar);
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.async;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context.result)
|
expect(sandbox.context.result)
|
||||||
.to.deep.equal([1, 2, 1]);
|
.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 () => {
|
test('destructures variables', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
|
@ -73,13 +86,7 @@ test('destructures variables', async () => {
|
||||||
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
a: 1,
|
a: 1,
|
||||||
|
@ -98,19 +105,17 @@ test('runs arbitrary number of ops', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
const foo = [];
|
const foo = [];
|
||||||
for (let i = 0; i < 1500; ++i) {
|
for (let i = 0; i < 150; ++i) {
|
||||||
foo.push(i);
|
foo.push(i);
|
||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
sandbox.run(1000);
|
sandbox.run(100);
|
||||||
expect(sandbox.context.foo.length)
|
expect(sandbox.context.foo.length)
|
||||||
.to.equal(1000);
|
.to.equal(100);
|
||||||
sandbox.run(1000);
|
sandbox.run(100);
|
||||||
expect(sandbox.context.foo.length)
|
expect(sandbox.context.foo.length)
|
||||||
.to.equal(1500);
|
.to.equal(150);
|
||||||
expect(true)
|
|
||||||
.to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('evaluates conditional branches', async () => {
|
test('evaluates conditional branches', async () => {
|
||||||
|
@ -129,15 +134,12 @@ test('evaluates conditional branches', async () => {
|
||||||
else {
|
else {
|
||||||
bar = 2;
|
bar = 2;
|
||||||
}
|
}
|
||||||
|
if (await false) {
|
||||||
|
bar = 1;
|
||||||
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
foo: 1,
|
foo: 1,
|
||||||
|
@ -145,10 +147,42 @@ test('evaluates conditional branches', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('evaluates conditional expressions', async () => {
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
const x = true || false ? 1 : 2
|
||||||
|
const y = false && true ? 1 : 2
|
||||||
|
const a = (await true) ? await 1 : await 2
|
||||||
|
const b = (await false) ? await 1 : await 2
|
||||||
|
xx = true || false ? 1 : 2
|
||||||
|
yy = false && true ? 1 : 2
|
||||||
|
aa = (await true) ? await 1 : await 2
|
||||||
|
bb = (await false) ? await 1 : await 2
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
await finish(sandbox);
|
||||||
|
expect(sandbox.context)
|
||||||
|
.to.deep.equal({
|
||||||
|
x: 1,
|
||||||
|
y: 2,
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
xx: 1,
|
||||||
|
yy: 2,
|
||||||
|
aa: 1,
|
||||||
|
bb: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('evaluates loops', async () => {
|
test('evaluates loops', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
let x = 0, y = 0, a = 0, b = 0, c = 0;
|
x = 0
|
||||||
|
y = 0
|
||||||
|
a = 0
|
||||||
|
b = 0
|
||||||
|
c = 0
|
||||||
|
d = 0
|
||||||
for (let i = 0; i < 3; ++i) {
|
for (let i = 0; i < 3; ++i) {
|
||||||
x += 1;
|
x += 1;
|
||||||
}
|
}
|
||||||
|
@ -159,25 +193,23 @@ test('evaluates loops', async () => {
|
||||||
a += 1;
|
a += 1;
|
||||||
} while (a < 3);
|
} while (a < 3);
|
||||||
do {
|
do {
|
||||||
b += 1;
|
b += await 1;
|
||||||
} while (await b < 3);
|
} while (await b < 3);
|
||||||
while (c < 3) {
|
while (c < 3) {
|
||||||
c += 1;
|
c += 1;
|
||||||
}
|
}
|
||||||
|
while (await d < 3) {
|
||||||
|
d += await 1;
|
||||||
|
}
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
let result;
|
await finish(sandbox);
|
||||||
do {
|
|
||||||
result = sandbox.step();
|
|
||||||
if (result.value?.async) {
|
|
||||||
await result.value.value;
|
|
||||||
}
|
|
||||||
} while (!result.done);
|
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({
|
.to.deep.equal({
|
||||||
a: 3,
|
a: 3,
|
||||||
b: 3,
|
b: 3,
|
||||||
c: 3,
|
c: 3,
|
||||||
|
d: 3,
|
||||||
x: 3,
|
x: 3,
|
||||||
y: 3,
|
y: 3,
|
||||||
});
|
});
|
||||||
|
@ -207,8 +239,20 @@ test('returns values at the top level', async () => {
|
||||||
y = 8
|
y = 8
|
||||||
`, {allowReturnOutsideFunction: true}),
|
`, {allowReturnOutsideFunction: true}),
|
||||||
);
|
);
|
||||||
const {value: {value}} = sandbox.run();
|
expect(sandbox.run().value)
|
||||||
expect(value)
|
.to.deep.equal([48, 12]);
|
||||||
|
expect(sandbox.context)
|
||||||
|
.to.deep.equal({x: 16, y: 4});
|
||||||
|
sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
x = 16
|
||||||
|
y = 4
|
||||||
|
return await [x * 3, y * 3]
|
||||||
|
x = 32
|
||||||
|
y = 8
|
||||||
|
`, {allowReturnOutsideFunction: true}),
|
||||||
|
);
|
||||||
|
expect((await finish(sandbox)).value)
|
||||||
.to.deep.equal([48, 12]);
|
.to.deep.equal([48, 12]);
|
||||||
expect(sandbox.context)
|
expect(sandbox.context)
|
||||||
.to.deep.equal({x: 16, y: 4});
|
.to.deep.equal({x: 16, y: 4});
|
||||||
|
@ -226,6 +270,24 @@ test('returns values at the top level', async () => {
|
||||||
.to.deep.equal({x: 16, y: 4});
|
.to.deep.equal({x: 16, y: 4});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns arbitrarily', async () => {
|
||||||
|
let sandbox;
|
||||||
|
sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
x = 16
|
||||||
|
y = 4
|
||||||
|
if (x === 16) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
`, {allowReturnOutsideFunction: true}),
|
||||||
|
);
|
||||||
|
const {value} = sandbox.run();
|
||||||
|
expect(value)
|
||||||
|
.to.deep.equal(false);
|
||||||
|
expect(sandbox.context)
|
||||||
|
.to.deep.equal({x: 16, y: 4});
|
||||||
|
});
|
||||||
|
|
||||||
test('sets variables in global scope', async () => {
|
test('sets variables in global scope', async () => {
|
||||||
const sandbox = new Sandbox(
|
const sandbox = new Sandbox(
|
||||||
await parse(`
|
await parse(`
|
||||||
|
@ -257,3 +319,133 @@ test('runs arbitrary number of ops', async () => {
|
||||||
expect(true)
|
expect(true)
|
||||||
.to.be.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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes member expressions', async () => {
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
let l = {}
|
||||||
|
l.f = 54
|
||||||
|
if (l.f) {
|
||||||
|
l.g = 65
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({
|
||||||
|
value: 65,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles nested yields', async () => {
|
||||||
|
expect(
|
||||||
|
await finish(new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
for (let i = 0; i < 1; ++i) {
|
||||||
|
for (let j = 0; j < 1; ++j) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
.to.deep.include({value: undefined});
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await finish(new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
for (let i = 0; i < 1; ++i) {
|
||||||
|
for (let j = 0; j < 1; ++j) {
|
||||||
|
await 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.to.deep.include({value: 1});
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await finish(new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
if (true) {
|
||||||
|
for (let i = 0; i < 1; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.to.deep.include({value: undefined});
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await finish(new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
if (1 ? await 2 : 3) {
|
||||||
|
for (let i = 0; i < 1; ++i) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.to.deep.include({value: undefined});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles optional expressions', async () => {
|
||||||
|
const context = {
|
||||||
|
console,
|
||||||
|
projected: undefined,
|
||||||
|
}
|
||||||
|
const sandbox = new Sandbox(
|
||||||
|
await parse(`
|
||||||
|
if (projected?.length > 0) {
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
expect(sandbox.run())
|
||||||
|
.to.deep.include({value: undefined});
|
||||||
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default class Scope {
|
||||||
}
|
}
|
||||||
|
|
||||||
allocate(key, value) {
|
allocate(key, value) {
|
||||||
this.context[key] = value;
|
return this.context[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const TRAVERSAL_PATH = {
|
||||||
IfStatement: ['alternate', 'consequent', 'test'],
|
IfStatement: ['alternate', 'consequent', 'test'],
|
||||||
MemberExpression: ['object', 'property'],
|
MemberExpression: ['object', 'property'],
|
||||||
Literal: [],
|
Literal: [],
|
||||||
|
LogicalExpression: ['left', 'right'],
|
||||||
ObjectExpression: ['properties'],
|
ObjectExpression: ['properties'],
|
||||||
ObjectPattern: ['properties'],
|
ObjectPattern: ['properties'],
|
||||||
Program: ['body'],
|
Program: ['body'],
|
||||||
|
@ -35,15 +36,9 @@ export default function traverse(node, visitor) {
|
||||||
}
|
}
|
||||||
visitor(node, 'enter');
|
visitor(node, 'enter');
|
||||||
const path = TRAVERSAL_PATH[node.type];
|
const path = TRAVERSAL_PATH[node.type];
|
||||||
let children;
|
const children = [];
|
||||||
if (path instanceof Function) {
|
for (const key of path) {
|
||||||
children = path(node);
|
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
|
||||||
}
|
|
||||||
else if (Array.isArray(path)) {
|
|
||||||
children = [];
|
|
||||||
for (const key of path) {
|
|
||||||
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (child) {
|
if (child) {
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
export function isArrayPattern(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'ArrayPattern') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isBlockStatement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'BlockStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isDoWhileStatement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'DoWhileStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExpressionStatement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'ExpressionStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isForStatement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'ForStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isIdentifier(node) {
|
|
||||||
if (!node || node.type !== 'Identifier') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isIfStatement(node) {
|
|
||||||
if (!node || node.type !== 'IfStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLiteral(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'Literal') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMemberExpression(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'MemberExpression') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObjectPattern(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'ObjectPattern') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isProperty(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'Property') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isReturnStatement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'ReturnStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSpreadElement(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'SpreadElement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isVariableDeclarator(node) {
|
|
||||||
/* v8 ignore next 3 */
|
|
||||||
if (!node || node.type !== 'VariableDeclarator') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isWhileStatement(node) {
|
|
||||||
if (!node || node.type !== 'WhileStatement') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
|
@ -1,12 +1,17 @@
|
||||||
import Component from '@/ecs/component.js';
|
import Component from '@/ecs/component.js';
|
||||||
|
|
||||||
export default class Controlled extends Component {
|
export default class Controlled extends Component {
|
||||||
|
instanceFromSchema() {
|
||||||
|
return class ControlledInstance extends super.instanceFromSchema() {
|
||||||
|
toJSON() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
static properties = {
|
static properties = {
|
||||||
locked: {type: 'uint8'},
|
|
||||||
moveUp: {type: 'float32'},
|
moveUp: {type: 'float32'},
|
||||||
moveRight: {type: 'float32'},
|
moveRight: {type: 'float32'},
|
||||||
moveDown: {type: 'float32'},
|
moveDown: {type: 'float32'},
|
||||||
moveLeft: {type: 'float32'},
|
moveLeft: {type: 'float32'},
|
||||||
changeSlot: {type: 'int8'},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,6 +130,15 @@ export default class Engine {
|
||||||
},
|
},
|
||||||
Water: {water: {}},
|
Water: {water: {}},
|
||||||
});
|
});
|
||||||
|
await ecs.create({
|
||||||
|
Position: {x: 100, y: 100},
|
||||||
|
Sprite: {
|
||||||
|
anchor: {x: 0.5, y: 0.8},
|
||||||
|
animation: 'shit-shack/shit-shack/0',
|
||||||
|
source: '/assets/shit-shack/shit-shack.json',
|
||||||
|
},
|
||||||
|
VisibleAabb: {},
|
||||||
|
});
|
||||||
const defaultSystems = [
|
const defaultSystems = [
|
||||||
'ResetForces',
|
'ResetForces',
|
||||||
'ApplyControlMovement',
|
'ApplyControlMovement',
|
||||||
|
@ -151,7 +160,7 @@ export default class Engine {
|
||||||
System.active = true;
|
System.active = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.saveEcs(join('homesteads', `${id}`), ecs);
|
await this.saveEcs(join('homesteads', `${id}`), ecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPlayer(id) {
|
async createPlayer(id) {
|
||||||
|
@ -184,7 +193,6 @@ export default class Engine {
|
||||||
},
|
},
|
||||||
Health: {health: 100},
|
Health: {health: 100},
|
||||||
Position: {x: 368, y: 368},
|
Position: {x: 368, y: 368},
|
||||||
VisibleAabb: {},
|
|
||||||
Speed: {speed: 100},
|
Speed: {speed: 100},
|
||||||
Sound: {},
|
Sound: {},
|
||||||
Sprite: {
|
Sprite: {
|
||||||
|
@ -196,6 +204,7 @@ export default class Engine {
|
||||||
speed: 0.115,
|
speed: 0.115,
|
||||||
},
|
},
|
||||||
Ticking: {},
|
Ticking: {},
|
||||||
|
VisibleAabb: {},
|
||||||
Wielder: {
|
Wielder: {
|
||||||
activeSlot: 0,
|
activeSlot: 0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,9 @@ class TestServer extends Server {
|
||||||
super();
|
super();
|
||||||
this.data = {};
|
this.data = {};
|
||||||
}
|
}
|
||||||
|
async readAsset() {
|
||||||
|
return new ArrayBuffer(0);
|
||||||
|
}
|
||||||
async readData(path) {
|
async readData(path) {
|
||||||
if (path in this.data) {
|
if (path in this.data) {
|
||||||
return this.data[path];
|
return this.data[path];
|
||||||
|
@ -34,16 +37,17 @@ test('visibility-based updates', async () => {
|
||||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
Position: {x: (RESOLUTION.x * 1.5) + 32 - 3, y: 20},
|
||||||
VisibleAabb: {},
|
VisibleAabb: {},
|
||||||
}));
|
}));
|
||||||
|
const {entity: mainEntity} = engine.connectedPlayers.get(0);
|
||||||
// Tick and get update. Should be a full update.
|
// Tick and get update. Should be a full update.
|
||||||
engine.tick(1);
|
engine.tick(1);
|
||||||
expect(engine.updateFor(0))
|
expect(engine.updateFor(0))
|
||||||
.to.deep.include({2: {MainEntity: {}, ...ecs.get(2).toJSON()}, 3: ecs.get(3).toJSON()});
|
.to.deep.include({[mainEntity.id]: {MainEntity: {}, ...ecs.get(mainEntity.id).toJSON()}, [entity.id]: ecs.get(entity.id).toJSON()});
|
||||||
engine.setClean();
|
engine.setClean();
|
||||||
// Tick and get update. Should be a partial update.
|
// Tick and get update. Should be a partial update.
|
||||||
engine.tick(1);
|
engine.tick(1);
|
||||||
expect(engine.updateFor(0))
|
expect(engine.updateFor(0))
|
||||||
.to.deep.include({
|
.to.deep.include({
|
||||||
3: {
|
[entity.id]: {
|
||||||
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
Position: {x: (RESOLUTION.x * 1.5) + 32 - 1},
|
||||||
VisibleAabb: {
|
VisibleAabb: {
|
||||||
x0: 1199,
|
x0: 1199,
|
||||||
|
@ -55,11 +59,11 @@ test('visibility-based updates', async () => {
|
||||||
// Tick and get update. Should remove the entity.
|
// Tick and get update. Should remove the entity.
|
||||||
engine.tick(1);
|
engine.tick(1);
|
||||||
expect(engine.updateFor(0))
|
expect(engine.updateFor(0))
|
||||||
.to.deep.include({3: false});
|
.to.deep.include({[entity.id]: false});
|
||||||
// Aim back toward visible area and tick. Should be a full update for that entity.
|
// Aim back toward visible area and tick. Should be a full update for that entity.
|
||||||
engine.setClean();
|
engine.setClean();
|
||||||
entity.Forces.forceX = -1;
|
entity.Forces.forceX = -1;
|
||||||
engine.tick(1);
|
engine.tick(1);
|
||||||
expect(engine.updateFor(0))
|
expect(engine.updateFor(0))
|
||||||
.to.deep.include({3: ecs.get(3).toJSON()});
|
.to.deep.include({[entity.id]: ecs.get(entity.id).toJSON()});
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,9 +10,6 @@ function onMessage(event) {
|
||||||
onmessage = async (event) => {
|
onmessage = async (event) => {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
const url = new URL(`wss://${event.data.host}/ws`)
|
const url = new URL(`wss://${event.data.host}/ws`)
|
||||||
if ('production' === process.env.NODE_ENV) {
|
|
||||||
url.protocol = 'ws:';
|
|
||||||
}
|
|
||||||
socket = new WebSocket(url.href);
|
socket = new WebSocket(url.href);
|
||||||
socket.binaryType = 'arraybuffer';
|
socket.binaryType = 'arraybuffer';
|
||||||
const {promise, resolve} = Promise.withResolvers();
|
const {promise, resolve} = Promise.withResolvers();
|
||||||
|
|
|
@ -26,9 +26,6 @@ export default class RemoteClient extends Client {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const url = new URL(`wss://${host}/ws`)
|
const url = new URL(`wss://${host}/ws`)
|
||||||
if ('production' === process.env.NODE_ENV) {
|
|
||||||
url.protocol = 'ws:';
|
|
||||||
}
|
|
||||||
this.socket = new WebSocket(url.href);
|
this.socket = new WebSocket(url.href);
|
||||||
this.socket.binaryType = 'arraybuffer';
|
this.socket.binaryType = 'arraybuffer';
|
||||||
const onMessage = (event) => {
|
const onMessage = (event) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {get, set} from 'idb-keyval';
|
import {del, get, set} from 'idb-keyval';
|
||||||
|
|
||||||
import {encode} from '@/packets/index.js';
|
import {encode} from '@/packets/index.js';
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@ class WorkerServer extends Server {
|
||||||
error.code = 'ENOENT';
|
error.code = 'ENOENT';
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
async removeData(path) {
|
||||||
|
await del(this.constructor.qualify(path));
|
||||||
|
}
|
||||||
async writeData(path, view) {
|
async writeData(path, view) {
|
||||||
await set(this.constructor.qualify(path), view);
|
await set(this.constructor.qualify(path), view);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +41,7 @@ const engine = new Engine(WorkerServer);
|
||||||
onmessage = async (event) => {
|
onmessage = async (event) => {
|
||||||
if (0 === event.data) {
|
if (0 === event.data) {
|
||||||
engine.stop();
|
engine.stop();
|
||||||
await engine.disconnectPlayer(0, 0);
|
await engine.disconnectPlayer(0);
|
||||||
await engine.saveEcses();
|
await engine.saveEcses();
|
||||||
postMessage(0);
|
postMessage(0);
|
||||||
return;
|
return;
|
||||||
|
@ -54,8 +57,14 @@ onmessage = async (event) => {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
import.meta.hot.accept('../engine/engine.js', async () => {
|
import.meta.hot.accept('../../engine.js', async ({default: Engine}) => {
|
||||||
await engine.disconnectPlayer(0, 0);
|
await engine.disconnectPlayer(0);
|
||||||
|
if (Engine.prototype.createHomestead.toString() !== engine.createHomestead.toString()) {
|
||||||
|
delete engine.ecses['homesteads/0'];
|
||||||
|
await engine.server.removeData('homesteads/0');
|
||||||
|
const newEngine = new Engine(WorkerServer);
|
||||||
|
await newEngine.createHomestead(0);
|
||||||
|
}
|
||||||
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
|
||||||
close();
|
close();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
|
import {useLoaderData} from '@remix-run/react';
|
||||||
|
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
export function loader({request}) {
|
||||||
|
return {
|
||||||
|
host: new URL(request.url).host,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const meta = () => {
|
export const meta = () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -13,12 +21,13 @@ export const meta = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
const {host} = useLoaderData();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.title}>Silphius</h1>
|
<h1 className={styles.title}>Silphius</h1>
|
||||||
<ul className={styles.actions}>
|
<ul className={styles.actions}>
|
||||||
<li><a href="/play/local">Single-player</a></li>
|
<li><a href="/play/local">Single-player</a></li>
|
||||||
<li><a href="/play/remote/localhost:3000">Multi-player</a></li>
|
<li><a href={`/play/remote/${host}`}>Multi-player</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default class Script {
|
||||||
|
|
||||||
evaluateSync() {
|
evaluateSync() {
|
||||||
this.sandbox.reset();
|
this.sandbox.reset();
|
||||||
const {value: {value}} = this.sandbox.run();
|
const {value} = this.sandbox.run();
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,23 +113,20 @@ export default class Script {
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
this.sandbox.context.elapsed = elapsed;
|
this.sandbox.context.elapsed = elapsed;
|
||||||
const {done, value} = this.sandbox.step();
|
const {async, done, value} = this.sandbox.step();
|
||||||
if (value) {
|
if (async) {
|
||||||
const {async, value: result} = value;
|
this.promise = value;
|
||||||
if (async) {
|
value
|
||||||
this.promise = result;
|
.catch(reject)
|
||||||
result
|
.then(() => {
|
||||||
.catch(reject)
|
if (done) {
|
||||||
.then(() => {
|
resolve();
|
||||||
if (done) {
|
}
|
||||||
resolve();
|
})
|
||||||
}
|
.finally(() => {
|
||||||
})
|
this.promise = null;
|
||||||
.finally(() => {
|
});
|
||||||
this.promise = null;
|
break;
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (done) {
|
if (done) {
|
||||||
resolve();
|
resolve();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {mkdir, readFile, writeFile} from 'node:fs/promises';
|
import {mkdir, readFile, unlink, writeFile} from 'node:fs/promises';
|
||||||
import {dirname, join} from 'node:path';
|
import {dirname, join} from 'node:path';
|
||||||
|
|
||||||
import {WebSocketServer} from 'ws';
|
import {WebSocketServer} from 'ws';
|
||||||
|
@ -8,6 +8,8 @@ import {getSession} from '@/session.server.js';
|
||||||
|
|
||||||
import Engine from './engine.js';
|
import Engine from './engine.js';
|
||||||
|
|
||||||
|
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
|
||||||
|
|
||||||
const wss = new WebSocketServer({
|
const wss = new WebSocketServer({
|
||||||
noServer: true,
|
noServer: true,
|
||||||
});
|
});
|
||||||
|
@ -50,7 +52,7 @@ class SocketServer extends Server {
|
||||||
}
|
}
|
||||||
async readAsset(path) {
|
async readAsset(path) {
|
||||||
const url = new URL(path, 'https://localhost:3000')
|
const url = new URL(path, 'https://localhost:3000')
|
||||||
if ('production' === process.env.NODE_ENV) {
|
if (isInsecure) {
|
||||||
url.protocol = 'http:';
|
url.protocol = 'http:';
|
||||||
}
|
}
|
||||||
return fetch(url.href).then((response) => (
|
return fetch(url.href).then((response) => (
|
||||||
|
@ -62,6 +64,9 @@ class SocketServer extends Server {
|
||||||
await this.ensurePath(dirname(qualified));
|
await this.ensurePath(dirname(qualified));
|
||||||
return readFile(qualified);
|
return readFile(qualified);
|
||||||
}
|
}
|
||||||
|
async removeData(path) {
|
||||||
|
await unlink(path);
|
||||||
|
}
|
||||||
async writeData(path, view) {
|
async writeData(path, view) {
|
||||||
const qualified = this.constructor.qualify(path);
|
const qualified = this.constructor.qualify(path);
|
||||||
await this.ensurePath(dirname(qualified));
|
await this.ensurePath(dirname(qualified));
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "remix vite:build",
|
"build": "remix vite:build",
|
||||||
"dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js",
|
"dev": "NODE_OPTIONS=--use-openssl-ca node ./server.js",
|
||||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||||
"start": "cross-env NODE_ENV=production NODE_OPTIONS=--use-openssl-ca npm run dev",
|
"start": "cross-env NODE_ENV=production npm run dev",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"storybook:build": "storybook build",
|
"storybook:build": "storybook build",
|
||||||
"test": "vitest app"
|
"test": "vitest app"
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
"icon": "/assets/hoe/icon.png",
|
"icon": "/assets/hoe/icon.png",
|
||||||
"projectionCheck": "/assets/hoe/projection-check.js",
|
"projectionCheck": "/assets/hoe/projection-check.js",
|
||||||
"projection": {
|
"projection": {
|
||||||
"distance": [1, 0],
|
"distance": [3, -1],
|
||||||
"grid": [
|
"grid": [
|
||||||
[1]
|
[1, 1, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
[1, 1, 1]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"start": "/assets/hoe/start.js"
|
"start": "/assets/hoe/start.js"
|
||||||
|
|
|
@ -118,9 +118,6 @@ if (projected?.length > 0) {
|
||||||
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
|
if ([1, 2, 3, 4].includes(layer.tile(projected[i]))) {
|
||||||
layer.stamp(projected[i], [[7]])
|
layer.stamp(projected[i], [[7]])
|
||||||
}
|
}
|
||||||
// else if ([6].includes(layer.tile(projected[i]))) {
|
|
||||||
// layer.stamp(projected[i], [[7]])
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Controlled.locked = 0;
|
Controlled.locked = 0;
|
||||||
|
|
1
public/assets/shit-shack/shit-shack.json
Normal file
1
public/assets/shit-shack/shit-shack.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"animations":{"shit-shack/shit-shack/0":["shit-shack/shit-shack/0"]},"frames":{"shit-shack/shit-shack/0":{"frame":{"x":0,"y":0,"w":110,"h":102},"spriteSourceSize":{"x":0,"y":0,"w":110,"h":102},"sourceSize":{"w":110,"h":102}}},"meta":{"format":"RGBA8888","image":"./shit-shack.png","scale":1,"size":{"w":110,"h":102}}}
|
BIN
public/assets/shit-shack/shit-shack.png
Normal file
BIN
public/assets/shit-shack/shit-shack.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -2,9 +2,11 @@
|
||||||
"icon": "/assets/watering-can/icon.png",
|
"icon": "/assets/watering-can/icon.png",
|
||||||
"projectionCheck": "/assets/watering-can/projection-check.js",
|
"projectionCheck": "/assets/watering-can/projection-check.js",
|
||||||
"projection": {
|
"projection": {
|
||||||
"distance": [1, 0],
|
"distance": [3, -1],
|
||||||
"grid": [
|
"grid": [
|
||||||
[1]
|
[1, 1, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
[1, 1, 1]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"start": "/assets/watering-can/start.js"
|
"start": "/assets/watering-can/start.js"
|
||||||
|
|
26
server.js
26
server.js
|
@ -4,11 +4,12 @@ import express from 'express';
|
||||||
import morgan from 'morgan';
|
import morgan from 'morgan';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
const isInsecure = process.env.SILPHIUS_INSECURE_HTTP;
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
let server;
|
let server;
|
||||||
if (isProduction) {
|
if (isInsecure) {
|
||||||
const {createServer} = await import('node:http');
|
const {createServer} = await import('node:http');
|
||||||
server = createServer(app);
|
server = createServer(app);
|
||||||
}
|
}
|
||||||
|
@ -39,18 +40,23 @@ const viteDevServer = isProduction
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const ssr = await (
|
let websocketBuilt = false;
|
||||||
viteDevServer
|
|
||||||
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
|
||||||
: import('./build/server/index.js')
|
|
||||||
);
|
|
||||||
|
|
||||||
const remixHandler = createRequestHandler({
|
const remixHandler = createRequestHandler({
|
||||||
build: () => ssr,
|
build: async () => {
|
||||||
|
const ssr = await (
|
||||||
|
viteDevServer
|
||||||
|
? viteDevServer.ssrLoadModule('virtual:remix/server-build')
|
||||||
|
: import('./build/server/index.js')
|
||||||
|
);
|
||||||
|
if (!websocketBuilt) {
|
||||||
|
await ssr.entry.module.websocket(server, viteDevServer);
|
||||||
|
websocketBuilt = true;
|
||||||
|
}
|
||||||
|
return ssr;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await ssr.entry.module.websocket(server, viteDevServer);
|
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||||
|
@ -79,5 +85,5 @@ app.all('*', remixHandler);
|
||||||
|
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
server.listen(port, () =>
|
server.listen(port, () =>
|
||||||
console.log(`Express server listening at http${isProduction ? '' : 's'}://localhost:${port}`)
|
console.log(`Express server listening at http${isInsecure ? '' : 's'}://localhost:${port}`)
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Hotbar from '@/react-components/hotbar.jsx';
|
||||||
import DomDecorator from './dom-decorator.jsx';
|
import DomDecorator from './dom-decorator.jsx';
|
||||||
|
|
||||||
const slots = Array(10).fill({});
|
const slots = Array(10).fill({});
|
||||||
slots[2] = {qty: 24, source: '/assets/potion'};
|
slots[2] = {qty: 24, icon: '/assets/potion/icon.png'};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Dom/Inventory/Hotbar',
|
title: 'Dom/Inventory/Hotbar',
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
args: {
|
args: {
|
||||||
source: '/assets/potion',
|
icon: '/assets/potion/icon.png',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user