Compare commits
9 Commits
6b60877711
...
71d35bd228
Author | SHA1 | Date | |
---|---|---|---|
|
71d35bd228 | ||
|
cf4d21db1f | ||
|
ff17dd883a | ||
|
0bc8d05706 | ||
|
3a37cf4938 | ||
|
ebc4ebd957 | ||
|
16fa5666a0 | ||
|
3964278ca0 | ||
|
cf04455725 |
|
@ -1,3 +0,0 @@
|
|||
# ASTRide
|
||||
|
||||
Ride your AST :)
|
|
@ -1,50 +0,0 @@
|
|||
const evaluators = Object.fromEntries(
|
||||
Object.entries(
|
||||
import.meta.glob(
|
||||
['./evaluators/*.js', '!./evaluators/*.test.js'],
|
||||
{eager: true, import: 'default'},
|
||||
)
|
||||
)
|
||||
.map(([path, evaluator]) => ([
|
||||
path.replace(/\.\/evaluators\/(.*)\.js/, '$1'),
|
||||
evaluator,
|
||||
])),
|
||||
);
|
||||
|
||||
export default function evaluate(node, {scope} = {}) {
|
||||
switch (node.type) {
|
||||
case 'ArrayExpression':
|
||||
return evaluators.array(node, {evaluate, scope});
|
||||
case 'AssignmentExpression':
|
||||
return evaluators.assignment(node, {evaluate, scope});
|
||||
case 'AwaitExpression':
|
||||
return evaluators.await(node, {evaluate, scope});
|
||||
case 'BinaryExpression':
|
||||
return evaluators.binary(node, {evaluate, scope});
|
||||
case 'Literal':
|
||||
return evaluators.literal(node, {evaluate, scope});
|
||||
case 'CallExpression':
|
||||
return evaluators.call(node, {evaluate, scope});
|
||||
case 'ChainExpression':
|
||||
return evaluate(node.expression, {evaluate, scope});
|
||||
case 'ConditionalExpression':
|
||||
return evaluators.conditional(node, {evaluate, scope});
|
||||
case 'Identifier':
|
||||
return evaluators.identifier(node, {evaluate, scope});
|
||||
case 'LogicalExpression':
|
||||
return evaluators.binary(node, {evaluate, scope});
|
||||
case 'MemberExpression':
|
||||
return evaluators.member(node, {evaluate, scope});
|
||||
case 'NewExpression':
|
||||
return evaluators.new(node, {evaluate, scope});
|
||||
case 'ObjectExpression':
|
||||
return evaluators.object(node, {evaluate, scope});
|
||||
case 'UnaryExpression':
|
||||
return evaluators.unary(node, {evaluate, scope});
|
||||
case 'UpdateExpression':
|
||||
return evaluators.update(node, {evaluate, scope});
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new EvalError(`astride: Can't evaluate node of type ${node.type}`)
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const elements = [];
|
||||
const asyncSpread = Object.create(null);
|
||||
let isAsync = false;
|
||||
for (const index in node.elements) {
|
||||
const element = node.elements[index];
|
||||
if ('SpreadElement' === element.type) {
|
||||
const {async, value} = evaluate(element.argument, {scope});
|
||||
isAsync = isAsync || async;
|
||||
if (async) {
|
||||
elements.push(value);
|
||||
asyncSpread[elements.length - 1] = true;
|
||||
}
|
||||
else {
|
||||
elements.push(...value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const {async, value} = evaluate(element, {scope});
|
||||
isAsync = isAsync || async;
|
||||
elements.push(value);
|
||||
}
|
||||
}
|
||||
return {
|
||||
async: !!isAsync,
|
||||
value: !isAsync
|
||||
? elements
|
||||
: Promise.all(elements).then((elementsAndOrSpreads) => {
|
||||
const elements = [];
|
||||
for (let i = 0; i < elementsAndOrSpreads.length; ++i) {
|
||||
if (asyncSpread[i]) {
|
||||
elements.push(...elementsAndOrSpreads[i]);
|
||||
}
|
||||
else {
|
||||
elements.push(elementsAndOrSpreads[i]);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates array of literals', async () => {
|
||||
expect(evaluate(await expression('[1.5, 2, "three"]')))
|
||||
.to.deep.include({value: [1.5, 2, 'three']});
|
||||
const evaluated = evaluate(await expression('[1.5, 2, await "three"]'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal([1.5, 2, 'three']);
|
||||
});
|
||||
|
||||
test('evaluates array spread', async () => {
|
||||
expect(evaluate(await expression('[...[4, 5, 6], 1.5, 2, "three"]')))
|
||||
.to.deep.include({value: [4, 5, 6, 1.5, 2, 'three']});
|
||||
const evaluated = evaluate(await expression('[...(await [4, 5, 6]), 1.5, 2, await "three"]'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal([4, 5, 6, 1.5, 2, 'three']);
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const {operator, left} = node;
|
||||
const right = evaluate(node.right, {scope});
|
||||
if (!('MemberExpression' === left.type)) {
|
||||
const assign = (value) => {
|
||||
switch (operator) {
|
||||
case '=' : return scope.set(left.name, value);
|
||||
case '+=' : return scope.set(left.name, scope.get(left.name) + value);
|
||||
case '-=' : return scope.set(left.name, scope.get(left.name) - value);
|
||||
case '*=' : return scope.set(left.name, scope.get(left.name) * value);
|
||||
case '/=' : return scope.set(left.name, scope.get(left.name) / value);
|
||||
case '%=' : return scope.set(left.name, scope.get(left.name) % value);
|
||||
case '**=' : return scope.set(left.name, scope.get(left.name) ** value);
|
||||
case '<<=' : return scope.set(left.name, scope.get(left.name) << value);
|
||||
case '>>=' : return scope.set(left.name, scope.get(left.name) >> value);
|
||||
case '>>>=': return scope.set(left.name, scope.get(left.name) >>> value);
|
||||
case '|=' : return scope.set(left.name, scope.get(left.name) | value);
|
||||
case '^=' : return scope.set(left.name, scope.get(left.name) ^ value);
|
||||
case '&=' : return scope.set(left.name, scope.get(left.name) & value);
|
||||
case '||=' : return scope.set(left.name, scope.get(left.name) || value);
|
||||
case '&&=' : return scope.set(left.name, scope.get(left.name) && value);
|
||||
case '??=' : {
|
||||
const l = scope.get(left.name);
|
||||
return scope.set(left.name, (l === null || l === undefined) ? value : l);
|
||||
}
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new Error(`operator not implemented: ${node.operator}`);
|
||||
}
|
||||
};
|
||||
if (right.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise.resolve(right.value).then(assign),
|
||||
};
|
||||
}
|
||||
return {value: assign(right.value)};
|
||||
}
|
||||
const {
|
||||
computed,
|
||||
object,
|
||||
property,
|
||||
} = left;
|
||||
const memberAssign = (O, P, value) => {
|
||||
switch (operator) {
|
||||
case '=' : return O[P] = value;
|
||||
case '+=' : return O[P] += value;
|
||||
case '-=' : return O[P] -= value;
|
||||
case '*=' : return O[P] *= value;
|
||||
case '/=' : return O[P] /= value;
|
||||
case '%=' : return O[P] %= value;
|
||||
case '**=' : return O[P] **= value;
|
||||
case '<<=' : return O[P] <<= value;
|
||||
case '>>=' : return O[P] >>= value;
|
||||
case '>>>=': return O[P] >>>= value;
|
||||
case '|=' : return O[P] |= value;
|
||||
case '^=' : return O[P] ^= value;
|
||||
case '&=' : return O[P] &= value;
|
||||
case '||=' : return O[P] ||= value;
|
||||
case '&&=' : return O[P] &&= value;
|
||||
case '??=' : return O[P] = (O[P] === null || O[P] === undefined) ? value : O[P];
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new Error(`operator not implemented: ${node.operator}`);
|
||||
}
|
||||
};
|
||||
const makeAsync = (O, P, value) => (
|
||||
Promise.all([O, P, value]).then(([O, P, value]) => memberAssign(O, P, value))
|
||||
);
|
||||
const O = evaluate(object, {scope});
|
||||
const P = computed
|
||||
? evaluate(property, {scope})
|
||||
// Otherwise, identifier
|
||||
: {value: property.name};
|
||||
if (right.async || O.async || P.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: makeAsync(O.value, P.value, right.value),
|
||||
};
|
||||
}
|
||||
return {value: memberAssign(O.value, P.value, right.value)};
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
scopeTest('evaluates =', async ({scope}) => {
|
||||
expect(evaluate(await expression('x = 4'), {scope}))
|
||||
.to.deep.include({value: 4});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(4);
|
||||
expect(evaluate(await expression('O.x = 8'), {scope}))
|
||||
.to.deep.include({value: 8});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(8);
|
||||
expect(evaluate(await expression('O["y"] = 16'), {scope}))
|
||||
.to.deep.include({value: 16});
|
||||
expect(scope.get('O').y)
|
||||
.to.equal(16);
|
||||
});
|
||||
|
||||
scopeTest('evaluates +=', async ({scope}) => {
|
||||
scope.set('x', 1);
|
||||
expect(evaluate(await expression('x += 4'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(5);
|
||||
scope.set('O', {x: 1});
|
||||
expect(evaluate(await expression('O.x += 4'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(5);
|
||||
});
|
||||
|
||||
scopeTest('evaluates -=', async ({scope}) => {
|
||||
scope.set('x', 5);
|
||||
expect(evaluate(await expression('x -= 4'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(1);
|
||||
scope.set('O', {x: 5});
|
||||
expect(evaluate(await expression('O.x -= 4'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
scopeTest('evaluates *=', async ({scope}) => {
|
||||
scope.set('x', 5);
|
||||
expect(evaluate(await expression('x *= 4'), {scope}))
|
||||
.to.deep.include({value: 20});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(20);
|
||||
scope.set('O', {x: 5});
|
||||
expect(evaluate(await expression('O.x *= 4'), {scope}))
|
||||
.to.deep.include({value: 20});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(20);
|
||||
});
|
||||
|
||||
scopeTest('evaluates /=', async ({scope}) => {
|
||||
scope.set('x', 25);
|
||||
expect(evaluate(await expression('x /= 5'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(5);
|
||||
scope.set('O', {x: 25});
|
||||
expect(evaluate(await expression('O.x /= 5'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(5);
|
||||
});
|
||||
|
||||
scopeTest('evaluates %=', async ({scope}) => {
|
||||
scope.set('x', 5);
|
||||
expect(evaluate(await expression('x %= 2'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(1);
|
||||
scope.set('O', {x: 5});
|
||||
expect(evaluate(await expression('O.x %= 2'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
scopeTest('evaluates **=', async ({scope}) => {
|
||||
scope.set('x', 5);
|
||||
expect(evaluate(await expression('x **= 3'), {scope}))
|
||||
.to.deep.include({value: 125});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(125);
|
||||
scope.set('O', {x: 5});
|
||||
expect(evaluate(await expression('O.x **= 3'), {scope}))
|
||||
.to.deep.include({value: 125});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(125);
|
||||
});
|
||||
|
||||
scopeTest('evaluates <<=', async ({scope}) => {
|
||||
scope.set('x', 2);
|
||||
expect(evaluate(await expression('x <<= 1'), {scope}))
|
||||
.to.deep.include({value: 4});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(4);
|
||||
scope.set('O', {x: 2});
|
||||
expect(evaluate(await expression('O.x <<= 1'), {scope}))
|
||||
.to.deep.include({value: 4});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(4);
|
||||
});
|
||||
|
||||
scopeTest('evaluates >>=', async ({scope}) => {
|
||||
scope.set('x', 8);
|
||||
expect(evaluate(await expression('x >>= 2'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(2);
|
||||
scope.set('O', {x: 8});
|
||||
expect(evaluate(await expression('O.x >>= 2'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(2);
|
||||
});
|
||||
|
||||
scopeTest('evaluates >>>=', async ({scope}) => {
|
||||
scope.set('x', -1);
|
||||
expect(evaluate(await expression('x >>>= 1'), {scope}))
|
||||
.to.deep.include({value: 2147483647});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(2147483647);
|
||||
scope.set('O', {x: -1});
|
||||
expect(evaluate(await expression('O.x >>>= 1'), {scope}))
|
||||
.to.deep.include({value: 2147483647});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(2147483647);
|
||||
});
|
||||
|
||||
scopeTest('evaluates |=', async ({scope}) => {
|
||||
scope.set('x', 3);
|
||||
expect(evaluate(await expression('x |= 5'), {scope}))
|
||||
.to.deep.include({value: 7});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(7);
|
||||
scope.set('O', {x: 3});
|
||||
expect(evaluate(await expression('O.x |= 5'), {scope}))
|
||||
.to.deep.include({value: 7});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(7);
|
||||
});
|
||||
|
||||
scopeTest('evaluates ^=', async ({scope}) => {
|
||||
scope.set('x', 7);
|
||||
expect(evaluate(await expression('x ^= 2'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(5);
|
||||
scope.set('O', {x: 7});
|
||||
expect(evaluate(await expression('O.x ^= 2'), {scope}))
|
||||
.to.deep.include({value: 5});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(5);
|
||||
});
|
||||
|
||||
scopeTest('evaluates &=', async ({scope}) => {
|
||||
scope.set('x', 5);
|
||||
expect(evaluate(await expression('x &= 3'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(1);
|
||||
scope.set('O', {x: 5});
|
||||
expect(evaluate(await expression('O.x &= 3'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
scopeTest('evaluates ||=', async ({scope}) => {
|
||||
scope.set('x', false);
|
||||
expect(evaluate(await expression('x ||= true'), {scope}))
|
||||
.to.deep.include({value: true});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(true);
|
||||
scope.set('O', {x: false});
|
||||
expect(evaluate(await expression('O.x ||= true'), {scope}))
|
||||
.to.deep.include({value: true});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(true);
|
||||
});
|
||||
|
||||
scopeTest('evaluates &&=', async ({scope}) => {
|
||||
scope.set('x', true);
|
||||
expect(evaluate(await expression('x &&= true'), {scope}))
|
||||
.to.deep.include({value: true});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(true);
|
||||
expect(evaluate(await expression('x &&= false'), {scope}))
|
||||
.to.deep.include({value: false});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(false);
|
||||
scope.set('O', {x: true});
|
||||
expect(evaluate(await expression('O.x &&= true'), {scope}))
|
||||
.to.deep.include({value: true});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(true);
|
||||
expect(evaluate(await expression('O.x &&= false'), {scope}))
|
||||
.to.deep.include({value: false});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(false);
|
||||
});
|
||||
|
||||
scopeTest('evaluates ??=', async ({scope}) => {
|
||||
scope.set('x', null);
|
||||
expect(evaluate(await expression('x ??= 2'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(2);
|
||||
expect(evaluate(await expression('x ??= 4'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('x'))
|
||||
.to.equal(2);
|
||||
scope.set('O', {x: null});
|
||||
expect(evaluate(await expression('O.x ??= 2'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(2);
|
||||
expect(evaluate(await expression('O.x ??= 4'), {scope}))
|
||||
.to.deep.include({value: 2});
|
||||
expect(scope.get('O').x)
|
||||
.to.equal(2);
|
||||
});
|
||||
|
||||
scopeTest('evaluates promised assignment', async ({scope}) => {
|
||||
const evaluated = evaluate(await expression('x = await 4'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(4);
|
||||
expect(await scope.get('x'))
|
||||
.to.equal(4);
|
||||
const evaluatedComputedObject = evaluate(await expression('O["x"] = await 4'), {scope});
|
||||
expect(evaluatedComputedObject.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluatedComputedObject.value)
|
||||
.to.equal(4);
|
||||
expect(await scope.get('O').x)
|
||||
.to.equal(4);
|
||||
const evaluatedObject = evaluate(await expression('O.x = await 4'), {scope});
|
||||
expect(evaluatedObject.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluatedObject.value)
|
||||
.to.equal(4);
|
||||
expect(await scope.get('O').x)
|
||||
.to.equal(4);
|
||||
const evaluatedPromisedObject = evaluate(await expression('(await O).x = await 4'), {scope});
|
||||
expect(evaluatedPromisedObject.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluatedPromisedObject.value)
|
||||
.to.equal(4);
|
||||
expect(await scope.get('O').x)
|
||||
.to.equal(4);
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const {value} = evaluate(node.argument, {scope});
|
||||
return {
|
||||
async: true,
|
||||
value: value instanceof Promise ? value : Promise.resolve(value),
|
||||
};
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates await expressions', async () => {
|
||||
const evaluated = evaluate(await expression('await 1'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(1);
|
||||
});
|
||||
|
||||
test('coalesces promises', async () => {
|
||||
const evaluated = evaluate(await expression('await await await 1'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(1);
|
||||
});
|
|
@ -1,48 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const binary = (left, right) => {
|
||||
switch (node.operator) {
|
||||
case '+' : return left + right;
|
||||
case '-' : return left - right;
|
||||
case '/' : return left / right;
|
||||
case '%' : return left % right;
|
||||
case '*' : return left * right;
|
||||
case '>' : return left > right;
|
||||
case '<' : return left < right;
|
||||
case 'in' : return left in right;
|
||||
case '>=' : return left >= right;
|
||||
case '<=' : return left <= right;
|
||||
case '**' : return left ** right;
|
||||
case '===': return left === right;
|
||||
case '!==': return left !== right;
|
||||
case '^' : return left ^ right;
|
||||
case '&' : return left & right;
|
||||
case '|' : return left | right;
|
||||
case '>>' : return left >> right;
|
||||
case '<<' : return left << right;
|
||||
case '>>>': return left >>> right;
|
||||
case '==' : return left == right;
|
||||
case '!=' : return left != right;
|
||||
case '||' : return left || right;
|
||||
case '&&' : return left && right;
|
||||
case '??' : return (left === null || left === undefined) ? right : left;
|
||||
case 'instanceof': return left instanceof right;
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new Error(`operator not implemented: ${node.operator}`);
|
||||
}
|
||||
};
|
||||
const left = evaluate(node.left, {scope});
|
||||
const right = evaluate(node.right, {scope});
|
||||
if (left.async || right.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise
|
||||
.all([left.value, right.value])
|
||||
.then(([left, right]) => binary(left, right)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
async: false,
|
||||
value: binary(left.value, right.value),
|
||||
};
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates +', async () => {
|
||||
expect(evaluate(await expression('10 + 2')))
|
||||
.to.deep.include({value: 12});
|
||||
});
|
||||
|
||||
test('evaluates -', async () => {
|
||||
expect(evaluate(await expression('10 - 2')))
|
||||
.to.deep.include({value: 8});
|
||||
});
|
||||
|
||||
test('evaluates /', async () => {
|
||||
expect(evaluate(await expression('10 / 2')))
|
||||
.to.deep.include({value: 5});
|
||||
});
|
||||
|
||||
test('evaluates %', async () => {
|
||||
expect(evaluate(await expression('10 % 3')))
|
||||
.to.deep.include({value: 1});
|
||||
});
|
||||
|
||||
test('evaluates *', async () => {
|
||||
expect(evaluate(await expression('10 * 2')))
|
||||
.to.deep.include({value: 20});
|
||||
});
|
||||
|
||||
test('evaluates >', async () => {
|
||||
expect(evaluate(await expression('10 > 2')))
|
||||
.to.deep.include({value: true});
|
||||
});
|
||||
|
||||
test('evaluates <', async () => {
|
||||
expect(evaluate(await expression('10 < 2')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates in', async () => {
|
||||
expect(evaluate(await expression('"i" in {i: 69}')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('"j" in {i: 69}')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates >=', async () => {
|
||||
expect(evaluate(await expression('10 >= 2')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('2 >= 2')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('1 >= 2')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates <=', async () => {
|
||||
expect(evaluate(await expression('10 <= 2')))
|
||||
.to.deep.include({value: false});
|
||||
expect(evaluate(await expression('2 <= 2')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('1 <= 2')))
|
||||
.to.deep.include({value: true});
|
||||
});
|
||||
|
||||
test('evaluates **', async () => {
|
||||
expect(evaluate(await expression('2 ** 16')))
|
||||
.to.deep.include({value: 65536});
|
||||
});
|
||||
|
||||
test('evaluates ===', async () => {
|
||||
expect(evaluate(await expression('10 === "10"')))
|
||||
.to.deep.include({value: false});
|
||||
expect(evaluate(await expression('10 === 10')))
|
||||
.to.deep.include({value: true});
|
||||
});
|
||||
|
||||
test('evaluates !==', async () => {
|
||||
expect(evaluate(await expression('10 !== "10"')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('10 !== 10')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates ^', async () => {
|
||||
expect(evaluate(await expression('7 ^ 2')))
|
||||
.to.deep.include({value: 5});
|
||||
});
|
||||
|
||||
test('evaluates &', async () => {
|
||||
expect(evaluate(await expression('5 & 3')))
|
||||
.to.deep.include({value: 1});
|
||||
});
|
||||
|
||||
test('evaluates |', async () => {
|
||||
expect(evaluate(await expression('1 | 2')))
|
||||
.to.deep.include({value: 3});
|
||||
});
|
||||
|
||||
test('evaluates >>', async () => {
|
||||
expect(evaluate(await expression('8 >> 1')))
|
||||
.to.deep.include({value: 4});
|
||||
});
|
||||
|
||||
test('evaluates <<', async () => {
|
||||
expect(evaluate(await expression('2 << 1')))
|
||||
.to.deep.include({value: 4});
|
||||
});
|
||||
|
||||
test('evaluates >>>', async () => {
|
||||
expect(evaluate(await expression('-1 >>> 1')))
|
||||
.to.deep.include({value: 2147483647});
|
||||
});
|
||||
|
||||
test('evaluates ==', async () => {
|
||||
expect(evaluate(await expression('10 == "10"')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('10 == 10')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('10 == "ten"')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates !=', async () => {
|
||||
expect(evaluate(await expression('10 != "10"')))
|
||||
.to.deep.include({value: false});
|
||||
expect(evaluate(await expression('10 != 10')))
|
||||
.to.deep.include({value: false});
|
||||
expect(evaluate(await expression('10 != "ten"')))
|
||||
.to.deep.include({value: true});
|
||||
});
|
||||
|
||||
test('evaluates ||', async () => {
|
||||
expect(evaluate(await expression('true || true')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('true || false')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('false || false')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates &&', async () => {
|
||||
expect(evaluate(await expression('true && true')))
|
||||
.to.deep.include({value: true});
|
||||
expect(evaluate(await expression('true && false')))
|
||||
.to.deep.include({value: false});
|
||||
expect(evaluate(await expression('false && false')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates ??', async () => {
|
||||
const scope = {
|
||||
get() { return undefined; },
|
||||
};
|
||||
expect(evaluate(await expression('null ?? 1')))
|
||||
.to.deep.include({value: 1});
|
||||
expect(evaluate(await expression('undefined ?? 1'), {scope}))
|
||||
.to.deep.include({value: 1});
|
||||
expect(evaluate(await expression('2 ?? 1')))
|
||||
.to.deep.include({value: 2});
|
||||
});
|
||||
|
||||
test('evaluates instanceof', async () => {
|
||||
const scope = {
|
||||
get() { return Object; },
|
||||
};
|
||||
expect(evaluate(await expression('({}) instanceof Object'), {scope}))
|
||||
.to.deep.include({value: true});
|
||||
});
|
||||
|
||||
test('evaluates promised expressions', async () => {
|
||||
const evaluated = evaluate(await expression('(await 1) + (await 2)'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(3);
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
import fastCall from '@/util/fast-call.js';
|
||||
|
||||
export default function(node, {evaluate, scope}) {
|
||||
let asyncArgs = false;
|
||||
const args = [];
|
||||
for (let i = 0; i < node.arguments.length; i++) {
|
||||
const arg = node.arguments[i];
|
||||
const {async, value} = evaluate(arg, {scope});
|
||||
asyncArgs ||= async;
|
||||
args.push(value);
|
||||
}
|
||||
const {callee} = node;
|
||||
const {
|
||||
computed,
|
||||
object,
|
||||
property,
|
||||
} = callee;
|
||||
const invoke = (fn, holder, args) => {
|
||||
if (node.optional && !fn) {
|
||||
return undefined;
|
||||
}
|
||||
return fastCall(fn, holder, args);
|
||||
};
|
||||
if (!('MemberExpression' === callee.type)) {
|
||||
const {async, value} = evaluate(callee, {scope});
|
||||
if (asyncArgs || async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise
|
||||
.all([value, Promise.all(args)])
|
||||
.then(([callee, args]) => invoke(callee, undefined, args)),
|
||||
};
|
||||
}
|
||||
return {value: invoke(value, undefined, args)};
|
||||
}
|
||||
const O = evaluate(object, {scope});
|
||||
const P = computed
|
||||
? evaluate(property, {scope})
|
||||
// Otherwise, identifier.
|
||||
: {value: property.name};
|
||||
if (asyncArgs || O.async || P.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise
|
||||
.all([O.value, P.value, Promise.all(args)])
|
||||
.then(([O, P, args]) => invoke(callee.optional ? O?.[P] : O[P], O, args)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
async: false,
|
||||
value: invoke(callee.optional ? O.value?.[P.value] : O.value[P.value], O.value, args),
|
||||
};
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
scopeTest('evaluates calls', async ({scope}) => {
|
||||
scope.set('f', (...args) => args.reduce((l, r) => l + r, 0));
|
||||
const evaluated = evaluate(await expression('f(1, 2, 3)'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(6);
|
||||
});
|
||||
|
||||
scopeTest('evaluates async calls', async ({scope}) => {
|
||||
const f = (...args) => args.reduce((l, r) => l + r, 0);
|
||||
scope.set('f', f);
|
||||
scope.set('O', {f});
|
||||
const evaluated = evaluate(await expression('f(await 1, 2, 3)'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(6);
|
||||
const evaluatedOptional = evaluate(await expression('O?.f(await 1, 2, 3)'), {scope});
|
||||
expect(evaluatedOptional.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluatedOptional.value)
|
||||
.to.equal(6);
|
||||
});
|
||||
|
||||
scopeTest('evaluates member calls', async ({scope}) => {
|
||||
scope.set('O', {f: (...args) => args.reduce((l, r) => l + r, 0)});
|
||||
expect(evaluate(await expression('O.f(1, 2, 3)'), {scope}).value)
|
||||
.to.equal(6);
|
||||
expect(evaluate(await expression('O["f"](1, 2, 3)'), {scope}).value)
|
||||
.to.equal(6);
|
||||
});
|
||||
|
||||
scopeTest('evaluates optional calls', async ({scope}) => {
|
||||
scope.set('O', {});
|
||||
expect(evaluate(await expression('g?.(1, 2, 3)'), {scope}).value)
|
||||
.to.equal(undefined);
|
||||
expect(evaluate(await expression('O?.g?.(1, 2, 3)'), {scope}).value)
|
||||
.to.equal(undefined);
|
||||
});
|
||||
|
||||
scopeTest('evaluates async calls', async ({scope}) => {
|
||||
scope.set('O', {f: (...args) => args.reduce((l, r) => l + r, 0)});
|
||||
const evaluated = evaluate(await expression('O.f(await 1, 2, 3)'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(6);
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const test = evaluate(node.test, {scope});
|
||||
if (test.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: (
|
||||
Promise.resolve(test.value)
|
||||
.then((test) => evaluate(test ? node.consequent : node.alternate).value, {scope})
|
||||
),
|
||||
};
|
||||
}
|
||||
return evaluate(test.value ? node.consequent : node.alternate, {scope});
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
scopeTest('evaluates conditional expression', async ({scope}) => {
|
||||
scope.set('x', true);
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('x ? 2 : 3'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(2);
|
||||
scope.set('x', false);
|
||||
evaluated = evaluate(await expression('x ? 2 : 3'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(3);
|
||||
});
|
||||
|
||||
scopeTest('evaluates async conditional expression', async ({scope}) => {
|
||||
scope.set('x', true);
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('(await x) ? 2 : 3'), {scope});
|
||||
expect(await evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(2);
|
||||
scope.set('x', false);
|
||||
evaluated = evaluate(await expression('(await x) ? 2 : 3'), {scope});
|
||||
expect(await evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(3);
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function(node, {scope}) {
|
||||
return {async: false, value: scope.get(node.name)};
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default function({value}) {
|
||||
return {async: false, value};
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates numeric literals', async () => {
|
||||
expect(evaluate(await expression('1')))
|
||||
.to.deep.include({value: 1});
|
||||
});
|
||||
|
||||
test('evaluates string literals', async () => {
|
||||
expect(evaluate(await expression('"1"')))
|
||||
.to.deep.include({value: '1'});
|
||||
});
|
|
@ -1,16 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const {computed, object, optional, property} = node;
|
||||
const member = (O, P) => (optional ? O?.[P] : O[P]);
|
||||
const O = evaluate(object, {scope});
|
||||
const P = computed
|
||||
? evaluate(property, {scope})
|
||||
// Otherwise, identifier
|
||||
: {value: property.name};
|
||||
if (O.async || P.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
|
||||
};
|
||||
}
|
||||
return {async: false, value: member(O.value, P.value)};
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {x: 32}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
scopeTest('evaluates member expression', async ({scope}) => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('O.x'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(32);
|
||||
});
|
||||
|
||||
scopeTest('evaluates optional member expression', async ({scope}) => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('O?.y'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(undefined);
|
||||
});
|
||||
|
||||
scopeTest('evaluates computed member expression', async ({scope}) => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('O["x"]'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(32);
|
||||
});
|
||||
|
||||
scopeTest('evaluates async member expression', async ({scope}) => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression('O[await "x"]'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(32);
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
let asyncArgs = false;
|
||||
const args = [];
|
||||
for (let i = 0; i < node.arguments.length; i++) {
|
||||
const arg = node.arguments[i];
|
||||
const {async, value} = evaluate(arg, {scope});
|
||||
asyncArgs ||= async;
|
||||
args.push(value);
|
||||
}
|
||||
const {callee} = node;
|
||||
const {async, value} = evaluate(callee, {scope});
|
||||
if (asyncArgs || async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise
|
||||
.all([value, Promise.all(args)])
|
||||
.then(([callee, args]) => new callee(...args)),
|
||||
};
|
||||
}
|
||||
return {value: new value(...args)};
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class C {
|
||||
foo = 'bar';
|
||||
constructor(a, b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
|
||||
scopeTest('creates instances', async ({scope}) => {
|
||||
scope.set('C', C);
|
||||
const evaluated = evaluate(await expression('new C(1, 2)'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
scopeTest('creates instances with async dependencies', async ({scope}) => {
|
||||
scope.set('C', C);
|
||||
scope.set('a', Promise.resolve(1));
|
||||
scope.set('b', Promise.resolve(2));
|
||||
const evaluated = evaluate(await expression('new C(await a, await b)'), {scope});
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const {properties} = node;
|
||||
let isAsync = false;
|
||||
const entries = [];
|
||||
for (let i = 0; i < properties.length; i++) {
|
||||
if ('Property' === properties[i].type) {
|
||||
const {computed, key, value} = properties[i];
|
||||
let k;
|
||||
if (computed) {
|
||||
k = evaluate(key, {scope});
|
||||
}
|
||||
else if ('Identifier' === key.type) {
|
||||
k = {value: key.name};
|
||||
}
|
||||
else if ('Literal' === key.type) {
|
||||
k = {value: key.value};
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
else {
|
||||
throw new Error(`property key type ${key.type} not implemented`);
|
||||
}
|
||||
const v = evaluate(value, {scope});
|
||||
isAsync ||= k.async || v.async;
|
||||
if (k.async || v.async) {
|
||||
entries.push(Promise.all([k.value, v.value]));
|
||||
}
|
||||
else {
|
||||
entries.push([k.value, v.value]);
|
||||
}
|
||||
}
|
||||
if ('SpreadElement' === properties[i].type) {
|
||||
const {argument} = properties[i];
|
||||
const spreading = evaluate(argument, {scope});
|
||||
isAsync ||= spreading.async;
|
||||
if (spreading.async) {
|
||||
entries.push(Promise.resolve(spreading.value).then((spreading) => {
|
||||
const entries = [];
|
||||
const keys = Object.keys(spreading);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
entries.push([key, spreading[key]]);
|
||||
}
|
||||
return entries;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
const keys = Object.keys(spreading.value);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i];
|
||||
entries.push([key, spreading.value[key]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
async: !!isAsync,
|
||||
value: isAsync
|
||||
? Promise.all(entries)
|
||||
.then((entries) => {
|
||||
const flat = [];
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const entry = entries[i];
|
||||
if (Array.isArray(entry[0])) {
|
||||
for (let j = 0; j < entry.length; j++) {
|
||||
flat.push(entry[j]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
flat.push(entry);
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(flat);
|
||||
})
|
||||
: Object.fromEntries(entries),
|
||||
};
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates object expression', async () => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression(`({
|
||||
["foo"]: 16,
|
||||
bar: 32,
|
||||
'baz': 64,
|
||||
})`));
|
||||
expect(evaluated.value)
|
||||
.to.deep.equal({
|
||||
foo: 16,
|
||||
bar: 32,
|
||||
baz: 64,
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluates async object expression', async () => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression(`({
|
||||
foo: await 32,
|
||||
})`));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal({
|
||||
foo: 32,
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluates object spread expression', async () => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression(`({
|
||||
foo: 16,
|
||||
...({bar: 32}),
|
||||
})`));
|
||||
expect(evaluated.value)
|
||||
.to.deep.equal({
|
||||
foo: 16,
|
||||
bar: 32,
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluates async spread expression', async () => {
|
||||
let evaluated;
|
||||
evaluated = evaluate(await expression(`({
|
||||
foo: 16,
|
||||
...(await {bar: 32}),
|
||||
})`));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.deep.equal({
|
||||
foo: 16,
|
||||
bar: 32,
|
||||
});
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const unary = (arg) => {
|
||||
switch (node.operator) {
|
||||
case '+' : return +arg;
|
||||
case '-' : return -arg;
|
||||
case '!' : return !arg;
|
||||
case '~' : return ~arg;
|
||||
case 'typeof': return typeof arg;
|
||||
case 'void' : return undefined;
|
||||
// case 'delete': ...
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new Error(`operator not implemented: ${node.operator}`);
|
||||
}
|
||||
};
|
||||
const arg = evaluate(node.argument, {scope});
|
||||
if (arg.async) {
|
||||
return {
|
||||
async: true,
|
||||
value: Promise.resolve(arg.value).then(unary),
|
||||
};
|
||||
}
|
||||
return {async: false, value: unary(arg.value)};
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
test('evaluates +', async () => {
|
||||
expect(evaluate(await expression('+1')))
|
||||
.to.deep.include({value: 1});
|
||||
});
|
||||
|
||||
test('evaluates -', async () => {
|
||||
expect(evaluate(await expression('-1')))
|
||||
.to.deep.include({value: -1});
|
||||
});
|
||||
|
||||
test('evaluates !', async () => {
|
||||
expect(evaluate(await expression('!true')))
|
||||
.to.deep.include({value: false});
|
||||
});
|
||||
|
||||
test('evaluates ~', async () => {
|
||||
expect(evaluate(await expression('~1')))
|
||||
.to.deep.include({value: -2});
|
||||
});
|
||||
|
||||
test('evaluates typeof', async () => {
|
||||
expect(evaluate(await expression('typeof "a"')))
|
||||
.to.deep.include({value: 'string'});
|
||||
});
|
||||
|
||||
test('evaluates void', async () => {
|
||||
expect(evaluate(await expression('void 0')))
|
||||
.to.deep.include({value: undefined});
|
||||
});
|
||||
|
||||
test('evaluates promised unary expression', async () => {
|
||||
const evaluated = evaluate(await expression('-(await 4)'));
|
||||
expect(evaluated.async)
|
||||
.to.equal(true);
|
||||
expect(await evaluated.value)
|
||||
.to.equal(-4);
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
export default function(node, {evaluate, scope}) {
|
||||
const {argument, operator, prefix} = node;
|
||||
const {value} = evaluate(argument, {scope});
|
||||
const update = (value) => {
|
||||
if (prefix) {
|
||||
switch (operator) {
|
||||
case '++': return scope.set(argument.name, value + 1);
|
||||
case '--': return scope.set(argument.name, value - 1);
|
||||
}
|
||||
}
|
||||
switch (operator) {
|
||||
case '++':
|
||||
scope.set(argument.name, value + 1);
|
||||
return value;
|
||||
case '--':
|
||||
scope.set(argument.name, value - 1);
|
||||
return value;
|
||||
}
|
||||
/* v8 ignore next */
|
||||
throw new Error(`operator not implemented: ${operator}`);
|
||||
};
|
||||
return {async: false, value: update(value)};
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import {expect, test} from 'vitest';
|
||||
|
||||
import evaluate from '@/astride/evaluate.js';
|
||||
import expression from '@/astride/test/expression.js';
|
||||
|
||||
const scopeTest = test.extend({
|
||||
scope: async ({}, use) => {
|
||||
await use({
|
||||
S: {O: {}},
|
||||
get(k) { return this.S[k]; },
|
||||
set(k, v) { return this.S[k] = v; }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
scopeTest('evaluates postfix updates', async ({scope}) => {
|
||||
scope.set('x', 4);
|
||||
let evaluated = evaluate(await expression('y = x++'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(4);
|
||||
expect(scope.get('x'))
|
||||
.to.equal(5);
|
||||
expect(scope.get('y'))
|
||||
.to.equal(4);
|
||||
evaluated = evaluate(await expression('y = x--'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(5);
|
||||
expect(scope.get('x'))
|
||||
.to.equal(4);
|
||||
expect(scope.get('y'))
|
||||
.to.equal(5);
|
||||
});
|
||||
|
||||
scopeTest('evaluates prefix updates', async ({scope}) => {
|
||||
scope.set('x', 4);
|
||||
let evaluated = evaluate(await expression('y = ++x'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(5);
|
||||
expect(scope.get('x'))
|
||||
.to.equal(5);
|
||||
expect(scope.get('y'))
|
||||
.to.equal(5);
|
||||
evaluated = evaluate(await expression('y = --x'), {scope});
|
||||
expect(evaluated.value)
|
||||
.to.equal(4);
|
||||
expect(scope.get('x'))
|
||||
.to.equal(4);
|
||||
expect(scope.get('y'))
|
||||
.to.equal(4);
|
||||
});
|
|
@ -1,736 +0,0 @@
|
|||
import evaluate from '@/astride/evaluate.js';
|
||||
import Scope from '@/astride/scope.js';
|
||||
import traverse from '@/astride/traverse.js';
|
||||
|
||||
const YIELD_NONE = 0;
|
||||
const YIELD_PROMISE = 1;
|
||||
const YIELD_LOOP_UPDATE = 2;
|
||||
const YIELD_RETURN = 3;
|
||||
const YIELD_BREAK = 4;
|
||||
|
||||
export default class Sandbox {
|
||||
|
||||
ast;
|
||||
$$execution;
|
||||
scopes;
|
||||
|
||||
constructor(ast, context = {}) {
|
||||
this.ast = ast;
|
||||
this.$$context = context;
|
||||
this.compile();
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new this.constructor(
|
||||
this.ast,
|
||||
{
|
||||
...this.$$context,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
compile() {
|
||||
let scope = new Scope();
|
||||
scope.context = this.$$context;
|
||||
this.scopes = new Map([[this.ast, scope]]);
|
||||
traverse(
|
||||
this.ast,
|
||||
(node, verb) => {
|
||||
if (
|
||||
'BlockStatement' === node.type
|
||||
|| 'ForStatement' === node.type
|
||||
|| 'ForOfStatement' === node.type
|
||||
) {
|
||||
switch (verb) {
|
||||
case 'enter': {
|
||||
scope = new Scope(scope);
|
||||
break;
|
||||
}
|
||||
case 'exit': {
|
||||
scope = scope.parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('enter' === verb) {
|
||||
this.scopes.set(node, scope);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get context() {
|
||||
return this.$$context;
|
||||
}
|
||||
|
||||
destructureArray(id, init) {
|
||||
const scope = this.scopes.get(id);
|
||||
const {elements} = id;
|
||||
for (let i = 0; i < elements.length; ++i) {
|
||||
const element = elements[i];
|
||||
if (null === element) {
|
||||
continue;
|
||||
}
|
||||
switch (element.type) {
|
||||
case 'ArrayPattern': {
|
||||
this.destructureArray(element, init[i]);
|
||||
break;
|
||||
}
|
||||
case 'ObjectPattern': {
|
||||
this.destructureObject(element, init[i]);
|
||||
break;
|
||||
}
|
||||
case 'Identifier': {
|
||||
scope.allocate(element.name, init[i]);
|
||||
break;
|
||||
}
|
||||
/* v8 ignore next 2 */
|
||||
default:
|
||||
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
destructureObject(id, init) {
|
||||
const scope = this.scopes.get(id);
|
||||
const {properties} = id;
|
||||
for (let i = 0; i < properties.length; ++i) {
|
||||
const property = properties[i];
|
||||
if ('ObjectPattern' === property.value.type) {
|
||||
this.destructureObject(property.value, init[property.key.name]);
|
||||
}
|
||||
else {
|
||||
scope.allocate(
|
||||
property.value.name,
|
||||
init[property.key.name],
|
||||
);
|
||||
}
|
||||
}
|
||||
return init;
|
||||
}
|
||||
|
||||
evaluate(node) {
|
||||
return evaluate(
|
||||
node,
|
||||
{scope: this.scopes.get(node)},
|
||||
);
|
||||
}
|
||||
|
||||
evaluateToResult(node) {
|
||||
const {async, value} = this.evaluate(node);
|
||||
return {
|
||||
value,
|
||||
yield: async ? YIELD_PROMISE : YIELD_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
executeSync(node, depth) {
|
||||
let result;
|
||||
const isReplaying = depth < this.$$execution.stack.length;
|
||||
const isReplayingThisLevel = depth === this.$$execution.stack.length - 1;
|
||||
if (!isReplaying) {
|
||||
this.$$execution.stack.push(node);
|
||||
}
|
||||
// Substitute executing the node for its deferred result.
|
||||
else if (isReplayingThisLevel) {
|
||||
if (this.$$execution.stack[depth] === node) {
|
||||
result = this.$$execution.deferred.get(node);
|
||||
this.$$execution.deferred.delete(node);
|
||||
this.$$execution.stack.pop();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
switch (node.type) {
|
||||
case 'ArrayExpression':
|
||||
case 'AssignmentExpression':
|
||||
case 'BinaryExpression':
|
||||
case 'CallExpression':
|
||||
case 'ChainExpression':
|
||||
case 'ObjectExpression':
|
||||
case 'Identifier':
|
||||
case 'MemberExpression':
|
||||
case 'NewExpression':
|
||||
case 'UpdateExpression': {
|
||||
result = this.evaluateToResult(node);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'AwaitExpression': {
|
||||
const coerce = !this.$$execution.deferred.has(node.argument);
|
||||
result = this.executeSync(node.argument, depth + 1);
|
||||
if (coerce) {
|
||||
result = {
|
||||
value: result.value,
|
||||
yield: YIELD_PROMISE,
|
||||
};
|
||||
}
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
/* v8 ignore next 2 */
|
||||
break;
|
||||
}
|
||||
case 'LogicalExpression': {
|
||||
const shouldVisitChild = (child) => (
|
||||
isReplaying
|
||||
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||
: true
|
||||
);
|
||||
if (shouldVisitChild(node.left)) {
|
||||
const left = this.executeSync(node.left, depth + 1);
|
||||
if (left.yield) {
|
||||
return left;
|
||||
}
|
||||
this.$$execution.deferred.set(node.left, left);
|
||||
}
|
||||
const left = this.$$execution.deferred.get(node.left);
|
||||
this.$$execution.deferred.delete(node.left);
|
||||
if ('||' === node.operator && left.value) {
|
||||
result = {
|
||||
value: true,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
break;
|
||||
}
|
||||
if ('&&' === node.operator && !left.value) {
|
||||
result = {
|
||||
value: false,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
break;
|
||||
}
|
||||
const right = this.executeSync(node.right, depth + 1);
|
||||
if (right.yield) {
|
||||
return right;
|
||||
}
|
||||
result = {
|
||||
value: !!right.value,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'BlockStatement': {
|
||||
result = {
|
||||
value: undefined,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
let skipping = isReplaying;
|
||||
for (const child of node.body) {
|
||||
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||
skipping = false;
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (skipping) {
|
||||
continue;
|
||||
}
|
||||
result = this.executeSync(child, depth + 1);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'BreakStatement': {
|
||||
walkUp: while (this.$$execution.stack.length > 0) {
|
||||
const frame = this.$$execution.stack[this.$$execution.stack.length - 1];
|
||||
switch (frame.type) {
|
||||
case 'ForStatement': {
|
||||
break walkUp;
|
||||
}
|
||||
case 'ForOfStatement': {
|
||||
break walkUp;
|
||||
}
|
||||
default: this.$$execution.stack.pop();
|
||||
}
|
||||
}
|
||||
return {
|
||||
value: undefined,
|
||||
yield: YIELD_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) {
|
||||
if (YIELD_BREAK === body.yield) {
|
||||
this.$$execution.deferred.delete(node.body);
|
||||
break;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
this.$$execution.deferred.set(node.body, body);
|
||||
}
|
||||
if (shouldVisitChild(node.update)) {
|
||||
const update = this.executeSync(node.update, depth + 1);
|
||||
if (update.yield) {
|
||||
return update;
|
||||
}
|
||||
this.$$execution.deferred.set(node.update, update);
|
||||
}
|
||||
// Yield
|
||||
this.$$execution.stack.push(node);
|
||||
const update = this.$$execution.deferred.get(node.update);
|
||||
this.$$execution.deferred.delete(node.update);
|
||||
return {
|
||||
value: update.value,
|
||||
yield: YIELD_LOOP_UPDATE,
|
||||
};
|
||||
}
|
||||
case 'ForOfStatement': {
|
||||
const shouldVisitChild = (child) => (
|
||||
isReplaying
|
||||
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||
: true
|
||||
);
|
||||
const scope = this.scopes.get(node);
|
||||
if (shouldVisitChild(node.right)) {
|
||||
const right = this.executeSync(node.right, depth + 1);
|
||||
if (right.yield) {
|
||||
return right;
|
||||
}
|
||||
scope.allocate('@@iterator', right.value[Symbol.iterator]());
|
||||
}
|
||||
result = this.$$execution.deferred.get(node.body) || {
|
||||
value: undefined,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
if (shouldVisitChild(node)) {
|
||||
this.$$execution.deferred.set(node.left, scope.get('@@iterator').next());
|
||||
}
|
||||
if (this.$$execution.stack[depth + 1] === node) {
|
||||
this.$$execution.stack.pop();
|
||||
}
|
||||
const {done, value} = this.$$execution.deferred.get(node.left);
|
||||
if (done) {
|
||||
this.$$execution.deferred.delete(node.left);
|
||||
this.$$execution.deferred.delete(node.body);
|
||||
break;
|
||||
}
|
||||
switch (node.left.type) {
|
||||
case 'ArrayPattern': {
|
||||
this.destructureArray(node.left, value)
|
||||
break;
|
||||
}
|
||||
case 'ObjectPattern': {
|
||||
this.destructureObject(node.left, value)
|
||||
break;
|
||||
}
|
||||
case 'VariableDeclaration': {
|
||||
const [declaration] = node.left.declarations;
|
||||
switch (declaration.id.type) {
|
||||
case 'Identifier': {
|
||||
scope.set(declaration.id.name, value);
|
||||
break;
|
||||
}
|
||||
case 'ArrayPattern': {
|
||||
this.destructureArray(declaration.id, value)
|
||||
break;
|
||||
}
|
||||
case 'ObjectPattern': {
|
||||
this.destructureObject(declaration.id, value)
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Identifier': {
|
||||
scope.set(node.left.name, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldVisitChild(node.body)) {
|
||||
const body = this.executeSync(node.body, depth + 1);
|
||||
if (body.yield) {
|
||||
if (YIELD_BREAK === body.yield) {
|
||||
this.$$execution.deferred.delete(node.left);
|
||||
this.$$execution.deferred.delete(node.body);
|
||||
break;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
this.$$execution.deferred.set(node.body, body);
|
||||
}
|
||||
// Yield
|
||||
this.$$execution.stack.push(node);
|
||||
return {
|
||||
value: undefined,
|
||||
yield: YIELD_LOOP_UPDATE,
|
||||
};
|
||||
}
|
||||
case 'IfStatement': {
|
||||
const shouldVisitChild = (child) => (
|
||||
isReplaying
|
||||
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||
: true
|
||||
);
|
||||
result = {
|
||||
value: undefined,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
if (shouldVisitChild(node.test)) {
|
||||
const test = this.executeSync(node.test, depth + 1);
|
||||
if (test.yield) {
|
||||
return test;
|
||||
}
|
||||
this.$$execution.deferred.set(node.test, test);
|
||||
}
|
||||
const test = this.$$execution.deferred.get(node.test);
|
||||
if (test.value) {
|
||||
result = this.executeSync(node.consequent, depth + 1);
|
||||
}
|
||||
else if (node.alternate) {
|
||||
result = this.executeSync(node.alternate, depth + 1);
|
||||
}
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
this.$$execution.deferred.delete(node.test);
|
||||
break;
|
||||
}
|
||||
case 'Literal': {
|
||||
result = {value: node.value, yield: YIELD_NONE};
|
||||
break;
|
||||
}
|
||||
case 'Program': {
|
||||
result = {value: undefined, yield: YIELD_NONE};
|
||||
let skipping = isReplaying;
|
||||
for (const child of node.body) {
|
||||
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||
skipping = false;
|
||||
}
|
||||
if (skipping) {
|
||||
continue;
|
||||
}
|
||||
result = this.executeSync(child, depth + 1);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result = {value: result.value, yield: YIELD_RETURN};
|
||||
break;
|
||||
}
|
||||
case 'ReturnStatement': {
|
||||
if (!node.argument) {
|
||||
return {
|
||||
value: undefined,
|
||||
yield: YIELD_RETURN,
|
||||
};
|
||||
}
|
||||
const argument = this.executeSync(node.argument, depth + 1);
|
||||
if (argument.yield) {
|
||||
return argument;
|
||||
}
|
||||
result = {
|
||||
value: argument.value,
|
||||
yield: YIELD_RETURN,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
if ('delete' === node.operator) {
|
||||
let property;
|
||||
if (node.argument.computed) {
|
||||
property = this.executeSync(node.argument.property, depth + 1);
|
||||
if (property.yield) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
else {
|
||||
property = {value: node.argument.property.name, yield: YIELD_NONE};
|
||||
}
|
||||
const scope = this.scopes.get(node);
|
||||
const object = scope.get(node.argument.object.name, undefined);
|
||||
delete object[property.value];
|
||||
result = {value: true, yield: YIELD_NONE};
|
||||
}
|
||||
else {
|
||||
result = this.evaluateToResult(node);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'VariableDeclaration': {
|
||||
let skipping = isReplaying;
|
||||
for (const child of node.declarations) {
|
||||
if (skipping && child === this.$$execution.stack[depth + 1]) {
|
||||
skipping = false;
|
||||
}
|
||||
/* v8 ignore next 3 */
|
||||
if (skipping) {
|
||||
continue;
|
||||
}
|
||||
const result = this.executeSync(child, depth + 1);
|
||||
if (result.yield) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
result = {value: undefined, yield: YIELD_NONE};
|
||||
break;
|
||||
}
|
||||
case 'VariableDeclarator': {
|
||||
const {id} = node;
|
||||
const scope = this.scopes.get(node);
|
||||
if (null === node.init) {
|
||||
scope.allocate(id.name, undefined);
|
||||
result = {value: undefined, yield: YIELD_NONE};
|
||||
}
|
||||
else {
|
||||
const init = this.executeSync(node.init, depth + 1);
|
||||
if ('Identifier' === id.type) {
|
||||
if (init.yield) {
|
||||
return {
|
||||
value: Promise.resolve(init.value)
|
||||
.then((value) => scope.allocate(id.name, value)),
|
||||
yield: init.yield,
|
||||
};
|
||||
}
|
||||
else {
|
||||
result = {
|
||||
value: scope.allocate(id.name, init.value),
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
}
|
||||
}
|
||||
else if ('ArrayPattern' === id.type) {
|
||||
if (init.yield) {
|
||||
return {
|
||||
value: Promise.resolve(init.value)
|
||||
.then((value) => this.destructureArray(id, value)),
|
||||
yield: init.yield,
|
||||
};
|
||||
}
|
||||
else {
|
||||
result = {
|
||||
value: this.destructureArray(id, init.value),
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
}
|
||||
}
|
||||
else if ('ObjectPattern' === id.type) {
|
||||
if (init.yield) {
|
||||
return {
|
||||
value: Promise.resolve(init.value)
|
||||
.then((value) => this.destructureObject(id, value)),
|
||||
yield: init.yield,
|
||||
};
|
||||
}
|
||||
else {
|
||||
result = {
|
||||
value: this.destructureObject(id, init.value),
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'WhileStatement': {
|
||||
const shouldVisitChild = (child) => (
|
||||
isReplaying
|
||||
? (!this.$$execution.stack[depth + 1] || this.$$execution.stack[depth + 1] === child)
|
||||
: true
|
||||
);
|
||||
if (this.$$execution.stack[depth + 1] === node) {
|
||||
this.$$execution.stack.pop();
|
||||
}
|
||||
result = this.$$execution.deferred.get(node.body) || {
|
||||
value: undefined,
|
||||
yield: YIELD_NONE,
|
||||
};
|
||||
if (shouldVisitChild(node.test)) {
|
||||
const test = this.executeSync(node.test, depth + 1);
|
||||
if (test.yield) {
|
||||
return test;
|
||||
}
|
||||
if (!test.value) {
|
||||
this.$$execution.deferred.delete(node.body);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldVisitChild(node.body)) {
|
||||
const body = this.executeSync(node.body, depth + 1);
|
||||
if (body.yield) {
|
||||
return body;
|
||||
}
|
||||
this.$$execution.deferred.set(node.body, body);
|
||||
}
|
||||
// Yield
|
||||
this.$$execution.stack.push(node);
|
||||
return {
|
||||
value: undefined,
|
||||
yield: YIELD_LOOP_UPDATE,
|
||||
};
|
||||
}
|
||||
/* v8 ignore next 7 */
|
||||
default:
|
||||
console.log(
|
||||
node.type,
|
||||
Object.keys(node)
|
||||
.filter((key) => !['start', 'end'].includes(key)),
|
||||
);
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
this.$$execution.stack.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.$$execution = undefined;
|
||||
for (const scope of this.scopes.values()) {
|
||||
scope.context = {};
|
||||
}
|
||||
this.scopes.get(this.ast).context = this.$$context;
|
||||
}
|
||||
|
||||
run(ops = 1000) {
|
||||
let result;
|
||||
for (let i = 0; i < ops; ++i) {
|
||||
result = this.step();
|
||||
if (result.done || result.async) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
step() {
|
||||
if (!this.$$execution) {
|
||||
this.$$execution = {
|
||||
deferred: new Map(),
|
||||
stack: [],
|
||||
};
|
||||
}
|
||||
const result = this.executeSync(this.ast, 0);
|
||||
const stepResult = {async: false, done: false, value: undefined};
|
||||
switch (result.yield) {
|
||||
case YIELD_PROMISE: {
|
||||
stepResult.async = true;
|
||||
const promise = result.value instanceof Promise
|
||||
? result.value
|
||||
: Promise.resolve(result.value);
|
||||
promise
|
||||
.then((value) => {
|
||||
const top = this.$$execution.stack[this.$$execution.stack.length - 1];
|
||||
this.$$execution.deferred.set(top, {
|
||||
value,
|
||||
yield: YIELD_NONE,
|
||||
});
|
||||
});
|
||||
stepResult.value = promise;
|
||||
break;
|
||||
}
|
||||
case YIELD_LOOP_UPDATE: {
|
||||
stepResult.value = result.value;
|
||||
break;
|
||||
}
|
||||
case YIELD_RETURN: {
|
||||
stepResult.done = true;
|
||||
stepResult.value = result.value;
|
||||
}
|
||||
}
|
||||
if (stepResult.done) {
|
||||
this.reset();
|
||||
}
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
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);
|
||||
}
|
||||
});
|
|
@ -1,758 +0,0 @@
|
|||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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(`
|
||||
const scalar = true ? +1 : 32;
|
||||
const asyncScalar = await 2;
|
||||
const array = [3, 4, 5];
|
||||
const asyncArray = await [6, 7, 8];
|
||||
const object = {9: '10'};
|
||||
const asyncObject = await {11: '12'};
|
||||
`),
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
scalar: 1,
|
||||
asyncScalar: 2,
|
||||
array: [3, 4, 5],
|
||||
asyncArray: [6, 7, 8],
|
||||
object: {9: '10'},
|
||||
asyncObject: {11: '12'},
|
||||
});
|
||||
});
|
||||
|
||||
test('scopes variables', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const result = [];
|
||||
const scalar = 1;
|
||||
result.push(scalar);
|
||||
{
|
||||
const scalar = 2;
|
||||
result.push(scalar);
|
||||
}
|
||||
result.push(scalar);
|
||||
`),
|
||||
);
|
||||
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(`
|
||||
const [a, , c] = [1, 2, 3];
|
||||
const {x: x1, y, z: {zz}} = {x: 4, y: 5, z: {zz: 6}};
|
||||
const [d, e] = await [7, 8];
|
||||
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
|
||||
const [[v], {w}] = [[11], {w: 12}];
|
||||
`),
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
a: 1,
|
||||
c: 3,
|
||||
x1: 4,
|
||||
y: 5,
|
||||
zz: 6,
|
||||
d: 7,
|
||||
e: 8,
|
||||
t: 9,
|
||||
uu: 10,
|
||||
v: 11,
|
||||
w: 12,
|
||||
});
|
||||
});
|
||||
|
||||
test('runs arbitrary number of ops', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const foo = [];
|
||||
for (let i = 0; i < 150; ++i) {
|
||||
foo.push(i);
|
||||
}
|
||||
`),
|
||||
);
|
||||
sandbox.run(100);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(100);
|
||||
sandbox.run(100);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(150);
|
||||
});
|
||||
|
||||
test('instantiates', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const x = new C(1, 2);
|
||||
const y = new C(await a, await b);
|
||||
`),
|
||||
{
|
||||
a: Promise.resolve(1),
|
||||
b: Promise.resolve(2),
|
||||
C: class {
|
||||
foo = 'bar';
|
||||
constructor(a, b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context.x)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
expect(sandbox.context.y)
|
||||
.to.deep.include({
|
||||
a: 1,
|
||||
b: 2,
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
test('deletes', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
delete foo.one;
|
||||
delete foo['two'];
|
||||
const x = 'three';
|
||||
delete foo[x];
|
||||
const y = 'four';
|
||||
delete foo[await y];
|
||||
`),
|
||||
{
|
||||
foo: {
|
||||
one: 1,
|
||||
two: 2,
|
||||
three: 3,
|
||||
four: 4,
|
||||
},
|
||||
},
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context.foo.one)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.two)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.three)
|
||||
.to.be.undefined;
|
||||
expect(sandbox.context.foo.four)
|
||||
.to.be.undefined;
|
||||
});
|
||||
|
||||
test('evaluates conditional branches', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
let foo, bar;
|
||||
if (true) {
|
||||
foo = 1;
|
||||
}
|
||||
else {
|
||||
foo = 2;
|
||||
}
|
||||
if (await false) {
|
||||
bar = 1;
|
||||
}
|
||||
else {
|
||||
bar = 2;
|
||||
}
|
||||
if (await false) {
|
||||
bar = 1;
|
||||
}
|
||||
`),
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
foo: 1,
|
||||
bar: 2,
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
x = 0
|
||||
y = 0
|
||||
a = 0
|
||||
b = 0
|
||||
c = 0
|
||||
d = 0
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
x += 1;
|
||||
}
|
||||
for (let i = await 0; i < await 3; i = 1 + await i) {
|
||||
y += 1;
|
||||
}
|
||||
do {
|
||||
a += 1;
|
||||
} while (a < 3);
|
||||
do {
|
||||
b += await 1;
|
||||
} while (await b < 3);
|
||||
while (c < 3) {
|
||||
c += 1;
|
||||
}
|
||||
while (await d < 3) {
|
||||
d += await 1;
|
||||
}
|
||||
`),
|
||||
);
|
||||
await finish(sandbox);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
a: 3,
|
||||
b: 3,
|
||||
c: 3,
|
||||
d: 3,
|
||||
x: 3,
|
||||
y: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('evaluates undefined for nonexistent variables in scope', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const x = y
|
||||
`),
|
||||
);
|
||||
sandbox.run();
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
x: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('returns values at the top level', async () => {
|
||||
let sandbox;
|
||||
sandbox = new Sandbox(
|
||||
await parse(`
|
||||
x = 16
|
||||
y = 4
|
||||
return [x * 3, y * 3]
|
||||
x = 32
|
||||
y = 8
|
||||
`, {allowReturnOutsideFunction: true}),
|
||||
);
|
||||
expect(sandbox.run().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]);
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({x: 16, y: 4});
|
||||
sandbox = new Sandbox(
|
||||
await parse(`
|
||||
x = 16
|
||||
y = 4
|
||||
return
|
||||
x = 32
|
||||
y = 8
|
||||
`, {allowReturnOutsideFunction: true}),
|
||||
);
|
||||
sandbox.run();
|
||||
expect(sandbox.context)
|
||||
.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 () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
x = y
|
||||
`),
|
||||
);
|
||||
sandbox.run();
|
||||
expect(sandbox.context)
|
||||
.to.deep.equal({
|
||||
x: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('runs arbitrary number of ops', async () => {
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const foo = [];
|
||||
for (let i = 0; i < 1500; ++i) {
|
||||
foo.push(i);
|
||||
}
|
||||
`),
|
||||
);
|
||||
sandbox.run(1000);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(1000);
|
||||
sandbox.run(1000);
|
||||
expect(sandbox.context.foo.length)
|
||||
.to.equal(1500);
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
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.todo('declares with assignment pattern', async () => {
|
||||
let sandbox;
|
||||
sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const {Player: {id: owner = 0} = {}, Position} = {};
|
||||
owner;
|
||||
`),
|
||||
);
|
||||
expect(sandbox.run().value)
|
||||
.to.equal(0);
|
||||
sandbox = new Sandbox(
|
||||
await parse(`
|
||||
const {Player: {id: [owner = 0]} = {id: []}, Position} = {};
|
||||
owner;
|
||||
`),
|
||||
);
|
||||
expect(sandbox.run().value)
|
||||
.to.equal(0);
|
||||
});
|
||||
|
||||
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 = {
|
||||
projected: undefined,
|
||||
}
|
||||
const sandbox = new Sandbox(
|
||||
await parse(`
|
||||
if (projected?.length > 0) {
|
||||
}
|
||||
`),
|
||||
context,
|
||||
);
|
||||
expect(sandbox.run())
|
||||
.to.deep.include({value: undefined});
|
||||
});
|
||||
|
||||
test('implements for...of', async () => {
|
||||
const context = {
|
||||
iterable: [1, 2, 3],
|
||||
}
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for (const i of iterable) {
|
||||
mapped.push(i * 2);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 4, 6]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for (j of iterable) {
|
||||
mapped.push(j * 3);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [3, 6, 9]});
|
||||
context.iterable = [[1, 2], [3, 4], [5, 6]];
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for ([x, y] of iterable) {
|
||||
mapped.push(x * y);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 12, 30]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for (const [u, v] of iterable) {
|
||||
mapped.push(u * v);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 12, 30]});
|
||||
context.iterable = [{x: 1, y: 2}, {x: 3, y: 4}, {x: 5, y: 6}];
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for ({x, y} of iterable) {
|
||||
mapped.push(x * y);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 12, 30]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for (const {x, y} of iterable) {
|
||||
mapped.push(x * y);
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 12, 30]});
|
||||
context.iterable = [{x: [[1, 2], [3, 4], [5, 6]]}];
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const mapped = [];
|
||||
for (const {x} of iterable) {
|
||||
for (const [y, z] of x) {
|
||||
mapped.push(y * z);
|
||||
}
|
||||
}
|
||||
mapped
|
||||
`),
|
||||
context,
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [2, 12, 30]});
|
||||
});
|
||||
|
||||
test('breaks loops', async () => {
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const out = [];
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
out.push(i);
|
||||
break;
|
||||
}
|
||||
out
|
||||
`),
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [0]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const out = [];
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
out.push(i);
|
||||
if (i > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
`),
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [0, 1]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const out = [];
|
||||
for (const x of [1, 2, 3]) {
|
||||
out.push(x);
|
||||
break;
|
||||
}
|
||||
out
|
||||
`),
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [1]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const out = [];
|
||||
for (const x of [1, 2, 3]) {
|
||||
for (const y of [4, 5, 6]) {
|
||||
out.push(x);
|
||||
if (y > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
`),
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [1, 1, 2, 2, 3, 3]});
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
const out = [];
|
||||
for (let x = 1; x < 4; ++x) {
|
||||
for (let y = 4; y < 7; ++y) {
|
||||
out.push(x);
|
||||
if (y > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
`),
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: [1, 1, 2, 2, 3, 3]});
|
||||
});
|
||||
|
||||
test('short-circuits logical expressions', async () => {
|
||||
let x = 0;
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
let y = 0;
|
||||
if (test || test()) {
|
||||
y = 1;
|
||||
}
|
||||
y
|
||||
`),
|
||||
{
|
||||
test: () => {
|
||||
x = 1;
|
||||
},
|
||||
}
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: 1});
|
||||
expect(x)
|
||||
.to.equal(0);
|
||||
expect(
|
||||
(new Sandbox(
|
||||
await parse(`
|
||||
let y = 0;
|
||||
if (!test && test()) {
|
||||
y = 1;
|
||||
}
|
||||
y
|
||||
`),
|
||||
{
|
||||
test: () => {
|
||||
x = 1;
|
||||
},
|
||||
}
|
||||
)).run()
|
||||
)
|
||||
.to.deep.include({value: 0});
|
||||
expect(x)
|
||||
.to.equal(0);
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
export default class Scope {
|
||||
|
||||
context = {};
|
||||
parent = null;
|
||||
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
allocate(key, value) {
|
||||
return this.context[key] = value;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let walk = this;
|
||||
while (walk) {
|
||||
if (key in walk.context) {
|
||||
return walk.context[key];
|
||||
}
|
||||
walk = walk.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
let walk = this;
|
||||
while (walk) {
|
||||
if (key in walk.context) {
|
||||
walk.context[key] = value;
|
||||
return value;
|
||||
}
|
||||
if (!walk.parent) {
|
||||
walk.context[key] = value;
|
||||
return value;
|
||||
}
|
||||
walk = walk.parent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export default async function(code) {
|
||||
const {parse} = await import('acorn');
|
||||
const ast = parse(code, {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
});
|
||||
return ast.body[0].expression;
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
export const TRAVERSAL_PATH = {
|
||||
ArrayExpression: ['elements'],
|
||||
ArrayPattern: ['elements'],
|
||||
AssignmentExpression: ['left', 'right'],
|
||||
AwaitExpression: ['argument'],
|
||||
BinaryExpression: ['left', 'right'],
|
||||
BlockStatement: ['body'],
|
||||
BreakStatement: [],
|
||||
CallExpression: ['arguments', 'callee'],
|
||||
ChainExpression: ['expression'],
|
||||
ConditionalExpression: ['alternate', 'consequent', 'test'],
|
||||
DoWhileStatement: ['body', 'test'],
|
||||
ExpressionStatement: ['expression'],
|
||||
ForOfStatement: ['body', 'left', 'right'],
|
||||
ForStatement: ['body', 'init', 'test', 'update'],
|
||||
Identifier: [],
|
||||
IfStatement: ['alternate', 'consequent', 'test'],
|
||||
MemberExpression: ['object', 'property'],
|
||||
NewExpression: ['arguments', 'callee'],
|
||||
Literal: [],
|
||||
LogicalExpression: ['left', 'right'],
|
||||
ObjectExpression: ['properties'],
|
||||
ObjectPattern: ['properties'],
|
||||
Program: ['body'],
|
||||
Property: ['key', 'value'],
|
||||
ReturnStatement: ['argument'],
|
||||
SpreadElement: ['argument'],
|
||||
UnaryExpression: ['argument'],
|
||||
UpdateExpression: ['argument'],
|
||||
VariableDeclaration: ['declarations'],
|
||||
VariableDeclarator: ['id', 'init'],
|
||||
WhileStatement: ['body', 'test'],
|
||||
};
|
||||
|
||||
export default function traverse(node, visitor) {
|
||||
/* v8 ignore next 3 */
|
||||
if (!(node.type in TRAVERSAL_PATH)) {
|
||||
throw new Error(`node type ${node.type} not traversable. (${Object.keys(node).join(', ')})`);
|
||||
}
|
||||
visitor(node, 'enter');
|
||||
for (const key of TRAVERSAL_PATH[node.type]) {
|
||||
if (Array.isArray(node[key])) {
|
||||
for (const child of node[key]) {
|
||||
if (child) {
|
||||
traverse(child, visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (node[key]) {
|
||||
traverse(node[key], visitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
visitor(node, 'exit');
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
import Components from '@/ecs/components/index.js';
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
import Systems from '@/ecs/systems/index.js';
|
||||
import {readAsset} from '@/util/resources.js';
|
||||
import {get, loadResources, readAsset} from '@/util/resources.js';
|
||||
|
||||
class PredictionEcs extends Ecs {
|
||||
async readAsset(path) {
|
||||
const resource = await readAsset(path);
|
||||
return resource
|
||||
? resource
|
||||
: new ArrayBuffer(0);
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +14,8 @@ const Flow = {
|
|||
DOWN: 1,
|
||||
};
|
||||
|
||||
await loadResources(await get());
|
||||
|
||||
const actions = new Map();
|
||||
let ecs = new PredictionEcs({Components, Systems});
|
||||
let mainEntityId = 0;
|
||||
|
|
|
@ -34,36 +34,6 @@ export default class Collider extends Component {
|
|||
}
|
||||
return aabbs;
|
||||
}
|
||||
endIntersections(other, intersections) {
|
||||
const otherEntity = ecs.get(other.entity);
|
||||
const thisEntity = ecs.get(this.entity);
|
||||
for (const intersection of intersections) {
|
||||
const [body, otherBody] = [
|
||||
intersection.entity.bodies[intersection.i],
|
||||
intersection.other.bodies[intersection.j],
|
||||
];
|
||||
if (this.$$collisionEnd) {
|
||||
const script = this.$$collisionEnd.clone();
|
||||
script.context.other = otherEntity;
|
||||
script.context.pair = [body, otherBody];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
thisEntity.Ticking.add(ticker);
|
||||
}
|
||||
if (other.$$collisionEnd) {
|
||||
const script = other.$$collisionEnd.clone();
|
||||
script.context.other = thisEntity;
|
||||
script.context.pair = [otherBody, body];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
otherEntity.Ticking.add(ticker);
|
||||
}
|
||||
}
|
||||
this.$$intersections.delete(other);
|
||||
other.$$intersections.delete(this);
|
||||
}
|
||||
checkCollision(other) {
|
||||
const otherEntity = ecs.get(other.entity);
|
||||
const thisEntity = ecs.get(this.entity);
|
||||
|
@ -181,6 +151,36 @@ export default class Collider extends Component {
|
|||
}
|
||||
this.$$intersections.clear();
|
||||
}
|
||||
endIntersections(other, intersections) {
|
||||
const otherEntity = ecs.get(other.entity);
|
||||
const thisEntity = ecs.get(this.entity);
|
||||
for (const intersection of intersections) {
|
||||
const [body, otherBody] = [
|
||||
intersection.entity.bodies[intersection.i],
|
||||
intersection.other.bodies[intersection.j],
|
||||
];
|
||||
if (this.$$collisionEnd) {
|
||||
const script = this.$$collisionEnd.clone();
|
||||
script.context.other = otherEntity;
|
||||
script.context.pair = [body, otherBody];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
thisEntity.Ticking.add(ticker);
|
||||
}
|
||||
if (other.$$collisionEnd) {
|
||||
const script = other.$$collisionEnd.clone();
|
||||
script.context.other = thisEntity;
|
||||
script.context.pair = [otherBody, body];
|
||||
const ticker = script.ticker();
|
||||
ecs.addDestructionDependency(thisEntity.id, ticker);
|
||||
ecs.addDestructionDependency(otherEntity.id, ticker);
|
||||
otherEntity.Ticking.add(ticker);
|
||||
}
|
||||
}
|
||||
this.$$intersections.delete(other);
|
||||
other.$$intersections.delete(this);
|
||||
}
|
||||
intersectionsWith(other) {
|
||||
const {aabb, aabbs} = this;
|
||||
const {aabb: otherAabb, aabbs: otherAabbs} = other;
|
||||
|
@ -288,9 +288,13 @@ export default class Collider extends Component {
|
|||
subtype: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
// if either body has a group of zero, use bits
|
||||
// if both groups are non-zero but different, use bits
|
||||
// if both groups are the same and positive, collide
|
||||
// if both groups are the same and negative, don't collide
|
||||
bits: {defaultValue: 0x00000001, type: 'uint32'},
|
||||
impassable: {type: 'uint8'},
|
||||
group: {type: 'int32'},
|
||||
impassable: {type: 'uint8'},
|
||||
mask: {defaultValue: 0xFFFFFFFF, type: 'uint32'},
|
||||
points: {
|
||||
type: 'array',
|
||||
|
|
|
@ -2,22 +2,21 @@ import Component from '@/ecs/component.js';
|
|||
|
||||
export default class Forces extends Component {
|
||||
instanceFromSchema() {
|
||||
const {ecs} = this;
|
||||
return class ForcesInstance extends super.instanceFromSchema() {
|
||||
applyForce({x, y}) {
|
||||
this.forceX += x;
|
||||
this.forceY += y;
|
||||
}
|
||||
applyImpulse({x, y}) {
|
||||
this.impulseX += x;
|
||||
this.impulseY += y;
|
||||
this.$$impulseX += x;
|
||||
this.$$impulseY += y;
|
||||
ecs.markChange(this.entity, {
|
||||
Forces: {
|
||||
impulseX: this.$$impulseX,
|
||||
impulseY: this.$$impulseY,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
static properties = {
|
||||
dampingX: {type: 'float32'},
|
||||
dampingY: {type: 'float32'},
|
||||
forceX: {type: 'float32'},
|
||||
forceY: {type: 'float32'},
|
||||
impulseX: {type: 'float32'},
|
||||
impulseY: {type: 'float32'},
|
||||
};
|
||||
|
|
|
@ -422,25 +422,19 @@ export default class Ecs {
|
|||
}
|
||||
}
|
||||
|
||||
async readJson(uri) {
|
||||
readJson(uri) {
|
||||
const key = ['$$json', uri].join(':');
|
||||
if (!cache.has(key)) {
|
||||
const {promise, resolve, reject} = withResolvers();
|
||||
cache.set(key, promise);
|
||||
this.readAsset(uri)
|
||||
.then((chars) => {
|
||||
resolve(
|
||||
chars.byteLength > 0
|
||||
? JSON.parse(textDecoder.decode(chars))
|
||||
: {},
|
||||
);
|
||||
})
|
||||
.catch(reject);
|
||||
const buffer = this.readAsset(uri);
|
||||
const json = buffer.byteLength > 0
|
||||
? JSON.parse(textDecoder.decode(buffer))
|
||||
: {};
|
||||
cache.set(key, json);
|
||||
}
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
async readScript(uriOrCode, context = {}) {
|
||||
readScript(uriOrCode, context = {}) {
|
||||
if (!uriOrCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -449,7 +443,7 @@ export default class Ecs {
|
|||
code = uriOrCode;
|
||||
}
|
||||
else {
|
||||
const buffer = await this.readAsset(uriOrCode);
|
||||
const buffer = this.readAsset(uriOrCode);
|
||||
if (buffer.byteLength > 0) {
|
||||
code = textDecoder.decode(buffer);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class Colliders extends System {
|
|||
if (!checked.has(entity)) {
|
||||
checked.set(entity, new Set());
|
||||
}
|
||||
const within = this.ecs.system('MaintainColliderHash').within(entity.Collider.aabb);
|
||||
const within = this.ecs.system('MaintainColliderHash').collisions(entity);
|
||||
for (const other of within) {
|
||||
if (entity === other || !other.Collider) {
|
||||
continue;
|
||||
|
|
|
@ -23,8 +23,18 @@ export default class IntegratePhysics extends System {
|
|||
if (!Forces || !Position) {
|
||||
return;
|
||||
}
|
||||
Position.x = Position.$$x + elapsed * (Forces.$$impulseX + Forces.$$forceX);
|
||||
Position.y = Position.$$y + elapsed * (Forces.$$impulseY + Forces.$$forceY);
|
||||
Position.lastX = Position.$$x;
|
||||
Position.$$x += elapsed * (Forces.$$impulseX);
|
||||
Position.lastY = Position.$$y;
|
||||
Position.$$y += elapsed * (Forces.$$impulseY);
|
||||
this.ecs.markChange(
|
||||
entity.id, {
|
||||
Position: {
|
||||
x: Position.$$x,
|
||||
y: Position.$$y,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,12 +3,103 @@ import SpatialHash from '@/util/spatial-hash.js';
|
|||
|
||||
export default class MaintainColliderHash extends System {
|
||||
|
||||
hash;
|
||||
AreaSize;
|
||||
|
||||
groups = new Map();
|
||||
|
||||
collisions(entity) {
|
||||
const collisions = new Set();
|
||||
const {aabb, bodies} = entity.Collider;
|
||||
for (let {bits, group, mask} of bodies) {
|
||||
const bitsList = [];
|
||||
const maskList = [];
|
||||
let bit = 0;
|
||||
while (bits > 0) {
|
||||
if (bits & 1) {
|
||||
bitsList.push(bit);
|
||||
}
|
||||
bit += 1;
|
||||
bits >>= 1;
|
||||
}
|
||||
if (mask !== 4294967295) {
|
||||
bit = 0;
|
||||
while (mask > 0) {
|
||||
if (mask & 1) {
|
||||
maskList.push(bit);
|
||||
}
|
||||
bit += 1;
|
||||
mask >>= 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
maskList.push(-1);
|
||||
}
|
||||
for (const [key, hashes] of this.groups) {
|
||||
if (group === key && group < 0) {
|
||||
continue;
|
||||
}
|
||||
if (group === key && group > 0) {
|
||||
const within = hashes.group.within(aabb);
|
||||
for (const other of within) {
|
||||
if (other !== entity.id) {
|
||||
collisions.add(this.ecs.get(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key !== group || 0 === key) {
|
||||
if (maskList.length > 0) {
|
||||
if (-1 === maskList[0]) {
|
||||
for (const [, hash] of hashes.bits) {
|
||||
const within = hash.within(aabb);
|
||||
for (const other of within) {
|
||||
if (other !== entity.id) {
|
||||
collisions.add(this.ecs.get(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (const mask of maskList) {
|
||||
if (!hashes.bits.get(mask)) {
|
||||
continue;
|
||||
}
|
||||
const within = hashes.bits.get(mask).within(aabb);
|
||||
for (const other of within) {
|
||||
if (other !== entity.id) {
|
||||
collisions.add(this.ecs.get(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const bits of bitsList) {
|
||||
if (!hashes.mask.get(bits)) {
|
||||
continue;
|
||||
}
|
||||
const within = hashes.mask.get(bits).within(aabb);
|
||||
for (const other of within) {
|
||||
if (other !== entity.id) {
|
||||
collisions.add(this.ecs.get(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return collisions;
|
||||
}
|
||||
|
||||
deindex(entities) {
|
||||
super.deindex(entities);
|
||||
for (const id of entities) {
|
||||
this.hash.remove(id);
|
||||
for (const [, hashes] of this.groups) {
|
||||
for (const id of entities) {
|
||||
hashes.group.remove(id);
|
||||
for (const map of [hashes.bits, hashes.mask]) {
|
||||
for (const [, hash] of map) {
|
||||
hash.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,22 +113,61 @@ export default class MaintainColliderHash extends System {
|
|||
for (const id of entities) {
|
||||
if (1 === id) {
|
||||
const {AreaSize} = this.ecs.get(1);
|
||||
if (AreaSize) {
|
||||
this.hash = new SpatialHash(AreaSize);
|
||||
}
|
||||
this.AreaSize = AreaSize;
|
||||
this.groups.clear();
|
||||
}
|
||||
}
|
||||
super.reindex(entities);
|
||||
for (const id of entities) {
|
||||
this.updateHash(this.ecs.get(id));
|
||||
this.updateHashes(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
||||
updateHash(entity) {
|
||||
if (!entity.Collider || !this.hash) {
|
||||
updateHashes(entity) {
|
||||
if (!entity.Collider || !this.AreaSize) {
|
||||
return;
|
||||
}
|
||||
this.hash.update(entity.Collider.aabb, entity.id);
|
||||
const {bodies} = entity.Collider;
|
||||
for (let {bits, group, mask} of bodies) {
|
||||
if (!this.groups.has(group)) {
|
||||
this.groups.set(group, {
|
||||
bits: new Map(),
|
||||
group: new SpatialHash(this.AreaSize),
|
||||
mask: new Map(),
|
||||
});
|
||||
}
|
||||
this.groups.get(group).group.update(entity.Collider.aabb, entity.id);
|
||||
let bit = 0;
|
||||
while (bits > 0) {
|
||||
if (bits & 1) {
|
||||
if (!this.groups.get(group).bits.has(bit)) {
|
||||
this.groups.get(group).bits.set(bit, new SpatialHash(this.AreaSize));
|
||||
}
|
||||
this.groups.get(group).bits.get(bit).update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
bit += 1;
|
||||
bits >>= 1;
|
||||
}
|
||||
if (mask !== 4294967295) {
|
||||
bit = 0;
|
||||
while (mask > 0) {
|
||||
if (mask & 1) {
|
||||
if (!this.groups.get(group).mask.has(bit)) {
|
||||
this.groups.get(group).mask.set(bit, new SpatialHash(this.AreaSize));
|
||||
}
|
||||
this.groups.get(group).mask.get(bit).update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
bit += 1;
|
||||
mask >>= 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this.groups.get(group).mask.has(-1)) {
|
||||
this.groups.get(group).mask.set(-1, new SpatialHash(this.AreaSize));
|
||||
}
|
||||
this.groups.get(group).mask.get(-1).update(entity.Collider.aabb, entity.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
|
@ -48,14 +178,14 @@ export default class MaintainColliderHash extends System {
|
|||
entity.Collider.updateAabbs();
|
||||
}
|
||||
for (const entity of this.ecs.changed(['Position'])) {
|
||||
this.updateHash(entity);
|
||||
this.updateHashes(entity);
|
||||
}
|
||||
}
|
||||
|
||||
within(query) {
|
||||
const within = new Set();
|
||||
if (this.hash) {
|
||||
for (const id of this.hash.within(query)) {
|
||||
for (const [, hashes] of this.groups) {
|
||||
for (const id of hashes.group.within(query)) {
|
||||
within.add(this.ecs.get(id));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,27 +22,21 @@ export default class ResetForces extends System {
|
|||
}
|
||||
}
|
||||
|
||||
tickSingle(entity, elapsed) {
|
||||
tickSingle(entity) {
|
||||
const {Forces} = entity;
|
||||
if (!Forces) {
|
||||
return;
|
||||
}
|
||||
if (0 !== Forces.forceX) {
|
||||
const factorX = Math.pow(1 - Forces.dampingX, elapsed);
|
||||
Forces.forceX *= factorX;
|
||||
if (Math.abs(Forces.forceX) <= 1) {
|
||||
Forces.forceX = 0;
|
||||
}
|
||||
}
|
||||
if (0 !== Forces.forceY) {
|
||||
const factorY = Math.pow(1 - Forces.dampingY, elapsed);
|
||||
Forces.forceY *= factorY;
|
||||
if (Math.abs(Forces.forceY) <= 1) {
|
||||
Forces.forceY = 0;
|
||||
}
|
||||
}
|
||||
Forces.impulseX = 0;
|
||||
Forces.impulseY = 0;
|
||||
Forces.$$impulseX = 0;
|
||||
Forces.$$impulseY = 0;
|
||||
this.ecs.markChange(
|
||||
entity.id, {
|
||||
Forces: {
|
||||
impulseX: Forces.$$impulseX,
|
||||
impulseY: Forces.$$impulseY,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -65,10 +65,20 @@ export default class VisibleAabbs extends System {
|
|||
if (!size) {
|
||||
throw new Error(`no size for aabb for entity ${entity.id}(${JSON.stringify(entity.toJSON(), null, 2)})`);
|
||||
}
|
||||
VisibleAabb.x0 = x - Sprite.anchor.x * size.w;
|
||||
VisibleAabb.x1 = x + (1 - Sprite.anchor.x) * size.w;
|
||||
VisibleAabb.y0 = y - Sprite.anchor.y * size.h;
|
||||
VisibleAabb.y1 = y + (1 - Sprite.anchor.y) * size.h;
|
||||
VisibleAabb.$$x0 = x - Sprite.anchor.x * size.w;
|
||||
VisibleAabb.$$x1 = x + (1 - Sprite.anchor.x) * size.w;
|
||||
VisibleAabb.$$y0 = y - Sprite.anchor.y * size.h;
|
||||
VisibleAabb.$$y1 = y + (1 - Sprite.anchor.y) * size.h;
|
||||
this.ecs.markChange(
|
||||
entity.id, {
|
||||
VisibleAabb: {
|
||||
x0: VisibleAabb.$$x0,
|
||||
x1: VisibleAabb.$$x1,
|
||||
y0: VisibleAabb.$$y0,
|
||||
y1: VisibleAabb.$$y1,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.updateHash(entity);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ export default class Server {
|
|||
addPacketListener(type, listener) {
|
||||
this.emitter.addListener(type, listener);
|
||||
}
|
||||
async load() {
|
||||
}
|
||||
async readJson(path) {
|
||||
return JSON.parse(textDecoder.decode(await this.readData(path)));
|
||||
}
|
||||
|
|
|
@ -13,10 +13,7 @@ export default class ClientEcs extends Ecs {
|
|||
}
|
||||
});
|
||||
}
|
||||
async readAsset(path) {
|
||||
const resource = await readAsset(path);
|
||||
return resource
|
||||
? resource
|
||||
: new ArrayBuffer(0);
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,12 @@ import {json, useLoaderData} from "@remix-run/react";
|
|||
import {useEffect, useState} from 'react';
|
||||
import {Outlet, useParams} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
computeMissing,
|
||||
fetchResources,
|
||||
get,
|
||||
readAsset,
|
||||
set,
|
||||
} from '@/util/resources.js';
|
||||
import {fetchMissingResources, readAsset} from '@/util/resources.js';
|
||||
|
||||
import styles from './play.module.css';
|
||||
|
||||
settings.ADAPTER.fetch = async (path) => {
|
||||
const resource = await readAsset(path);
|
||||
const resource = readAsset(path);
|
||||
return resource ? new Response(resource) : new Response(undefined, {status: 404});
|
||||
};
|
||||
|
||||
|
@ -29,34 +23,17 @@ export async function loader({request}) {
|
|||
|
||||
export default function Play() {
|
||||
const {manifest} = useLoaderData();
|
||||
const [assetsLoaded, setAssetsLoaded] = useState(false);
|
||||
const [Client, setClient] = useState();
|
||||
const params = useParams();
|
||||
const [type] = params['*'].split('/');
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const {signal} = controller;
|
||||
async function receiveResources() {
|
||||
const current = await get();
|
||||
const paths = await computeMissing(current, manifest);
|
||||
if (paths.length > 0 && !signal.aborted) {
|
||||
try {
|
||||
const resources = await fetchResources(paths, {signal});
|
||||
if (resources) {
|
||||
for (const key in resources) {
|
||||
current[key] = resources[key];
|
||||
}
|
||||
await set(current);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if ((e instanceof DOMException) && 'AbortError' === e.name) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
receiveResources();
|
||||
fetchMissingResources(manifest, signal)
|
||||
.then(() => {
|
||||
setAssetsLoaded(true);
|
||||
});
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
|
@ -78,7 +55,9 @@ export default function Play() {
|
|||
}, [type]);
|
||||
return (
|
||||
<div className={styles.play}>
|
||||
<Outlet context={Client} />
|
||||
{assetsLoaded && (
|
||||
<Outlet context={Client} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
import Ecs from '@/ecs/ecs.js';
|
||||
import {decode, encode} from '@/net/packets/index.js';
|
||||
import {
|
||||
|
@ -8,7 +6,6 @@ import {
|
|||
TPS,
|
||||
UPS,
|
||||
} from '@/util/constants.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
|
||||
import createEcs from './create/ecs.js';
|
||||
import createForest from './create/forest.js';
|
||||
|
@ -19,10 +16,6 @@ import createTown from './create/town.js';
|
|||
|
||||
const UPS_PER_S = 1 / UPS;
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export default class Engine {
|
||||
|
@ -56,15 +49,8 @@ export default class Engine {
|
|||
lookupPlayerEntity(id) {
|
||||
return engine.lookupPlayerEntity(id);
|
||||
}
|
||||
async readAsset(uri) {
|
||||
if (!cache.has(uri)) {
|
||||
const {promise, resolve, reject} = withResolvers();
|
||||
cache.set(uri, promise);
|
||||
server.readAsset(uri)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
return cache.get(uri);
|
||||
readAsset(uri) {
|
||||
return server.readAsset(uri);
|
||||
}
|
||||
async switchEcs(entity, path, updates) {
|
||||
for (const [connection, connectedPlayer] of engine.connectedPlayers) {
|
||||
|
@ -372,6 +358,7 @@ export default class Engine {
|
|||
}
|
||||
|
||||
async load() {
|
||||
await this.server.load();
|
||||
let townData;
|
||||
try {
|
||||
townData = await this.server.readData('town');
|
||||
|
|
|
@ -5,35 +5,25 @@ import {WebSocketServer} from 'ws';
|
|||
|
||||
import Server from '@/net/server.js';
|
||||
import {getSession} from '@/server/session.server.js';
|
||||
import {loadResources, readAsset} from '@/util/resources.js';
|
||||
import {loadResources as loadServerResources} from '@/util/resources.server.js';
|
||||
|
||||
import Engine from './engine.js';
|
||||
|
||||
const {
|
||||
RESOURCES_PATH = [process.cwd(), 'resources'].join('/'),
|
||||
} = process.env;
|
||||
|
||||
global.__silphiusWebsocket = null;
|
||||
|
||||
class SocketServer extends Server {
|
||||
async ensurePath(path) {
|
||||
await mkdir(path, {recursive: true});
|
||||
}
|
||||
async load() {
|
||||
await loadResources(await loadServerResources());
|
||||
}
|
||||
static qualify(path) {
|
||||
return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
|
||||
}
|
||||
async readAsset(path) {
|
||||
const {pathname} = new URL(path, 'http://example.org');
|
||||
const resourcePath = pathname.slice('/resources/'.length);
|
||||
try {
|
||||
const {buffer} = await readFile([RESOURCES_PATH, resourcePath].join('/'));
|
||||
return buffer;
|
||||
}
|
||||
catch (error) {
|
||||
if ('ENOENT' !== error.code) {
|
||||
throw error;
|
||||
}
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
async readData(path) {
|
||||
const qualified = this.constructor.qualify(path);
|
||||
|
|
|
@ -3,7 +3,7 @@ import {del, get, set} from 'idb-keyval';
|
|||
import {encode} from '@/net/packets/index.js';
|
||||
import Server from '@/net/server.js';
|
||||
import {withResolvers} from '@/util/promise.js';
|
||||
import {readAsset} from '@/util/resources.js';
|
||||
import {get as getResources, loadResources, readAsset} from '@/util/resources.js';
|
||||
|
||||
import createEcs from './create/ecs.js';
|
||||
import './create/forest.js';
|
||||
|
@ -18,14 +18,14 @@ class WorkerServer extends Server {
|
|||
super();
|
||||
this.fs = {};
|
||||
}
|
||||
async load() {
|
||||
await loadResources(await getResources());
|
||||
}
|
||||
static qualify(path) {
|
||||
return ['UNIVERSE', path].join('/');
|
||||
}
|
||||
async readAsset(path) {
|
||||
const resource = await readAsset(path);
|
||||
return resource
|
||||
? resource
|
||||
: new ArrayBuffer(0);
|
||||
readAsset(path) {
|
||||
return readAsset(path) ?? new ArrayBuffer(0);
|
||||
}
|
||||
async readData(path) {
|
||||
const data = await get(this.constructor.qualify(path));
|
||||
|
@ -42,7 +42,9 @@ class WorkerServer extends Server {
|
|||
async writeData(path, view) {
|
||||
await set(this.constructor.qualify(path), view);
|
||||
}
|
||||
transmit(connection, packed) { postMessage(packed); }
|
||||
transmit(connection, packed) {
|
||||
postMessage(packed);
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new Engine(WorkerServer);
|
||||
|
|
|
@ -65,4 +65,10 @@ export class Ticker extends Promise {
|
|||
this.ticker(elapsed, this.resolve, this.reject);
|
||||
}
|
||||
|
||||
then(...args) {
|
||||
const promise = super.then(...args);
|
||||
promise.ticker = this.ticker;
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,3 @@
|
|||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
const cache = new LRUCache({
|
||||
max: 128,
|
||||
});
|
||||
|
||||
export async function computeMissing(current, manifest) {
|
||||
const missing = [];
|
||||
for (const path in manifest) {
|
||||
|
@ -14,6 +8,57 @@ export async function computeMissing(current, manifest) {
|
|||
return missing;
|
||||
}
|
||||
|
||||
export async function fetchMissingResources(manifest, signal) {
|
||||
const current = await get();
|
||||
const paths = await computeMissing(current, manifest);
|
||||
if (paths.length > 0 && !signal.aborted) {
|
||||
try {
|
||||
const resources = await fetchResources(paths, {signal});
|
||||
if (resources) {
|
||||
for (const key in resources) {
|
||||
current[key] = resources[key];
|
||||
}
|
||||
await set(current);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (
|
||||
!(e instanceof DOMException)
|
||||
|| 'AbortError' !== e.name
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
loadResources(current);
|
||||
}
|
||||
|
||||
export async function fetchResources(paths, {signal} = {}) {
|
||||
const response = await fetch('/resources/stream', {
|
||||
body: JSON.stringify(paths),
|
||||
method: 'post',
|
||||
signal,
|
||||
});
|
||||
if (signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
return parseResources(paths, await response.arrayBuffer());
|
||||
}
|
||||
|
||||
export async function get() {
|
||||
const {get} = await import('idb-keyval');
|
||||
const resources = await get('$$silphius_resources');
|
||||
return resources || {};
|
||||
}
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
export function loadResources(resources) {
|
||||
for (const path in resources) {
|
||||
cache.set(path, resources[path].asset);
|
||||
}
|
||||
}
|
||||
|
||||
const octets = [];
|
||||
for (let i = 0; i < 256; ++i) {
|
||||
octets.push(i.toString(16).padStart(2, '0'));
|
||||
|
@ -44,35 +89,13 @@ export async function parseResources(resources, buffer) {
|
|||
return manifest;
|
||||
}
|
||||
|
||||
export async function fetchResources(paths, {signal} = {}) {
|
||||
const response = await fetch('/resources/stream', {
|
||||
body: JSON.stringify(paths),
|
||||
method: 'post',
|
||||
signal,
|
||||
});
|
||||
if (signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
return parseResources(paths, await response.arrayBuffer());
|
||||
}
|
||||
|
||||
export async function get() {
|
||||
const {get} = await import('idb-keyval');
|
||||
const resources = await get('$$silphius_resources');
|
||||
return resources || {};
|
||||
export function readAsset(path) {
|
||||
const {pathname} = new URL(path, 'http://example.org');
|
||||
const resourcePath = pathname.slice('/resources/'.length);
|
||||
return cache.get(resourcePath);
|
||||
}
|
||||
|
||||
export async function set(resources) {
|
||||
const {set} = await import('idb-keyval');
|
||||
cache.clear();
|
||||
await set('$$silphius_resources', resources);
|
||||
}
|
||||
|
||||
export async function readAsset(path) {
|
||||
if (!cache.has(path)) {
|
||||
const {pathname} = new URL(path, 'http://example.org');
|
||||
const resourcePath = pathname.slice('/resources/'.length);
|
||||
cache.set(path, get().then((resources) => resources[resourcePath]?.asset));
|
||||
}
|
||||
return cache.get(path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {parse as acornParse} from 'acorn';
|
||||
import {Runner} from 'astride';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
|
||||
import Sandbox from '@/astride/sandbox.js';
|
||||
import * as color from '@/util/color.js';
|
||||
import delta from '@/util/delta.js';
|
||||
import lfo from '@/util/lfo.js';
|
||||
|
@ -36,7 +36,7 @@ export default class Script {
|
|||
}
|
||||
|
||||
get context() {
|
||||
return this.sandbox.context;
|
||||
return this.sandbox.locals;
|
||||
}
|
||||
|
||||
static contextDefaults() {
|
||||
|
@ -79,21 +79,28 @@ export default class Script {
|
|||
|
||||
evaluate() {
|
||||
this.sandbox.reset();
|
||||
const {value} = this.sandbox.run();
|
||||
return value;
|
||||
try {
|
||||
const {value} = this.sandbox.step();
|
||||
return value;
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(this.sandbox.$$stack);
|
||||
console.warn(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static async fromCode(code, context = {}) {
|
||||
static fromCode(code, context = {}) {
|
||||
if (!cache.has(code)) {
|
||||
cache.set(code, this.parse(code));
|
||||
}
|
||||
return new this(
|
||||
new Sandbox(await cache.get(code), this.createContext(context)),
|
||||
new Runner(cache.get(code), this.createContext(context)),
|
||||
code,
|
||||
);
|
||||
}
|
||||
|
||||
static async parse(code) {
|
||||
static parse(code) {
|
||||
return parse(
|
||||
code,
|
||||
{
|
||||
|
@ -104,7 +111,7 @@ export default class Script {
|
|||
|
||||
reset() {
|
||||
this.promise = null;
|
||||
this.sandbox.compile();
|
||||
this.sandbox.reset();
|
||||
}
|
||||
|
||||
tick(elapsed, resolve, reject) {
|
||||
|
@ -115,14 +122,13 @@ export default class Script {
|
|||
return;
|
||||
}
|
||||
while (true) {
|
||||
this.sandbox.context.elapsed = elapsed;
|
||||
this.sandbox.locals.elapsed = elapsed;
|
||||
let async, done, value;
|
||||
try {
|
||||
({async, done, value} = this.sandbox.step());
|
||||
}
|
||||
catch (error) {
|
||||
const node = this.sandbox.$$execution.stack.pop();
|
||||
console.warn('Script ran into a problem at', this.code.slice(node.start, node.end));
|
||||
console.warn(this.sandbox.$$stack);
|
||||
console.warn(error);
|
||||
if (resolve) {
|
||||
resolve();
|
||||
|
|
21
package-lock.json
generated
21
package-lock.json
generated
|
@ -22,6 +22,7 @@
|
|||
"@remix-run/react": "^2.9.2",
|
||||
"acorn": "^8.12.0",
|
||||
"alea": "^1.0.1",
|
||||
"astride": "file:../astride",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.18.2",
|
||||
|
@ -68,6 +69,22 @@
|
|||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"../astride": {
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"acorn": "^8.12.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"vite": "^5.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz",
|
||||
|
@ -8091,6 +8108,10 @@
|
|||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/astride": {
|
||||
"resolved": "../astride",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/astring": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"@remix-run/react": "^2.9.2",
|
||||
"acorn": "^8.12.0",
|
||||
"alea": "^1.0.1",
|
||||
"astride": "file:../astride",
|
||||
"compression": "^1.7.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.18.2",
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
const {Player, Position} = wielder;
|
||||
|
||||
const shots = [];
|
||||
|
||||
const EVERY = 0.03;
|
||||
const N = 14;
|
||||
const SPREAD = 1;
|
||||
|
@ -48,43 +46,47 @@ for (const id of await Promise.all(promises)) {
|
|||
creating.push(ecs.get(id));
|
||||
}
|
||||
|
||||
const accumulated = {};
|
||||
const shot = creating.shift();
|
||||
shot.Sprite.alpha = 1;
|
||||
accumulated[shot.id] = 0;
|
||||
shots.push(shot)
|
||||
const shots = [
|
||||
{
|
||||
accumulated: 0,
|
||||
entity: shot,
|
||||
},
|
||||
];
|
||||
|
||||
let spawner = 0;
|
||||
while (shots.length > 0) {
|
||||
spawner += elapsed;
|
||||
if (creating.length > 0 && spawner >= EVERY) {
|
||||
const shot = creating.shift();
|
||||
shot.Sprite.alpha = 1;
|
||||
accumulated[shot.id] = 0;
|
||||
shots.push(shot)
|
||||
const entity = creating.shift();
|
||||
entity.Sprite.alpha = 1;
|
||||
shots.push({accumulated: 0, entity})
|
||||
spawner -= EVERY;
|
||||
}
|
||||
const destroying = [];
|
||||
for (const shot of shots) {
|
||||
accumulated[shot.id] += elapsed;
|
||||
if (accumulated[shot.id] <= SPREAD) {
|
||||
shot.Speed.speed = 100 * (1 - (accumulated[shot.id] / SPREAD))
|
||||
shot.accumulated += elapsed;
|
||||
if (shot.accumulated <= SPREAD) {
|
||||
shot.entity.Speed.speed = 100 * (1 - (shot.accumulated / SPREAD))
|
||||
}
|
||||
else {
|
||||
const toward = Math.atan2(
|
||||
where.y - shot.Position.y,
|
||||
where.x - shot.Position.x,
|
||||
)
|
||||
shot.Speed.speed = 400;
|
||||
shot.Direction.direction = (Math.TAU + toward) % Math.TAU;
|
||||
if (accumulated[shot.id] > 1.5 || Math.distance(where, shot.Position) < 4) {
|
||||
delete accumulated[shot.id];
|
||||
shot.Sprite.alpha = 0;
|
||||
ecs.destroy(shot.id);
|
||||
if (!shot.oriented) {
|
||||
const toward = Math.atan2(
|
||||
where.y - shot.entity.Position.y,
|
||||
where.x - shot.entity.Position.x,
|
||||
)
|
||||
shot.entity.Speed.speed = 400;
|
||||
shot.entity.Direction.direction = (Math.TAU + toward) % Math.TAU;
|
||||
shot.oriented = true;
|
||||
}
|
||||
if (shot.accumulated > 1.5) {
|
||||
shot.entity.Sprite.alpha = 0;
|
||||
ecs.destroy(shot.entity.id);
|
||||
destroying.push(shot);
|
||||
}
|
||||
}
|
||||
shot.Controlled.directionMove(shot.Direction.direction);
|
||||
shot.entity.Controlled.directionMove(shot.entity.Direction.direction);
|
||||
}
|
||||
for (let i = 0; i < destroying.length; ++i) {
|
||||
shots.splice(shots.indexOf(destroying[i]), 1);
|
||||
|
|
Loading…
Reference in New Issue
Block a user