silphius/app/astride/sandbox.test.js
2024-07-26 07:04:36 -05:00

759 lines
14 KiB
JavaScript

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