From 2f93e42106389fd7f723a9fefe93814aee41f600 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 16 Jun 2024 08:01:01 -0500 Subject: [PATCH] feat: initial swcx --- .eslintrc.cjs | 8 + .gitignore | 1 + app/swcx/builders.js | 5 + app/swcx/evaluate.js | 47 +++++ app/swcx/evaluators/array.js | 13 ++ app/swcx/evaluators/array.test.js | 17 ++ app/swcx/evaluators/assignment.js | 84 ++++++++ app/swcx/evaluators/assignment.test.js | 267 ++++++++++++++++++++++++ app/swcx/evaluators/await.js | 7 + app/swcx/evaluators/await.test.js | 20 ++ app/swcx/evaluators/binary.js | 45 ++++ app/swcx/evaluators/binary.test.js | 177 ++++++++++++++++ app/swcx/evaluators/call.js | 53 +++++ app/swcx/evaluators/call.test.js | 64 ++++++ app/swcx/evaluators/conditional.js | 13 ++ app/swcx/evaluators/conditional.test.js | 43 ++++ app/swcx/evaluators/identifier.js | 3 + app/swcx/evaluators/literal.js | 3 + app/swcx/evaluators/literal.test.js | 14 ++ app/swcx/evaluators/member.js | 16 ++ app/swcx/evaluators/member.test.js | 44 ++++ app/swcx/evaluators/object.js | 88 ++++++++ app/swcx/evaluators/object.test.js | 60 ++++++ app/swcx/evaluators/unary.js | 24 +++ app/swcx/evaluators/unary.test.js | 42 ++++ app/swcx/evaluators/update.js | 23 ++ app/swcx/evaluators/update.test.js | 50 +++++ app/swcx/sandbox.js | 266 +++++++++++++++++++++++ app/swcx/sandbox.test.js | 197 +++++++++++++++++ app/swcx/scope.js | 40 ++++ app/swcx/traverse.js | 63 ++++++ app/swcx/types.js | 143 +++++++++++++ app/util/fast-call.js | 36 ++++ package-lock.json | 201 ++++++++++++++++++ package.json | 1 + 35 files changed, 2178 insertions(+) create mode 100644 app/swcx/builders.js create mode 100644 app/swcx/evaluate.js create mode 100644 app/swcx/evaluators/array.js create mode 100644 app/swcx/evaluators/array.test.js create mode 100644 app/swcx/evaluators/assignment.js create mode 100644 app/swcx/evaluators/assignment.test.js create mode 100644 app/swcx/evaluators/await.js create mode 100644 app/swcx/evaluators/await.test.js create mode 100644 app/swcx/evaluators/binary.js create mode 100644 app/swcx/evaluators/binary.test.js create mode 100644 app/swcx/evaluators/call.js create mode 100644 app/swcx/evaluators/call.test.js create mode 100644 app/swcx/evaluators/conditional.js create mode 100644 app/swcx/evaluators/conditional.test.js create mode 100644 app/swcx/evaluators/identifier.js create mode 100644 app/swcx/evaluators/literal.js create mode 100644 app/swcx/evaluators/literal.test.js create mode 100644 app/swcx/evaluators/member.js create mode 100644 app/swcx/evaluators/member.test.js create mode 100644 app/swcx/evaluators/object.js create mode 100644 app/swcx/evaluators/object.test.js create mode 100644 app/swcx/evaluators/unary.js create mode 100644 app/swcx/evaluators/unary.test.js create mode 100644 app/swcx/evaluators/update.js create mode 100644 app/swcx/evaluators/update.test.js create mode 100644 app/swcx/sandbox.js create mode 100644 app/swcx/sandbox.test.js create mode 100644 app/swcx/scope.js create mode 100644 app/swcx/traverse.js create mode 100644 app/swcx/types.js create mode 100644 app/util/fast-call.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d7efa3d..414e4b2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -28,6 +28,14 @@ module.exports = { extends: ['eslint:recommended'], overrides: [ + // Tests + { + files: ['**/*.test.{js,jsx,ts,tsx}'], + rules: { + 'no-empty-pattern': 'off', + }, + }, + // React { files: ['**/*.{js,jsx,ts,tsx}'], diff --git a/.gitignore b/.gitignore index 0a3a6fc..fb93fea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules /app/data /.cache /build +/coverage .env diff --git a/app/swcx/builders.js b/app/swcx/builders.js new file mode 100644 index 0000000..4431d11 --- /dev/null +++ b/app/swcx/builders.js @@ -0,0 +1,5 @@ +export async function first(code) { + const {parse} = await import('@swc/core'); + const ast = await parse(code); + return ast.body[0].expression; +} diff --git a/app/swcx/evaluate.js b/app/swcx/evaluate.js new file mode 100644 index 0000000..9ac6d57 --- /dev/null +++ b/app/swcx/evaluate.js @@ -0,0 +1,47 @@ +import {unwrap} from '@/swcx/types.js'; + +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} = {}) { + const unwrapped = unwrap(node); + switch (unwrapped.type) { + case 'ArrayExpression': + return evaluators.array(unwrapped, {evaluate, scope}); + case 'AssignmentExpression': + return evaluators.assignment(unwrapped, {evaluate, scope}); + case 'AwaitExpression': + return evaluators.await(unwrapped, {evaluate, scope}); + case 'BinaryExpression': + return evaluators.binary(unwrapped, {evaluate, scope}); + case 'BooleanLiteral': + case 'NullLiteral': + case 'NumericLiteral': + case 'StringLiteral': + return evaluators.literal(unwrapped, {evaluate, scope}); + case 'CallExpression': + return evaluators.call(unwrapped, {evaluate, scope}); + case 'ConditionalExpression': + return evaluators.conditional(unwrapped, {evaluate, scope}); + case 'Identifier': + return evaluators.identifier(unwrapped, {evaluate, scope}); + case 'MemberExpression': + return evaluators.member(unwrapped, {evaluate, scope}); + case 'ObjectExpression': + return evaluators.object(unwrapped, {evaluate, scope}); + case 'UnaryExpression': + return evaluators.unary(unwrapped, {evaluate, scope}); + case 'UpdateExpression': + return evaluators.update(unwrapped, {evaluate, scope}); + } +} diff --git a/app/swcx/evaluators/array.js b/app/swcx/evaluators/array.js new file mode 100644 index 0000000..6f7f12d --- /dev/null +++ b/app/swcx/evaluators/array.js @@ -0,0 +1,13 @@ +export default function(node, {evaluate, scope}) { + const elements = []; + let isAsync = false; + for (const {expression} of node.elements) { + const {async, value} = evaluate(expression, {scope}); + isAsync = isAsync || async; + elements.push(value); + } + return { + async: !!isAsync, + value: isAsync ? Promise.all(elements) : elements, + }; +} diff --git a/app/swcx/evaluators/array.test.js b/app/swcx/evaluators/array.test.js new file mode 100644 index 0000000..18859bc --- /dev/null +++ b/app/swcx/evaluators/array.test.js @@ -0,0 +1,17 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates array of literals', async () => { + expect(evaluate(await first('[1.5, 2, "three"]'))) + .to.deep.include({value: [1.5, 2, 'three']}); +}); + +test('evaluates array containing promises', async () => { + const evaluated = evaluate(await first('[1.5, 2, await "three"]')); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.deep.equal([1.5, 2, 'three']); +}); diff --git a/app/swcx/evaluators/assignment.js b/app/swcx/evaluators/assignment.js new file mode 100644 index 0000000..8f95535 --- /dev/null +++ b/app/swcx/evaluators/assignment.js @@ -0,0 +1,84 @@ +import { + isComputed, + isMemberExpression, +} from '@/swcx/types.js'; + +export default function(node, {evaluate, scope}) { + const {operator, left} = node; + const right = evaluate(node.right, {scope}); + if (!isMemberExpression(left)) { + const assign = (value) => { + switch (operator) { + case '=' : return scope.set(left.value, value); + case '+=' : return scope.set(left.value, scope.get(left.value) + value); + case '-=' : return scope.set(left.value, scope.get(left.value) - value); + case '*=' : return scope.set(left.value, scope.get(left.value) * value); + case '/=' : return scope.set(left.value, scope.get(left.value) / value); + case '%=' : return scope.set(left.value, scope.get(left.value) % value); + case '**=' : return scope.set(left.value, scope.get(left.value) ** value); + case '<<=' : return scope.set(left.value, scope.get(left.value) << value); + case '>>=' : return scope.set(left.value, scope.get(left.value) >> value); + case '>>>=': return scope.set(left.value, scope.get(left.value) >>> value); + case '|=' : return scope.set(left.value, scope.get(left.value) | value); + case '^=' : return scope.set(left.value, scope.get(left.value) ^ value); + case '&=' : return scope.set(left.value, scope.get(left.value) & value); + case '||=' : return scope.set(left.value, scope.get(left.value) || value); + case '&&=' : return scope.set(left.value, scope.get(left.value) && value); + case '??=' : { + const l = scope.get(left.value); + return scope.set(left.value, (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 { + 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 = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; + 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)}; +} + diff --git a/app/swcx/evaluators/assignment.test.js b/app/swcx/evaluators/assignment.test.js new file mode 100644 index 0000000..e34ba49 --- /dev/null +++ b/app/swcx/evaluators/assignment.test.js @@ -0,0 +1,267 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.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 first('x = 4'), {scope})) + .to.deep.include({value: 4}); + expect(scope.get('x')) + .to.equal(4); + expect(evaluate(await first('O.x = 4'), {scope})) + .to.deep.include({value: 4}); + expect(scope.get('O').x) + .to.equal(4); +}); + +scopeTest('evaluates +=', async ({scope}) => { + scope.set('x', 1); + expect(evaluate(await first('x += 4'), {scope})) + .to.deep.include({value: 5}); + expect(scope.get('x')) + .to.equal(5); + scope.set('O', {x: 1}); + expect(evaluate(await first('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 first('x -= 4'), {scope})) + .to.deep.include({value: 1}); + expect(scope.get('x')) + .to.equal(1); + scope.set('O', {x: 5}); + expect(evaluate(await first('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 first('x *= 4'), {scope})) + .to.deep.include({value: 20}); + expect(scope.get('x')) + .to.equal(20); + scope.set('O', {x: 5}); + expect(evaluate(await first('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 first('x /= 5'), {scope})) + .to.deep.include({value: 5}); + expect(scope.get('x')) + .to.equal(5); + scope.set('O', {x: 25}); + expect(evaluate(await first('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 first('x %= 2'), {scope})) + .to.deep.include({value: 1}); + expect(scope.get('x')) + .to.equal(1); + scope.set('O', {x: 5}); + expect(evaluate(await first('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 first('x **= 3'), {scope})) + .to.deep.include({value: 125}); + expect(scope.get('x')) + .to.equal(125); + scope.set('O', {x: 5}); + expect(evaluate(await first('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 first('x <<= 1'), {scope})) + .to.deep.include({value: 4}); + expect(scope.get('x')) + .to.equal(4); + scope.set('O', {x: 2}); + expect(evaluate(await first('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 first('x >>= 2'), {scope})) + .to.deep.include({value: 2}); + expect(scope.get('x')) + .to.equal(2); + scope.set('O', {x: 8}); + expect(evaluate(await first('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 first('x >>>= 1'), {scope})) + .to.deep.include({value: 2147483647}); + expect(scope.get('x')) + .to.equal(2147483647); + scope.set('O', {x: -1}); + expect(evaluate(await first('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 first('x |= 5'), {scope})) + .to.deep.include({value: 7}); + expect(scope.get('x')) + .to.equal(7); + scope.set('O', {x: 3}); + expect(evaluate(await first('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 first('x ^= 2'), {scope})) + .to.deep.include({value: 5}); + expect(scope.get('x')) + .to.equal(5); + scope.set('O', {x: 7}); + expect(evaluate(await first('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 first('x &= 3'), {scope})) + .to.deep.include({value: 1}); + expect(scope.get('x')) + .to.equal(1); + scope.set('O', {x: 5}); + expect(evaluate(await first('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 first('x ||= true'), {scope})) + .to.deep.include({value: true}); + expect(scope.get('x')) + .to.equal(true); + scope.set('O', {x: false}); + expect(evaluate(await first('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 first('x &&= true'), {scope})) + .to.deep.include({value: true}); + expect(scope.get('x')) + .to.equal(true); + expect(evaluate(await first('x &&= false'), {scope})) + .to.deep.include({value: false}); + expect(scope.get('x')) + .to.equal(false); + scope.set('O', {x: true}); + expect(evaluate(await first('O.x &&= true'), {scope})) + .to.deep.include({value: true}); + expect(scope.get('O').x) + .to.equal(true); + expect(evaluate(await first('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 first('x ??= 2'), {scope})) + .to.deep.include({value: 2}); + expect(scope.get('x')) + .to.equal(2); + expect(evaluate(await first('x ??= 4'), {scope})) + .to.deep.include({value: 2}); + expect(scope.get('x')) + .to.equal(2); + scope.set('O', {x: null}); + expect(evaluate(await first('O.x ??= 2'), {scope})) + .to.deep.include({value: 2}); + expect(scope.get('O').x) + .to.equal(2); + expect(evaluate(await first('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 first('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 first('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 first('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 first('(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); +}); diff --git a/app/swcx/evaluators/await.js b/app/swcx/evaluators/await.js new file mode 100644 index 0000000..0e3358d --- /dev/null +++ b/app/swcx/evaluators/await.js @@ -0,0 +1,7 @@ +export default function(node, {evaluate, scope}) { + const {value} = evaluate(node.argument, {scope}); + return { + async: true, + value: value instanceof Promise ? value : Promise.resolve(value), + }; +} diff --git a/app/swcx/evaluators/await.test.js b/app/swcx/evaluators/await.test.js new file mode 100644 index 0000000..e360447 --- /dev/null +++ b/app/swcx/evaluators/await.test.js @@ -0,0 +1,20 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates await expressions', async () => { + const evaluated = evaluate(await first('await 1')); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(1); +}); + +test('coalesces promises', async () => { + const evaluated = evaluate(await first('await await await 1')); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(1); +}); diff --git a/app/swcx/evaluators/binary.js b/app/swcx/evaluators/binary.js new file mode 100644 index 0000000..748bb42 --- /dev/null +++ b/app/swcx/evaluators/binary.js @@ -0,0 +1,45 @@ +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 {value: binary(left.value, right.value)}; +} diff --git a/app/swcx/evaluators/binary.test.js b/app/swcx/evaluators/binary.test.js new file mode 100644 index 0000000..7c2fc3d --- /dev/null +++ b/app/swcx/evaluators/binary.test.js @@ -0,0 +1,177 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates +', async () => { + expect(evaluate(await first('10 + 2'))) + .to.deep.include({value: 12}); +}); + +test('evaluates -', async () => { + expect(evaluate(await first('10 - 2'))) + .to.deep.include({value: 8}); +}); + +test('evaluates /', async () => { + expect(evaluate(await first('10 / 2'))) + .to.deep.include({value: 5}); +}); + +test('evaluates %', async () => { + expect(evaluate(await first('10 % 3'))) + .to.deep.include({value: 1}); +}); + +test('evaluates *', async () => { + expect(evaluate(await first('10 * 2'))) + .to.deep.include({value: 20}); +}); + +test('evaluates >', async () => { + expect(evaluate(await first('10 > 2'))) + .to.deep.include({value: true}); +}); + +test('evaluates <', async () => { + expect(evaluate(await first('10 < 2'))) + .to.deep.include({value: false}); +}); + +test('evaluates in', async () => { + expect(evaluate(await first('"i" in {i: 69}'))) + .to.deep.include({value: true}); + expect(evaluate(await first('"j" in {i: 69}'))) + .to.deep.include({value: false}); +}); + +test('evaluates >=', async () => { + expect(evaluate(await first('10 >= 2'))) + .to.deep.include({value: true}); + expect(evaluate(await first('2 >= 2'))) + .to.deep.include({value: true}); + expect(evaluate(await first('1 >= 2'))) + .to.deep.include({value: false}); +}); + +test('evaluates <=', async () => { + expect(evaluate(await first('10 <= 2'))) + .to.deep.include({value: false}); + expect(evaluate(await first('2 <= 2'))) + .to.deep.include({value: true}); + expect(evaluate(await first('1 <= 2'))) + .to.deep.include({value: true}); +}); + +test('evaluates **', async () => { + expect(evaluate(await first('2 ** 16'))) + .to.deep.include({value: 65536}); +}); + +test('evaluates ===', async () => { + expect(evaluate(await first('10 === "10"'))) + .to.deep.include({value: false}); + expect(evaluate(await first('10 === 10'))) + .to.deep.include({value: true}); +}); + +test('evaluates !==', async () => { + expect(evaluate(await first('10 !== "10"'))) + .to.deep.include({value: true}); + expect(evaluate(await first('10 !== 10'))) + .to.deep.include({value: false}); +}); + +test('evaluates ^', async () => { + expect(evaluate(await first('7 ^ 2'))) + .to.deep.include({value: 5}); +}); + +test('evaluates &', async () => { + expect(evaluate(await first('5 & 3'))) + .to.deep.include({value: 1}); +}); + +test('evaluates |', async () => { + expect(evaluate(await first('1 | 2'))) + .to.deep.include({value: 3}); +}); + +test('evaluates >>', async () => { + expect(evaluate(await first('8 >> 1'))) + .to.deep.include({value: 4}); +}); + +test('evaluates <<', async () => { + expect(evaluate(await first('2 << 1'))) + .to.deep.include({value: 4}); +}); + +test('evaluates >>>', async () => { + expect(evaluate(await first('-1 >>> 1'))) + .to.deep.include({value: 2147483647}); +}); + +test('evaluates ==', async () => { + expect(evaluate(await first('10 == "10"'))) + .to.deep.include({value: true}); + expect(evaluate(await first('10 == 10'))) + .to.deep.include({value: true}); + expect(evaluate(await first('10 == "ten"'))) + .to.deep.include({value: false}); +}); + +test('evaluates !=', async () => { + expect(evaluate(await first('10 != "10"'))) + .to.deep.include({value: false}); + expect(evaluate(await first('10 != 10'))) + .to.deep.include({value: false}); + expect(evaluate(await first('10 != "ten"'))) + .to.deep.include({value: true}); +}); + +test('evaluates ||', async () => { + expect(evaluate(await first('true || true'))) + .to.deep.include({value: true}); + expect(evaluate(await first('true || false'))) + .to.deep.include({value: true}); + expect(evaluate(await first('false || false'))) + .to.deep.include({value: false}); +}); + +test('evaluates &&', async () => { + expect(evaluate(await first('true && true'))) + .to.deep.include({value: true}); + expect(evaluate(await first('true && false'))) + .to.deep.include({value: false}); + expect(evaluate(await first('false && false'))) + .to.deep.include({value: false}); +}); + +test('evaluates ??', async () => { + const scope = { + get() { return undefined; }, + }; + expect(evaluate(await first('null ?? 1'))) + .to.deep.include({value: 1}); + expect(evaluate(await first('undefined ?? 1'), {scope})) + .to.deep.include({value: 1}); + expect(evaluate(await first('2 ?? 1'))) + .to.deep.include({value: 2}); +}); + +test('evaluates instanceof', async () => { + const scope = { + get() { return Object; }, + }; + expect(evaluate(await first('({}) instanceof Object'), {scope})) + .to.deep.include({value: true}); +}); + +test('evaluates promised expressions', async () => { + const evaluated = evaluate(await first('(await 1) + (await 2)')); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(3); +}); diff --git a/app/swcx/evaluators/call.js b/app/swcx/evaluators/call.js new file mode 100644 index 0000000..1be701c --- /dev/null +++ b/app/swcx/evaluators/call.js @@ -0,0 +1,53 @@ +import fastCall from '@/util/fast-call.js'; +import { + isComputed, + isMemberExpression, + unwrap, +} from '@/swcx/types.js'; + +export default function(node, {evaluate, scope}) { + let asyncArgs = false; + const args = []; + for (let i = 0; i < node.arguments.length; i++) { + const {expression: arg} = node.arguments[i]; + const {async, value} = evaluate(arg, {scope}); + asyncArgs ||= async; + args.push(value); + } + const {callee: wrappedCallee} = node; + const callee = unwrap(wrappedCallee); + const callOptional = callee.wrapper?.optional || node.wrapper?.optional; + const invoke = (fn, holder, args) => { + if (callOptional && !fn) { + return undefined; + } + return fastCall(fn, holder, args); + }; + if (!isMemberExpression(callee)) { + 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 { + object, + property, + } = callee; + const O = evaluate(object, {scope}); + const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; + if (asyncArgs || O.async || P.async) { + return { + async: true, + value: Promise + .all([O.value, P.value, Promise.all(args)]) + .then(([O, P, args]) => invoke(callOptional ? O?.[P] : O[P], O, args)), + }; + } + return {value: invoke(callOptional ? O.value?.[P.value] : O.value[P.value], O.value, args)}; +} diff --git a/app/swcx/evaluators/call.test.js b/app/swcx/evaluators/call.test.js new file mode 100644 index 0000000..3649f7b --- /dev/null +++ b/app/swcx/evaluators/call.test.js @@ -0,0 +1,64 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.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 first('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 first('f(await 1, 2, 3)'), {scope}); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(6); + const evaluatedOptional = evaluate(await first('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 first('O.f(1, 2, 3)'), {scope}).value) + .to.equal(6); + expect(evaluate(await first('O["f"](1, 2, 3)'), {scope}).value) + .to.equal(6); +}); + +scopeTest('evaluates optional calls', async ({scope}) => { + scope.set('O', {}); + expect(evaluate(await first('g?.(1, 2, 3)'), {scope}).value) + .to.equal(undefined); + expect(evaluate(await first('O?.g(1, 2, 3)'), {scope}).value) + .to.equal(undefined); + expect(evaluate(await first('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 first('O.f(await 1, 2, 3)'), {scope}); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(6); +}); diff --git a/app/swcx/evaluators/conditional.js b/app/swcx/evaluators/conditional.js new file mode 100644 index 0000000..94aa11c --- /dev/null +++ b/app/swcx/evaluators/conditional.js @@ -0,0 +1,13 @@ +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}); +} diff --git a/app/swcx/evaluators/conditional.test.js b/app/swcx/evaluators/conditional.test.js new file mode 100644 index 0000000..46781b0 --- /dev/null +++ b/app/swcx/evaluators/conditional.test.js @@ -0,0 +1,43 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.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 first('x ? 2 : 3'), {scope}); + expect(evaluated.value) + .to.equal(2); + scope.set('x', false); + evaluated = evaluate(await first('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 first('(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 first('(await x) ? 2 : 3'), {scope}); + expect(await evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(3); +}); + diff --git a/app/swcx/evaluators/identifier.js b/app/swcx/evaluators/identifier.js new file mode 100644 index 0000000..5b9f690 --- /dev/null +++ b/app/swcx/evaluators/identifier.js @@ -0,0 +1,3 @@ +export default function(node, {scope}) { + return {value: scope.get(node.value)}; +} diff --git a/app/swcx/evaluators/literal.js b/app/swcx/evaluators/literal.js new file mode 100644 index 0000000..7a45d68 --- /dev/null +++ b/app/swcx/evaluators/literal.js @@ -0,0 +1,3 @@ +export default function({value}) { + return {value}; +} diff --git a/app/swcx/evaluators/literal.test.js b/app/swcx/evaluators/literal.test.js new file mode 100644 index 0000000..2b19e1e --- /dev/null +++ b/app/swcx/evaluators/literal.test.js @@ -0,0 +1,14 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates numeric literals', async () => { + expect(evaluate(await first('1'))) + .to.deep.include({value: 1}); +}); + +test('evaluates string literals', async () => { + expect(evaluate(await first('"1"'))) + .to.deep.include({value: '1'}); +}); diff --git a/app/swcx/evaluators/member.js b/app/swcx/evaluators/member.js new file mode 100644 index 0000000..f52399a --- /dev/null +++ b/app/swcx/evaluators/member.js @@ -0,0 +1,16 @@ +import { + isComputed, +} from '@/swcx/types.js'; + +export default function({object, property, wrapper}, {evaluate, scope}) { + const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]); + const O = evaluate(object, {scope}); + const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value}; + if (O.async || P.async) { + return { + async: true, + value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)), + }; + } + return {value: member(O.value, P.value)}; +} diff --git a/app/swcx/evaluators/member.test.js b/app/swcx/evaluators/member.test.js new file mode 100644 index 0000000..96b6987 --- /dev/null +++ b/app/swcx/evaluators/member.test.js @@ -0,0 +1,44 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.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 first('O.x'), {scope}); + expect(evaluated.value) + .to.equal(32); +}); + +scopeTest('evaluates optional member expression', async ({scope}) => { + let evaluated; + evaluated = evaluate(await first('O?.y'), {scope}); + expect(evaluated.value) + .to.equal(undefined); +}); + +scopeTest('evaluates computed member expression', async ({scope}) => { + let evaluated; + evaluated = evaluate(await first('O["x"]'), {scope}); + expect(evaluated.value) + .to.equal(32); +}); + +scopeTest('evaluates async member expression', async ({scope}) => { + let evaluated; + evaluated = evaluate(await first('O[await "x"]'), {scope}); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(32); +}); diff --git a/app/swcx/evaluators/object.js b/app/swcx/evaluators/object.js new file mode 100644 index 0000000..5877507 --- /dev/null +++ b/app/swcx/evaluators/object.js @@ -0,0 +1,88 @@ +import { + isComputed, + isIdentifier, + isKeyValueProperty, + isNumericLiteral, + isSpreadElement, + isStringLiteral, +} from '@/swcx/types.js'; + +export default function(node, {evaluate, scope}) { + const {properties} = node; + let isAsync = false; + const entries = []; + for (let i = 0; i < properties.length; i++) { + if (isKeyValueProperty(properties[i])) { + const {key, value} = properties[i]; + let k; + if (isComputed(key)) { + k = evaluate(key, {scope}); + } + else if (isIdentifier(key)) { + k = {value: key.value}; + } + else if (isNumericLiteral(key)) { + k = {value: key.value}; + } + else if (isStringLiteral(key)) { + 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 (isSpreadElement(properties[i])) { + const {arguments: 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), + }; +} diff --git a/app/swcx/evaluators/object.test.js b/app/swcx/evaluators/object.test.js new file mode 100644 index 0000000..c1537f4 --- /dev/null +++ b/app/swcx/evaluators/object.test.js @@ -0,0 +1,60 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates object expression', async () => { + let evaluated; + evaluated = evaluate(await first(`({ + ["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 first(`({ + 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 first(`({ + 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 first(`({ + foo: 16, + ...(await {bar: 32}), + })`)); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.deep.equal({ + foo: 16, + bar: 32, + }); +}); diff --git a/app/swcx/evaluators/unary.js b/app/swcx/evaluators/unary.js new file mode 100644 index 0000000..0418eb1 --- /dev/null +++ b/app/swcx/evaluators/unary.js @@ -0,0 +1,24 @@ +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 {value: unary(arg.value)}; +} diff --git a/app/swcx/evaluators/unary.test.js b/app/swcx/evaluators/unary.test.js new file mode 100644 index 0000000..87b4f14 --- /dev/null +++ b/app/swcx/evaluators/unary.test.js @@ -0,0 +1,42 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.js'; + +test('evaluates +', async () => { + expect(evaluate(await first('+1'))) + .to.deep.include({value: 1}); +}); + +test('evaluates -', async () => { + expect(evaluate(await first('-1'))) + .to.deep.include({value: -1}); +}); + +test('evaluates !', async () => { + expect(evaluate(await first('!true'))) + .to.deep.include({value: false}); +}); + +test('evaluates ~', async () => { + expect(evaluate(await first('~1'))) + .to.deep.include({value: -2}); +}); + +test('evaluates typeof', async () => { + expect(evaluate(await first('typeof "a"'))) + .to.deep.include({value: 'string'}); +}); + +test('evaluates void', async () => { + expect(evaluate(await first('void 0'))) + .to.deep.include({value: undefined}); +}); + +test('evaluates promised unary expression', async () => { + const evaluated = evaluate(await first('-(await 4)')); + expect(evaluated.async) + .to.equal(true); + expect(await evaluated.value) + .to.equal(-4); +}); diff --git a/app/swcx/evaluators/update.js b/app/swcx/evaluators/update.js new file mode 100644 index 0000000..7336f88 --- /dev/null +++ b/app/swcx/evaluators/update.js @@ -0,0 +1,23 @@ +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.value, value + 1); + case '--': return scope.set(argument.value, value - 1); + } + } + switch (operator) { + case '++': + scope.set(argument.value, value + 1); + return value; + case '--': + scope.set(argument.value, value - 1); + return value; + } + /* v8 ignore next */ + throw new Error(`operator not implemented: ${operator}`); + }; + return {value: update(value)}; +} diff --git a/app/swcx/evaluators/update.test.js b/app/swcx/evaluators/update.test.js new file mode 100644 index 0000000..f9318fc --- /dev/null +++ b/app/swcx/evaluators/update.test.js @@ -0,0 +1,50 @@ +import {expect, test} from 'vitest'; + +import {first} from '@/swcx/builders.js'; +import evaluate from '@/swcx/evaluate.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 first('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 first('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 first('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 first('y = --x'), {scope}); + expect(evaluated.value) + .to.equal(4); + expect(scope.get('x')) + .to.equal(4); + expect(scope.get('y')) + .to.equal(4); +}); diff --git a/app/swcx/sandbox.js b/app/swcx/sandbox.js new file mode 100644 index 0000000..3364c97 --- /dev/null +++ b/app/swcx/sandbox.js @@ -0,0 +1,266 @@ +import evaluate from '@/swcx/evaluate.js'; +import Scope from '@/swcx/scope.js'; +import traverse, {TRAVERSAL_PATH} from '@/swcx/traverse.js'; +import { + isArrayPattern, + isBlockStatement, + isDoWhileStatement, + isExpressionStatement, + isForStatement, + isIdentifier, + isIfStatement, + isObjectPattern, + isVariableDeclarator, + isWhileStatement, +} from '@/swcx/types.js'; + +export default class Sandbox { + + ast; + generator; + scopes; + + constructor(ast, context = {}) { + this.ast = ast; + this.$$context = context; + this.compile(); + } + + compile() { + let scope = new Scope(); + scope.context = this.$$context; + this.scopes = new WeakMap([[this.ast, scope]]); + traverse( + this.ast, + (node, verb) => { + if ( + isBlockStatement(node) + || isForStatement(node) + ) { + 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.scopes.get(this.ast).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; + } + if (isIdentifier(element)) { + scope.allocate(element.value, init[i]); + } + /* v8 ignore next 3 */ + else { + throw new Error(`destructureArray(): Can't array destructure type ${element.type}`); + } + } + return undefined; + } + + 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 (isObjectPattern(property.value)) { + this.destructureObject(property.value, init[property.key.value], scope); + } + else { + scope.allocate( + property.value ? property.value.value : property.key.value, + init[property.key.value], + ); + } + } + } + + evaluate(node) { + return evaluate( + node, + {scope: this.scopes.get(node)}, + ); + } + + *execute(node, parent) { + let keys = TRAVERSAL_PATH[node.type]; + if (isVariableDeclarator(node)) { + const {id} = node; + const scope = this.scopes.get(node); + if (null === node.init) { + scope.allocate(id.value, undefined); + } + else { + const init = this.evaluate(node.init); + if (isIdentifier(id)) { + if (init.async) { + yield { + async: true, + value: Promise.resolve(init.value).then((value) => { + scope.allocate(id.value, value); + }), + }; + } + else { + scope.allocate(id.value, init.value); + } + } + else if (isArrayPattern(id)) { + const promiseOrVoid = init.async + ? Promise.resolve(init.value).then((init) => this.destructureArray(id, init)) + : this.destructureArray(id, init.value); + if (promiseOrVoid) { + yield { + async: true, + value: promiseOrVoid, + }; + } + } + else if (isObjectPattern(id)) { + const promiseOrVoid = init.async + ? Promise.resolve(init.value).then((init) => this.destructureObject(id, init)) + : this.destructureObject(id, init.value); + if (promiseOrVoid) { + yield { + async: true, + value: promiseOrVoid, + }; + } + } + } + } + // Blocks... + if (isIfStatement(node)) { + const {async, value} = this.evaluate(node.test); + const branch = (value) => { + keys = [value ? 'consequent' : 'alternate']; + }; + if (async) { + yield { + async: true, + value: Promise.resolve(value).then(branch), + }; + } + else { + branch(value); + } + } + // Loops... + let loop = false; + if (isForStatement(node)) { + const {value} = this.execute(node.init, node).next(); + if (value?.async) { + yield value; + } + } + do { + if ( + isForStatement(node) + || isWhileStatement(node) + ) { + const {async, value} = this.evaluate(node.test); + if (async) { + yield { + async: true, + value: Promise.resolve(value).then((value) => { + keys = value ? ['body'] : []; + }), + }; + } + else { + keys = value ? ['body'] : []; + } + loop = keys.length > 0; + } + // Recur... + let children = []; + if (keys instanceof Function) { + children = keys(node); + } + else { + for (const key of keys) { + children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]])); + } + } + for (const child of children) { + if (!child) { + continue; + } + yield* this.execute(child, node); + } + // Loops... + if (isForStatement(node)) { + const result = this.execute(node.update, node).next(); + if (result.value?.async) { + yield result.value; + } + } + if (isDoWhileStatement(node)) { + const test = this.evaluate(node.test); + if (test.async) { + yield { + async: true, + value: Promise.resolve(test.value).then((value) => { + loop = value; + }), + }; + } + else { + loop = test.value; + } + } + } while (loop); + if (isExpressionStatement(node)) { + yield this.evaluate(node.expression); + } + // yield ForStatement afterthought. + if (isForStatement(parent) && !isBlockStatement(node)) { + yield this.evaluate(node); + /* v8 ignore next */ + } + } + + run(ops = 1000) { + let result; + for (let i = 0; i < ops; ++i) { + result = this.step(); + if (result.done || result.value?.async) { + break; + } + } + return result; + } + + step() { + if (!this.generator) { + this.generator = this.execute(this.ast); + } + const result = this.generator.next(); + if (result.done) { + this.generator = undefined; + this.compile(); + } + return result; + } + +} diff --git a/app/swcx/sandbox.test.js b/app/swcx/sandbox.test.js new file mode 100644 index 0000000..30eaafc --- /dev/null +++ b/app/swcx/sandbox.test.js @@ -0,0 +1,197 @@ +import {expect, test} from 'vitest'; + +import {parse} from '@swc/core'; +import Sandbox from '@/swcx/sandbox.js'; + +test('declares variables', async () => { + const sandbox = new Sandbox( + await parse(` + const scalar = 1; + const asyncScalar = await 2; + const array = [3, 4, 5]; + const asyncArray = await [6, 7, 8]; + const object = {9: '10'}; + const asyncObject = await {11: '12'}; + `), + ); + let result; + do { + result = sandbox.step(); + if (result.value?.async) { + await result.value.async; + } + } while (!result.done); + 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('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}}; + `), + ); + let result; + do { + result = sandbox.step(); + if (result.value?.async) { + await result.value.value; + } + } while (!result.done); + 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 < 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('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; + } + `), + ); + let result; + do { + result = sandbox.step(); + if (result.value?.async) { + await result.value.value; + } + } while (!result.done); + expect(sandbox.context) + .to.deep.equal({ + foo: 1, + bar: 2, + }); +}); + +test('evaluates loops', async () => { + const sandbox = new Sandbox( + await parse(` + let x = 0, y = 0, a = 0, b = 0, c = 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 += 1; + } while (await b < 3); + while (c < 3) { + c += 1; + } + `), + ); + let result; + do { + result = sandbox.step(); + if (result.value?.async) { + await result.value.value; + } + } while (!result.done); + expect(sandbox.context) + .to.deep.equal({ + a: 3, + b: 3, + c: 3, + x: 3, + y: 3, + }); +}); + +test('retuns 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('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; +}); diff --git a/app/swcx/scope.js b/app/swcx/scope.js new file mode 100644 index 0000000..d8c5360 --- /dev/null +++ b/app/swcx/scope.js @@ -0,0 +1,40 @@ +export default class Scope { + + context = {}; + parent = null; + + constructor(parent) { + this.parent = parent; + } + + allocate(key, value) { + 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; + } + } + +} diff --git a/app/swcx/traverse.js b/app/swcx/traverse.js new file mode 100644 index 0000000..0cee08d --- /dev/null +++ b/app/swcx/traverse.js @@ -0,0 +1,63 @@ +export const TRAVERSAL_PATH = { + ArrayExpression: (node) => node.elements.map(({expression}) => expression), + ArrayPattern: ['elements'], + AssignmentExpression: ['left', 'right'], + AssignmentPatternProperty: ['key'], + AwaitExpression: ['argument'], + BinaryExpression: ['left', 'right'], + BlockStatement: ['stmts'], + BooleanLiteral: [], + CallExpression: (node) => ([ + node.callee, + ...node.arguments.map(({expression}) => expression), + ]), + Computed: ['expression'], + ConditionalExpression: ['alternate', 'consequent', 'test'], + DoWhileStatement: ['body', 'test'], + ExpressionStatement: ['expression'], + ForStatement: ['body', 'init', 'test', 'update'], + Identifier: [], + IfStatement: ['alternate', 'consequent', 'test'], + KeyValuePatternProperty: ['key', 'value'], + KeyValueProperty: ['key', 'value'], + MemberExpression: ['object', 'property'], + Module: ['body'], + NullLiteral: [], + NumericLiteral: [], + ObjectExpression: ['properties'], + ObjectPattern: ['properties'], + OptionalChainingExpression: ['base'], + ParenthesisExpression: ['expression'], + RegExpLiteral: [], + StringLiteral: [], + 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`); + } + visitor(node, 'enter'); + const path = TRAVERSAL_PATH[node.type]; + let children; + if (path instanceof Function) { + children = path(node); + } + else if (Array.isArray(path)) { + children = []; + for (const key of path) { + children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]])); + } + } + for (const child of children) { + if (child) { + traverse(child, visitor); + } + } + visitor(node, 'exit'); +} diff --git a/app/swcx/types.js b/app/swcx/types.js new file mode 100644 index 0000000..41071f1 --- /dev/null +++ b/app/swcx/types.js @@ -0,0 +1,143 @@ +export function isArrayPattern(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'ArrayPattern') { + return false; + } + return true; +} + +export function isBlockStatement(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'BlockStatement') { + return false; + } + return true; +} + +export function isComputed(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'Computed') { + return false; + } + return true; +} + +export function isDoWhileStatement(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'DoWhileStatement') { + return false; + } + return true; +} + +export function isExpressionStatement(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'ExpressionStatement') { + return false; + } + return true; +} + +export function isForStatement(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'ForStatement') { + return false; + } + return true; +} + +export function isIdentifier(node) { + if (!node || node.type !== 'Identifier') { + return false; + } + return true; +} + +export function isIfStatement(node) { + if (!node || node.type !== 'IfStatement') { + return false; + } + return true; +} + +export function isKeyValueProperty(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'KeyValueProperty') { + return false; + } + return true; +} + +export function isMemberExpression(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'MemberExpression') { + return false; + } + return true; +} + +export function isNumericLiteral(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'NumericLiteral') { + return false; + } + return true; +} + +export function isObjectPattern(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'ObjectPattern') { + return false; + } + return true; +} + +export function isSpreadElement(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'SpreadElement') { + return false; + } + return true; +} + +export function isStringLiteral(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'StringLiteral') { + return false; + } + return true; +} + +export function isVariableDeclarator(node) { + /* v8 ignore next 3 */ + if (!node || node.type !== 'VariableDeclarator') { + return false; + } + return true; +} + +export function isWhileStatement(node) { + if (!node || node.type !== 'WhileStatement') { + return false; + } + return true; +} + +export function unwrap(node) { + let wrapped = node; + switch (node.type) { + case 'Computed': + wrapped = unwrap(node.expression); + break; + case 'OptionalChainingExpression': + wrapped = unwrap(node.base); + break; + case 'ParenthesisExpression': + wrapped = unwrap(node.expression); + break; + } + if (node !== wrapped) { + wrapped.wrapper = node; + } + return wrapped; +} diff --git a/app/util/fast-call.js b/app/util/fast-call.js new file mode 100644 index 0000000..3329ee5 --- /dev/null +++ b/app/util/fast-call.js @@ -0,0 +1,36 @@ +export default function(fn, holder, args) { + if (holder) { + const {name} = fn; + if (name in holder && holder[name] === fn) { + switch (args.length) { + case 0 : return holder[name](); + case 1 : return holder[name](args[0]); + case 2 : return holder[name](args[0], args[1]); + case 3 : return holder[name](args[0], args[1], args[2]); + case 4 : return holder[name](args[0], args[1], args[2], args[3]); + case 5 : return holder[name](args[0], args[1], args[2], args[3], args[4]); + default: return holder[name](...args); + } + } + const bound = fn.bind(holder); + switch (args.length) { + case 0 : return bound(); + case 1 : return bound(args[0]); + case 2 : return bound(args[0], args[1]); + case 3 : return bound(args[0], args[1], args[2]); + case 4 : return bound(args[0], args[1], args[2], args[3]); + case 5 : return bound(args[0], args[1], args[2], args[3], args[4]); + default: return bound(...args); + } + } + switch (args.length) { + case 0 : return fn(); + case 1 : return fn(args[0]); + case 2 : return fn(args[0], args[1]); + case 3 : return fn(args[0], args[1], args[2]); + case 4 : return fn(args[0], args[1], args[2], args[3]); + case 5 : return fn(args[0], args[1], args[2], args[3], args[4]); + default: return fn(...args); + } +} + diff --git a/package-lock.json b/package-lock.json index 5a063bb..2ae73d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@remix-run/express": "^2.9.2", "@remix-run/node": "^2.9.2", "@remix-run/react": "^2.9.2", + "@swc/core": "^1.6.0", "compression": "^1.7.4", "express": "^4.18.2", "idb-keyval": "^6.2.1", @@ -6316,6 +6317,206 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@swc/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.0.tgz", + "integrity": "sha512-Wynbo79uIVBgmq3TPcTMdtXUkqk69IPSVuzo7/Jl1OhR4msC7cUaoRB1216ZanWttrAZ4/g6u17w9XZG4fzp1A==", + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.6.0", + "@swc/core-darwin-x64": "1.6.0", + "@swc/core-linux-arm-gnueabihf": "1.6.0", + "@swc/core-linux-arm64-gnu": "1.6.0", + "@swc/core-linux-arm64-musl": "1.6.0", + "@swc/core-linux-x64-gnu": "1.6.0", + "@swc/core-linux-x64-musl": "1.6.0", + "@swc/core-win32-arm64-msvc": "1.6.0", + "@swc/core-win32-ia32-msvc": "1.6.0", + "@swc/core-win32-x64-msvc": "1.6.0" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.0.tgz", + "integrity": "sha512-W1Mwk0WRrJ5lAVkYRPxpxOmwu8p9ASXeOmiORhXvE7DYREyI30005xlqSOITU1pfSNKj7G9u3+9DjsOzPPPbBw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.0.tgz", + "integrity": "sha512-EzxLnpPC1zgLb2Y0iVUG6b+/GUv43k6uJUIs52UzxOnBElYP/WeItI3RJ+LUMFzCpZMk/IxB10wofEoeQ1H/Xg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.0.tgz", + "integrity": "sha512-uP/STDjWZ5N6lc8mxJFsex4NXDaqhfzd8UOrI3LfdV97+4faE4/BC6bVqDNHFFzZi0PHuVBxD6md7IfPjugk6A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.0.tgz", + "integrity": "sha512-UgNz6anowcnYzJtZohzpii31FOgouBHJqluiq+p2geX/agbC+KfOKwVXdljn95+Qc4ygBuw/hjKjgF2msOLeVg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.0.tgz", + "integrity": "sha512-xPV6qrnj4nFwXQbIv70C1Kn5z5Th53sirIY76aEonr78qeC6+ywaBZR4uLFNHsljVjyuvVQfTTcl2qraGhu6oQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.0.tgz", + "integrity": "sha512-xTeWn4OT5uQ+DxT2cy94ngK8tF1U/5fMC49/V6FhCS2Wh+Xa/O+OWcOyKvYtk3b0eGYS4iNIRKgzog7fLSFtvQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.0.tgz", + "integrity": "sha512-3P01mYD5XbyaVLT0MGZmZE+ZdgmGSvuvIhSejRDBlEXqkFnH79nWds+KsE+91hzVU8XsgzX57Yzv4eO5dlIuPw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.0.tgz", + "integrity": "sha512-xFuook1efU0ctzMAEeol4eI7J6+k/c/pMJpp/NP/4JJDnhlHwAi2iyiZcID8YZS+ePHgXMLndGdIMHVv/wIPkQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.0.tgz", + "integrity": "sha512-VCJa5vTywxzASqvf9OEUM5SZBcNrWbuIkSGM5T9guuBzyrh/tSqVHjzOWL9qpP69uPVj5G/I5bJObLiUKErhvQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.0.tgz", + "integrity": "sha512-L7i8WBSIJTQiMONJGHnznDydZmlJIqHjZ3VhBHeTTms8cEAuwkAVgzPwgr5cD9GhmcwdeBI9iYdOuKr1pUx19Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/types": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz", + "integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", diff --git a/package.json b/package.json index c8d5709..d8d4431 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@remix-run/express": "^2.9.2", "@remix-run/node": "^2.9.2", "@remix-run/react": "^2.9.2", + "@swc/core": "^1.6.0", "compression": "^1.7.4", "express": "^4.18.2", "idb-keyval": "^6.2.1",