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