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}}; `), ); 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, }); }); 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('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('handles nested yields', async () => { expect( await finish(new Sandbox( await parse(` for (let i = 0; i < 1; ++i) { for (let j = 0; j < 1; ++j) { } } `), )) ) .to.deep.include({value: undefined}); expect( ( await finish(new Sandbox( await parse(` for (let i = 0; i < 1; ++i) { for (let j = 0; j < 1; ++j) { await 1; } } `), )) ) ) .to.deep.include({value: 1}); expect( ( await finish(new Sandbox( await parse(` if (true) { for (let i = 0; i < 1; ++i) { } } `), )) ) ) .to.deep.include({value: undefined}); expect( ( await finish(new Sandbox( await parse(` if (1 ? await 2 : 3) { for (let i = 0; i < 1; ++i) { } } `), )) ) ) .to.deep.include({value: undefined}); }); test('handles optional expressions', async () => { const context = { console, projected: undefined, } const sandbox = new Sandbox( await parse(` if (projected?.length > 0) { } `), context, ); expect(sandbox.run()) .to.deep.include({value: undefined}); });