Compare commits

...

9 Commits

Author SHA1 Message Date
cha0s
71d35bd228 fix: update last position 2024-10-15 23:26:23 -05:00
cha0s
cf4d21db1f refactor: upfront/sync resource loading 2024-10-15 22:18:45 -05:00
cha0s
ff17dd883a fix: Ticker::then 2024-10-15 16:44:09 -05:00
cha0s
0bc8d05706 perf: magic swords 2024-10-11 13:51:02 -05:00
cha0s
3a37cf4938 perf: collision hashes 2024-10-11 13:07:05 -05:00
cha0s
ebc4ebd957 chore: doc 2024-10-08 11:24:58 -05:00
cha0s
16fa5666a0 chore: alpha 2024-10-08 11:10:45 -05:00
cha0s
3964278ca0 refactor: astride 2024-10-07 15:04:19 -05:00
cha0s
cf04455725 perf: change marking 2024-10-07 02:20:36 -05:00
54 changed files with 396 additions and 3316 deletions

View File

@ -1,3 +0,0 @@
# ASTRide
Ride your AST :)

View File

@ -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}`)
}
}

View File

@ -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;
}),
};
}

View File

@ -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']);
});

View File

@ -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)};
}

View File

@ -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);
});

View File

@ -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),
};
}

View File

@ -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);
});

View File

@ -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),
};
}

View File

@ -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);
});

View File

@ -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),
};
}

View File

@ -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);
});

View File

@ -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});
}

View File

@ -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);
});

View File

@ -1,3 +0,0 @@
export default function(node, {scope}) {
return {async: false, value: scope.get(node.name)};
}

View File

@ -1,3 +0,0 @@
export default function({value}) {
return {async: false, value};
}

View File

@ -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'});
});

View File

@ -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)};
}

View File

@ -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);
});

View File

@ -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)};
}

View File

@ -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',
});
});

View File

@ -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),
};
}

View File

@ -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,
});
});

View File

@ -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)};
}

View File

@ -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);
});

View File

@ -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)};
}

View File

@ -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);
});

View File

@ -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;
}
}

View File

@ -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);
}
});

View File

@ -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);
});

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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');
}

View File

@ -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;

View File

@ -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',

View File

@ -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'},
};

View File

@ -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);
}

View File

@ -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;

View File

@ -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,
},
},
);
}
}

View File

@ -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 [, hashes] of this.groups) {
for (const id of entities) {
this.hash.remove(id);
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));
}
}

View File

@ -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,
},
},
);
}
}

View File

@ -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);
}
}

View File

@ -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)));
}

View File

@ -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);
}
}

View File

@ -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}>
{assetsLoaded && (
<Outlet context={Client} />
)}
</div>
);
}

View File

@ -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');

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
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
View File

@ -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",

View File

@ -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",

View File

@ -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 {
if (!shot.oriented) {
const toward = Math.atan2(
where.y - shot.Position.y,
where.x - shot.Position.x,
where.y - shot.entity.Position.y,
where.x - shot.entity.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);
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);