From b7a6ce0998c3e563c71fd3eee76d00c2976b14a5 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Sun, 4 Feb 2024 17:25:06 -0800 Subject: [PATCH] Add jsonr package --- README.md | 1 + codecov.yml | 3 + index.d.ts | 1 + index.js | 1 + package-lock.json | 15 + package.json | 2 + packages/commons/package.json | 2 +- packages/commons/src/async/promise.ts | 68 +++-- packages/cqrs/package.json | 2 +- packages/cqrs/src/__tests__/iterator.spec.ts | 8 +- packages/cqrs/src/iterator.ts | 2 +- packages/crdt/package.json | 2 +- packages/jsonr/LICENSE | 21 ++ packages/jsonr/README.md | 38 +++ packages/jsonr/jest.config.cjs | 1 + packages/jsonr/package.json | 47 +++ .../__snapshots__/integ.spec.ts.snap | 37 +++ packages/jsonr/src/__tests__/data/async.json | 7 + packages/jsonr/src/__tests__/data/macro.json | 9 + packages/jsonr/src/__tests__/env.spec.ts | 130 +++++++++ packages/jsonr/src/__tests__/integ.spec.ts | 64 +++++ packages/jsonr/src/__tests__/utils.spec.ts | 100 +++++++ packages/jsonr/src/env.ts | 73 +++++ packages/jsonr/src/index.ts | 7 + .../interpreter/__tests__/interpreter.spec.ts | 221 ++++++++++++++ packages/jsonr/src/interpreter/control.ts | 31 ++ packages/jsonr/src/interpreter/eval.ts | 270 ++++++++++++++++++ packages/jsonr/src/interpreter/fn.ts | 66 +++++ packages/jsonr/src/interpreter/index.ts | 1 + packages/jsonr/src/interpreter/interpreter.ts | 14 + packages/jsonr/src/interpreter/meta.ts | 59 ++++ .../jsonr/src/parser/__tests__/ast.spec.ts | 77 +++++ packages/jsonr/src/parser/ast.ts | 55 ++++ packages/jsonr/src/parser/index.ts | 1 + .../jsonr/src/stdlib/__tests__/ops.spec.ts | 44 +++ .../src/stdlib/__tests__/predicates.spec.ts | 53 ++++ .../jsonr/src/stdlib/__tests__/types.spec.ts | 63 ++++ packages/jsonr/src/stdlib/index.ts | 38 +++ packages/jsonr/src/stdlib/ops.ts | 111 +++++++ packages/jsonr/src/stdlib/predicates.ts | 41 +++ packages/jsonr/src/stdlib/types.ts | 86 ++++++ packages/jsonr/src/symbol.ts | 7 + packages/jsonr/src/types.ts | 112 ++++++++ packages/jsonr/src/utils.ts | 46 +++ packages/jsonr/tsconfig.build.json | 6 + packages/jsonr/tsconfig.json | 10 + packages/jsonr/typedoc.json | 8 + packages/messaging/package.json | 2 +- packages/messaging/src/impl/simple.ts | 16 +- packages/plugins/denokv/package.json | 2 +- packages/plugins/ipfs/package.json | 2 +- packages/plugins/level/package.json | 2 +- packages/plugins/redis/package.json | 2 +- 53 files changed, 2039 insertions(+), 48 deletions(-) create mode 100644 packages/jsonr/LICENSE create mode 100644 packages/jsonr/README.md create mode 100644 packages/jsonr/jest.config.cjs create mode 100644 packages/jsonr/package.json create mode 100644 packages/jsonr/src/__tests__/__snapshots__/integ.spec.ts.snap create mode 100644 packages/jsonr/src/__tests__/data/async.json create mode 100644 packages/jsonr/src/__tests__/data/macro.json create mode 100644 packages/jsonr/src/__tests__/env.spec.ts create mode 100644 packages/jsonr/src/__tests__/integ.spec.ts create mode 100644 packages/jsonr/src/__tests__/utils.spec.ts create mode 100644 packages/jsonr/src/env.ts create mode 100644 packages/jsonr/src/index.ts create mode 100644 packages/jsonr/src/interpreter/__tests__/interpreter.spec.ts create mode 100644 packages/jsonr/src/interpreter/control.ts create mode 100644 packages/jsonr/src/interpreter/eval.ts create mode 100644 packages/jsonr/src/interpreter/fn.ts create mode 100644 packages/jsonr/src/interpreter/index.ts create mode 100644 packages/jsonr/src/interpreter/interpreter.ts create mode 100644 packages/jsonr/src/interpreter/meta.ts create mode 100644 packages/jsonr/src/parser/__tests__/ast.spec.ts create mode 100644 packages/jsonr/src/parser/ast.ts create mode 100644 packages/jsonr/src/parser/index.ts create mode 100644 packages/jsonr/src/stdlib/__tests__/ops.spec.ts create mode 100644 packages/jsonr/src/stdlib/__tests__/predicates.spec.ts create mode 100644 packages/jsonr/src/stdlib/__tests__/types.spec.ts create mode 100644 packages/jsonr/src/stdlib/index.ts create mode 100644 packages/jsonr/src/stdlib/ops.ts create mode 100644 packages/jsonr/src/stdlib/predicates.ts create mode 100644 packages/jsonr/src/stdlib/types.ts create mode 100644 packages/jsonr/src/symbol.ts create mode 100644 packages/jsonr/src/types.ts create mode 100644 packages/jsonr/src/utils.ts create mode 100644 packages/jsonr/tsconfig.build.json create mode 100644 packages/jsonr/tsconfig.json create mode 100644 packages/jsonr/typedoc.json diff --git a/README.md b/README.md index 900fcffb..01ffad27 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Core: |[`@mithic/commons`](./packages/commons)|[![npm](https://img.shields.io/npm/v/@mithic/commons?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/commons)|Common utilities| |[`@mithic/cqrs`](./packages/cqrs)|[![npm](https://img.shields.io/npm/v/@mithic/cqrs?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/cqrs)|CQRS interface| |[`@mithic/crdt`](./packages/crdt)|[![npm](https://img.shields.io/npm/v/@mithic/crdt?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/crdt)|Eventsourced CRDT library| +|[`@mithic/jsonr`](./packages/jsonr)|[![npm](https://img.shields.io/npm/v/@mithic/jsonr?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/jsonr)|JSON intermediate representation for sandboxed scripting| |[`@mithic/messaging`](./packages/messaging)|[![npm](https://img.shields.io/npm/v/@mithic/messaging?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/messaging)|Messaging interface| Plugins: diff --git a/codecov.yml b/codecov.yml index 4d19c493..bfb45dc2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,6 +14,9 @@ component_management: - component_id: "@mithic/crdt" paths: - "packages/crdt" + - component_id: "@mithic/jsonr" + paths: + - "packages/jsonr" - component_id: "@mithic/messaging" paths: - "packages/messaging" diff --git a/index.d.ts b/index.d.ts index ad6f6f2c..a5608061 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,4 +2,5 @@ export * from '@mithic/commons'; export * from '@mithic/collections'; export * from '@mithic/cqrs'; export * from '@mithic/crdt'; +export * from '@mithic/jsonr'; export * from '@mithic/messaging'; diff --git a/index.js b/index.js index ad6f6f2c..a5608061 100644 --- a/index.js +++ b/index.js @@ -2,4 +2,5 @@ export * from '@mithic/commons'; export * from '@mithic/collections'; export * from '@mithic/cqrs'; export * from '@mithic/crdt'; +export * from '@mithic/jsonr'; export * from '@mithic/messaging'; diff --git a/package-lock.json b/package-lock.json index 2ea4d100..8eab21b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "workspaces": [ "./packages/commons", "./packages/collections", + "./packages/jsonr", "./packages/messaging", "./packages/cqrs", "./packages/crdt", @@ -25,6 +26,7 @@ "@mithic/commons": "^0.3", "@mithic/cqrs": "^0.3", "@mithic/crdt": "^0.3", + "@mithic/jsonr": "^0.3", "@mithic/messaging": "^0.3" }, "devDependencies": { @@ -2922,6 +2924,10 @@ "resolved": "packages/plugins/ipfs", "link": true }, + "node_modules/@mithic/jsonr": { + "resolved": "packages/jsonr", + "link": true + }, "node_modules/@mithic/level": { "resolved": "packages/plugins/level", "link": true @@ -8561,6 +8567,15 @@ }, "devDependencies": {} }, + "packages/jsonr": { + "name": "@mithic/jsonr", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "@mithic/commons": "^0.3" + }, + "devDependencies": {} + }, "packages/messaging": { "name": "@mithic/messaging", "version": "0.3.0", diff --git a/package.json b/package.json index 094a627d..d3018515 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@mithic/commons": "^0.3", "@mithic/cqrs": "^0.3", "@mithic/crdt": "^0.3", + "@mithic/jsonr": "^0.3", "@mithic/messaging": "^0.3" }, "devDependencies": { @@ -75,6 +76,7 @@ "workspaces": [ "./packages/commons", "./packages/collections", + "./packages/jsonr", "./packages/messaging", "./packages/cqrs", "./packages/crdt", diff --git a/packages/commons/package.json b/packages/commons/package.json index 81236b86..32a2df78 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -19,7 +19,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "test:update": "npm run test -- -u", diff --git a/packages/commons/src/async/promise.ts b/packages/commons/src/async/promise.ts index 9861adba..e5f1e333 100644 --- a/packages/commons/src/async/promise.ts +++ b/packages/commons/src/async/promise.ts @@ -45,8 +45,38 @@ export type ReduceAsync = { ): MaybePromise; }; -/** Reduces a {@link MaybePromise} of array using a maybe-async reducer function. */ -export const reduceAsync = maybeAsync(reduce) as ReduceAsync; +function* emptyCoroutine() { } + +class MaybeAsyncCorountine { + private coroutine: Generator, MaybePromise, V> = + emptyCoroutine() as Generator, MaybePromise, V>; + + public constructor() { + this.run = this.run.bind(this); + this.resume = this.resume.bind(this); + } + + public start(coroutine: Generator, MaybePromise, V>): MaybePromise { + this.coroutine = coroutine; + return this.run(); + } + + private run(resolved?: V): MaybePromise { + let result; + while (!(result = resolved === void 0 ? this.coroutine.next() : this.coroutine.next(resolved)).done) { + const value = result.value; + if (isThenable(value)) { return value.then(this.run, this.resume); } + resolved = value; + } + return result.value; + } + + private resume(e: unknown): MaybePromise { + const { done, value } = this.coroutine.throw(e); + if (done) { return value; } + return isThenable(value) ? value.then(this.run, this.resume) : this.run(value); + } +} /** * Wraps a {@link MaybePromise}-yielding coroutine (generator function) into a function that returns {@link MaybePromise}. @@ -65,35 +95,13 @@ export const reduceAsync = maybeAsync(reduce) as ReduceAsync; */ export function maybeAsync( // eslint-disable-next-line @typescript-eslint/no-explicit-any - coroutine: (...args: Args) => Generator, any>, + coroutineFn: (...args: Args) => Generator, any>, thisArg?: unknown, ): (...args: Args) => MaybePromise { - return (...args) => { - return new MaybeAsyncCorountine(coroutine.call(thisArg, ...args)).run(); - }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const coroutine = new MaybeAsyncCorountine(); + return (...args) => coroutine.start(coroutineFn.call(thisArg, ...args)); } -class MaybeAsyncCorountine { - public constructor( - private readonly iter: Generator, MaybePromise, V>, - ) { - this.run = this.run.bind(this); - this.resume = this.resume.bind(this); - } - - public run(resolved?: V): MaybePromise { - let result; - while (!(result = resolved === void 0 ? this.iter.next() : this.iter.next(resolved)).done) { - const value = result.value; - if (isThenable(value)) { return value.then(this.run, this.resume); } - resolved = value; - } - return result.value; - } - - public resume(e: unknown): MaybePromise { - const { done, value } = this.iter.throw(e); - if (done) { return value; } - return isThenable(value) ? value.then(this.run, this.resume) : this.run(value); - } -} +/** Reduces a {@link MaybePromise} of array using a maybe-async reducer function. */ +export const reduceAsync = maybeAsync(reduce) as ReduceAsync; diff --git a/packages/cqrs/package.json b/packages/cqrs/package.json index a00f85d8..6c78cc08 100644 --- a/packages/cqrs/package.json +++ b/packages/cqrs/package.json @@ -19,7 +19,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/cqrs/src/__tests__/iterator.spec.ts b/packages/cqrs/src/__tests__/iterator.spec.ts index 1cf9ed73..c2821760 100644 --- a/packages/cqrs/src/__tests__/iterator.spec.ts +++ b/packages/cqrs/src/__tests__/iterator.spec.ts @@ -59,7 +59,9 @@ describe(AsyncSubscriber.name, () => { const controller = new AbortController(); const subscriber = new AsyncSubscriber(eventBus, controller); const events = [1, 2, 3]; - events.forEach((event) => eventBus.dispatch(event)); + for (const event of events) { + await eventBus.dispatch(event); + } const result = []; let actualError; @@ -125,7 +127,9 @@ describe(AsyncSubscriber.name, () => { it('should ignore new values on fcfs mode if buffer is full', async () => { const subscriber = new AsyncSubscriber(eventBus, { bufferSize: 1, fcfs: true }); const events = [1, 2, 3]; - events.forEach((event) => eventBus.dispatch(event)); + for (const event of events) { + await eventBus.dispatch(event); + } for await (const event of subscriber) { expect(event).toEqual(events[0]); // later events dropped diff --git a/packages/cqrs/src/iterator.ts b/packages/cqrs/src/iterator.ts index 97780b1d..edbc6678 100644 --- a/packages/cqrs/src/iterator.ts +++ b/packages/cqrs/src/iterator.ts @@ -70,7 +70,7 @@ export class AsyncSubscriber implements AsyncIterableIterator, return this.close(); } - private async push(value: Message) { + private push(value: Message) { const resolve = this.pullQueue.shift(); if (resolve) { resolve(this.running ? { value, done: false } : { value: void 0, done: true }); diff --git a/packages/crdt/package.json b/packages/crdt/package.json index 0e8b296f..d76202db 100644 --- a/packages/crdt/package.json +++ b/packages/crdt/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/jsonr/LICENSE b/packages/jsonr/LICENSE new file mode 100644 index 00000000..4b56614b --- /dev/null +++ b/packages/jsonr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Andy K. S. Wong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/jsonr/README.md b/packages/jsonr/README.md new file mode 100644 index 00000000..25cdc1b6 --- /dev/null +++ b/packages/jsonr/README.md @@ -0,0 +1,38 @@ +# @mithic/jsonr + +[![mithic](https://img.shields.io/badge/project-mithic-blueviolet.svg?style=flat-square&logo=github)](https://github.com/andykswong/mithic) +[![npm](https://img.shields.io/npm/v/@mithic/jsonr?style=flat-square&logo=npm)](https://www.npmjs.com/package/@mithic/jsonr) +[![docs](https://img.shields.io/badge/docs-typedoc-blue?style=flat-square&logo=typescript&logoColor=white)](http://andykswong.github.io/mithic) +[![license: MIT](https://img.shields.io/badge/License-MIT-red.svg?style=flat-square)](./LICENSE) +[![build](https://img.shields.io/github/actions/workflow/status/andykswong/mithic/build.yaml?style=flat-square)](https://github.com/andykswong/mithic/actions/workflows/build.yaml) + +
+ +> mithic JSON intermediate representation for sandboxed scripting + +
+ +## Install +```shell +npm install --save @mithic/jsonr +``` + +## Basic Usage +To run an expression: +1. Use a `Parser` (`JsonAstParser`) to parse JSON string into AST +1. Use an `Evaluator` (currently `Interpreter` only) to evaluate the AST against an `Env` (`DefaultEnv`) to get a result as JS object + +#### Simplified JavaScript +```js +import { DefaultEnv, Interpreter, JsonAstParser, Stdlib } from '@mithic/jsonr'; + +const env = new DefaultEnv(null, Stdlib); +const parser = new JsonAstParser(); +const evaluator = new Interpreter(); + +const ast = parser.parse('["+",3,5]'); +const result = evaluator.eval(ast, env); // 8 +``` + +## License +This repository and the code inside it is licensed under the MIT License. Read [LICENSE](./LICENSE) for more information. diff --git a/packages/jsonr/jest.config.cjs b/packages/jsonr/jest.config.cjs new file mode 100644 index 00000000..ef11b719 --- /dev/null +++ b/packages/jsonr/jest.config.cjs @@ -0,0 +1 @@ +module.exports = require('../../jest.config.cjs'); diff --git a/packages/jsonr/package.json b/packages/jsonr/package.json new file mode 100644 index 00000000..2fb6f84c --- /dev/null +++ b/packages/jsonr/package.json @@ -0,0 +1,47 @@ +{ + "name": "@mithic/jsonr", + "version": "0.3.0", + "description": "JSON intermediate representation for sandboxed scripting", + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "/dist" + ], + "scripts": { + "prepublishOnly": "npm run clean && npm run build && npm test && npm run doc", + "clean": "rimraf coverage docs dist", + "prebuild": "npm run lint", + "build": "npm run tsc && npm run babel", + "lint": "eslint src --ext .ts", + "babel": "babel src -d dist -x '.ts,.tsx' --root-mode upward", + "tsc": "tsc --project tsconfig.build.json", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", + "test:update": "npm run test -- -u", + "prestart": "npm run build", + "doc": "typedoc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/andykswong/mithic.git" + }, + "keywords": [ + "mithic", + "typescript" + ], + "author": "Andy K.S. Wong ", + "license": "MIT", + "bugs": { + "url": "https://github.com/andykswong/mithic/issues" + }, + "homepage": "https://github.com/andykswong/mithic", + "dependencies": { + "@mithic/commons": "^0.3" + }, + "devDependencies": { + } +} diff --git a/packages/jsonr/src/__tests__/__snapshots__/integ.spec.ts.snap b/packages/jsonr/src/__tests__/__snapshots__/integ.spec.ts.snap new file mode 100644 index 00000000..f94f661a --- /dev/null +++ b/packages/jsonr/src/__tests__/__snapshots__/integ.spec.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Integration tests async should catch rejected promise from readline 1`] = ` +[ + [ + "you said:", + "", + ], +] +`; + +exports[`Integration tests async should print the correct results 1`] = ` +[ + [ + "you said:", + "testing", + ], +] +`; + +exports[`Integration tests macro should print the correct results 1`] = ` +[ + [ + 2, + ], + [ + 3, + ], + [ + [ + "+", + 1, + 2, + ], + ], +] +`; diff --git a/packages/jsonr/src/__tests__/data/async.json b/packages/jsonr/src/__tests__/data/async.json new file mode 100644 index 00000000..df70719b --- /dev/null +++ b/packages/jsonr/src/__tests__/data/async.json @@ -0,0 +1,7 @@ +[ + ";", + ["async", "echo", ["prompt"], ["try", ["await", ["readline", "prompt", 2000]], ["catch", "e", "'"]]], + ["let", "input", ["await", ["echo", "'your input: "]]], + ["println", "'you said:", "input"], + null +] diff --git a/packages/jsonr/src/__tests__/data/macro.json b/packages/jsonr/src/__tests__/data/macro.json new file mode 100644 index 00000000..e56365a5 --- /dev/null +++ b/packages/jsonr/src/__tests__/data/macro.json @@ -0,0 +1,9 @@ +[ + ";", + ["fn", "x", ["n"], ["+", "n", 1]], + ["macro", "y", ["a", "op", "b"], ["[]", "op", "a", "b"]], + ["println", ["x", 1]], + ["println", ["y", 1, "+", 2]], + ["println", ["macroexpand", ["y", 1, "+", 2]]], + null +] diff --git a/packages/jsonr/src/__tests__/env.spec.ts b/packages/jsonr/src/__tests__/env.spec.ts new file mode 100644 index 00000000..1edf5760 --- /dev/null +++ b/packages/jsonr/src/__tests__/env.spec.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from '@jest/globals'; +import { ProtoChainEnv } from '../env.ts'; + +describe(ProtoChainEnv, () => { + describe('constructor', () => { + it('should have the correct binding chain', () => { + const global = new ProtoChainEnv(); + const parent = new ProtoChainEnv(global); + const child = new ProtoChainEnv(parent); + + expect(Object.getPrototypeOf(global['binds'])).toBe(null); + expect(Object.getPrototypeOf(parent['binds'])).toBe(global['binds']); + expect(Object.getPrototypeOf(child['binds'])).toBe(parent['binds']); + }); + }); + + describe('global', () => { + it('should carry global scope from parent', () => { + const global = new ProtoChainEnv(); + const parent = new ProtoChainEnv(global); + const child = new ProtoChainEnv(parent); + + expect(global.global).toBe(global); + expect(parent.global).toBe(global); + expect(child.global).toBe(global); + }); + }); + + describe('async', () => { + it('should be true for global scope', () => { + const global = new ProtoChainEnv(); + expect(global.async).toBe(true); + }); + + it('should be false by default', () => { + const global = new ProtoChainEnv(); + const env = new ProtoChainEnv(global); + expect(env.async).toBe(false); + }); + + it('should return previously set value on current scope', () => { + const global = new ProtoChainEnv(); + const parent = new ProtoChainEnv(global); + const child = new ProtoChainEnv(parent); + parent.async = true; + child.async = false; + expect(child.async).toBe(false); + parent.async = false; + child.async = true; + expect(child.async).toBe(true); + }); + }); + + describe('get', () => { + it('should return values from env stack', () => { + const global = new ProtoChainEnv(null, { scope: 'global', global: true }); + const child = new ProtoChainEnv(global, { scope: 'child' }); + + expect(child.get('scope')).toBe('child'); + expect(child.get('global')).toBe(true); + }); + + it('should return undefined for non-existent keys', () => { + const global = new ProtoChainEnv(null, { scope: 'global', global: true }); + const child = new ProtoChainEnv(global, { scope: 'child' }); + + expect(child.get('abc')).toBeUndefined(); + }); + }); + + describe('getOwn', () => { + it('should return values from own scope', () => { + const global = new ProtoChainEnv(null, { scope: 'global', global: true }); + const child = new ProtoChainEnv(global, { scope: 'child' }); + + expect(child.getOwn('scope')).toBe('child'); + }); + + it('should return undefined for keys not in own scope', () => { + const global = new ProtoChainEnv(null, { scope: 'global', global: true }); + const child = new ProtoChainEnv(global, { scope: 'child' }); + + expect(child.getOwn('global')).toBeUndefined(); + expect(child.getOwn('abc')).toBeUndefined(); + }); + }); + + describe('set', () => { + it('should set value to correct scope', () => { + const global = new ProtoChainEnv(null); + const child = new ProtoChainEnv(global); + global.set('scope', 'global'); + child.set('scope', 'child'); + child.set('child', true); + + expect(child.get('scope')).toBe('child'); + expect(child.get('child')).toBe(true); + expect(global.get('scope')).toBe('child'); + expect(global.get('child')).toBeUndefined(); + }); + }); + + describe('setOwn', () => { + it('should set value to current scope', () => { + const global = new ProtoChainEnv(null); + const child = new ProtoChainEnv(global); + global.set('scope', 'global'); + child.setOwn('scope', 'child'); + + expect(child.get('scope')).toBe('child'); + expect(global.get('scope')).toBe('global'); + }); + + it('should set non writable value if readOnly = true', () => { + const env = new ProtoChainEnv(); + env.setOwn('field', 'value', true); + expect(() => env.set('field', 'value2')).toThrow(/Cannot assign/); + }); + }); + + describe('push', () => { + it('should return a child scope', () => { + const env = new ProtoChainEnv(); + const child = env.push(); + + expect(child.global).toBe(env); + expect(Object.getPrototypeOf(child['binds'])).toBe(env['binds']); + }); + }); +}); diff --git a/packages/jsonr/src/__tests__/integ.spec.ts b/packages/jsonr/src/__tests__/integ.spec.ts new file mode 100644 index 00000000..04954b86 --- /dev/null +++ b/packages/jsonr/src/__tests__/integ.spec.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { DefaultEnv } from '../env.ts'; +import { Interpreter } from '../interpreter/index.ts'; +import { JsonAstParser } from '../parser/index.ts'; +import { Stdlib } from '../stdlib/index.ts'; +import { AsyncValue, Env, Evaluator, JSONType, Parser } from '../types.ts'; + +import asyncTest from './data/async.json'; +import macroTest from './data/macro.json'; + +describe('Integration tests', () => { + let env: Env; + let evaluator: Evaluator; + let parser: Parser; + let lines: AsyncValue[][]; + let readline: jest.Mock<(prompt: string, timeout?: number) => Promise>; + + beforeEach(() => { + env = new DefaultEnv(null, { + ...Stdlib, + println: (...args: AsyncValue[]) => (lines.push(args), null), + readline: (...args: AsyncValue[]) => readline(...args as [string, number?]), + }); + evaluator = new Interpreter(); + parser = new JsonAstParser(); + lines = []; + readline = jest.fn(); + }); + + describe('macro', () => { + it('should print the correct results', async () => { + const code = JSON.stringify(macroTest); + const ast = parser.parse(code); + const result = await evaluator.eval(ast, env); + expect(result).toBe(null); + expect(lines).toMatchSnapshot(); + }); + }); + + describe('async', () => { + let ast: JSONType; + + beforeEach(() => { + const code = JSON.stringify(asyncTest); + ast = parser.parse(code); + }); + + it('should print the correct results', async () => { + const input = 'testing'; + readline.mockResolvedValueOnce(input); + const result = await evaluator.eval(ast, env); + expect(result).toBe(null); + expect(lines).toMatchSnapshot(); + expect(readline).toHaveBeenCalledWith('your input: ', 2000); + }); + + it('should catch rejected promise from readline', async () => { + readline.mockRejectedValueOnce(new Error('aborted')); + const result = await evaluator.eval(ast, env); + expect(result).toBe(null); + expect(lines).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jsonr/src/__tests__/utils.spec.ts b/packages/jsonr/src/__tests__/utils.spec.ts new file mode 100644 index 00000000..7e7ca975 --- /dev/null +++ b/packages/jsonr/src/__tests__/utils.spec.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from '@jest/globals'; +import { assertArray, assertFunction, assertString, assertStrings, isFunction, isObj, isString, obj } from '../utils.ts'; +import { AsyncValue } from '../types.ts'; + +describe(assertString.name, () => { + it('should not throw if value is a string', () => { + expect(() => assertString('foo')).not.toThrow(); + }); + + it.each([ + [null], [123], [true], [['a']], [{ a: 1 }], Promise.resolve('a') + ])('should throw if value = $p', (value: AsyncValue) => { + expect(() => assertString(value)).toThrow(SyntaxError); + }); +}); + +describe(assertArray.name, () => { + it('should not throw if value is an array', () => { + expect(() => assertArray([])).not.toThrow(); + expect(() => assertArray(['a', 'b'])).not.toThrow(); + expect(() => assertArray([Promise.resolve('a')])).not.toThrow(); + }); + + it.each([ + [null], [123], [true], [{ a: 1 }], ['abc'] + ])('should throw if value = $p', (value: AsyncValue) => { + expect(() => assertArray(value)).toThrow(SyntaxError); + }); +}); + +describe(assertStrings.name, () => { + it('should not throw if value is a string array', () => { + expect(() => assertStrings([])).not.toThrow(); + expect(() => assertStrings(['a', 'b'])).not.toThrow(); + }); + + it.each([ + [null], [123], [true], [{ a: 1 }], ['abc'], + [[123]], [['abc', 123]], [Promise.resolve('a')] + ])('should throw if value = $p', (value: AsyncValue) => { + expect(() => assertStrings(value)).toThrow(SyntaxError); + }); +}); + +describe(assertFunction.name, () => { + it('should not throw if value is a function', () => { + expect(() => assertFunction(() => true)).not.toThrow(); + }); + + it.each([ + [null], [123], [true], [['a']], [{ a: 1 }], ['abc'], Promise.resolve(() => true) + ])('should throw if value = $p', (value: AsyncValue) => { + expect(() => assertFunction(value)).toThrow(TypeError); + }); +}); + +describe(isString.name, () => { + it('should return true if value is a string', () => { + expect(isString('foo')).toBe(true); + }); + + it.each([ + [null], [123], [true], [['a']], [{ a: 1 }], Promise.resolve('a') + ])('should return false if value = $p', (value: AsyncValue) => { + expect(isString(value)).toBe(false); + }); +}); + +describe(isFunction.name, () => { + it('should return true if value is a function', () => { + expect(isFunction(() => true)).toBe(true); + }); + + it.each([ + [null], [123], [true], [['a']], [{ a: 1 }], ['abc'], Promise.resolve(() => true) + ])('should return false if value = $p', (value: AsyncValue) => { + expect(isFunction(value)).toBe(false); + }); +}); + +describe(isObj.name, () => { + it('should return true if value is an object with null prototype', () => { + expect(isObj(Object.create(null))).toBe(true); + }); + + it.each([ + [null], [123], [true], [['a']], [{ a: 1 }], ['abc'], Promise.resolve(Object.create(null)) + ])('should return false if value = $p', (value: AsyncValue) => { + expect(isObj(value)).toBe(false); + }); +}); + +describe(obj.name, () => { + it('should return a santized version of given object', () => { + const input = { a: 1, b: 2, c: 3 }; + const output = obj(input); + expect(output).toEqual(input); + expect(Object.getPrototypeOf(output)).toBe(null); + }); +}); diff --git a/packages/jsonr/src/env.ts b/packages/jsonr/src/env.ts new file mode 100644 index 00000000..beda9aa6 --- /dev/null +++ b/packages/jsonr/src/env.ts @@ -0,0 +1,73 @@ +import { symAsync, symFn } from './symbol.ts'; +import { AsyncValue, Bindings, Env } from './types.ts'; + +/** Environment that resolves values using object prototype chain. */ +export class ProtoChainEnv implements Env { + protected readonly binds: Bindings; + + public constructor( + public readonly parent: ProtoChainEnv | null = null, + constants: Bindings = {}, + ) { + this.binds = Object.create(parent?.binds ?? null); + const descriptors: PropertyDescriptorMap = {}; + for (const [key, value] of Object.entries(constants)) { + descriptors[key] = { value, writable: false }; + } + Object.defineProperties(this.binds, descriptors); + } + + public get global(): ProtoChainEnv { + return this.parent?.global ?? this; + } + + public get fn(): boolean { + return !!this.binds[symFn]; + } + + public set fn(isFn: boolean) { + this.binds[symFn] = isFn; + } + + public get async(): boolean { + if (this.global === this) { return true; } // global scope is always async + return !!this.binds[symAsync]; + } + + public set async(isAsync: boolean) { + this.binds[symAsync] = isAsync; + } + + public get(name: string): AsyncValue | undefined { + return this.binds[name]; + } + + public getOwn(name: string): AsyncValue | undefined { + return Object.hasOwn(this.binds, name) ? this.binds[name] : void 0; + } + + public set(name: string, value: V): V { + if (!(name in this.binds)) { + return (this.binds[name] = value) as V; + } + + let binds = this.binds; + while (!Object.hasOwn(binds, name)) { binds = Object.getPrototypeOf(binds); } + return (binds[name] = value) as V; + } + + public setOwn(name: string, value: V, readOnly = false): V { + if (readOnly) { + Object.defineProperty(this.binds, name, { value, writable: false }); + return value; + } + return (this.binds[name] = value) as V; + } + + public push(): ProtoChainEnv { + return new ProtoChainEnv(this); + } +} + +/** Default environment implementation. */ +export const DefaultEnv = ProtoChainEnv; diff --git a/packages/jsonr/src/index.ts b/packages/jsonr/src/index.ts new file mode 100644 index 00000000..b80999c9 --- /dev/null +++ b/packages/jsonr/src/index.ts @@ -0,0 +1,7 @@ +export * from './env.ts'; +export * from './symbol.ts'; +export * from './types.ts'; +export * from './interpreter/index.ts'; +export * from './parser/index.ts'; +export * from './stdlib/index.ts'; +export { isObj, obj } from './utils.ts'; diff --git a/packages/jsonr/src/interpreter/__tests__/interpreter.spec.ts b/packages/jsonr/src/interpreter/__tests__/interpreter.spec.ts new file mode 100644 index 00000000..756a841e --- /dev/null +++ b/packages/jsonr/src/interpreter/__tests__/interpreter.spec.ts @@ -0,0 +1,221 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { DefaultEnv } from '../../env.ts'; +import { Stdlib } from '../../stdlib/index.ts'; +import { Env, JSONType } from '../../types.ts'; +import { obj } from '../../utils.ts'; +import { Interpreter } from '../interpreter.ts'; + +describe(Interpreter.name, () => { + let env: Env; + let interpreter: Interpreter; + + beforeEach(() => { + env = new DefaultEnv(null, { + ...Stdlib, + unreachable: () => { throw new Error('unreachable'); }, + promise: (x) => Promise.resolve(x) + }); + interpreter = new Interpreter(); + }); + + describe('compile', () => { + it('should return a function for running given AST', () => { + const f = interpreter.compile<[number]>(['+', 'n', 3], ['n']); + expect(f.call(env, 2)).toBe(5); + }); + + it('should support no-arg function', () => { + const f = interpreter.compile(['+', 2, 3]); + expect(f.call(env)).toBe(5); + }); + + }); + + describe('eval', () => { + it.each([ + [], obj(), 0, -12, false, true, null + ])('should return literals as is %#', (expr) => { + expect(run(expr)).toEqual(expr); + }); + + it.each([ + [['[]', 1, ['+', 2, 3], true, ['\'', 'abc']], [1, 5, true, 'abc']], + [obj({ a: ['+', 3, 5], b: ['\'', '123'] }), obj({ a: 8, b: '123' })], + [['/', ['-', ['+', 515, ['*', 87, 311]], 302], 27], 1010], + ])('should evaluate expressions correctly %#', (expr, expected) => { + expect(run(expr)).toEqual(expected); + }); + + it('should get and set variables correctly', () => { + expect(run(['let', 'x', 3, 'y', 4])).toEqual(4); + expect(run(obj({ x: 'x', y: 'y' }))).toEqual(obj({ x: 3, y: 4 })); + + expect(run(['let', 'z', 5, 'z', 6])).toEqual(6); + expect(run('z')).toEqual(6); + + expect(run(['let', 'a', 7])).toEqual(7); + expect(run(['let', 'A', 8])).toEqual(8); + expect(run('a')).toEqual(7); + expect(run(['@', ['\'', 'A']])).toEqual(8); + }); + + it.each(['let', 'const', '='])('should support destructuring assignment with `%s`', (keyword) => { + run(['let', 'x', 0, 'y', 1]); + expect(run([keyword, ['x', 'y'], ['[]', 3, 4]])).toEqual([3, 4]); + expect(run('x')).toEqual(3); + expect(run('y')).toEqual(4); + }); + + it('should define and call functions correctly', () => { + expect(run(['let', 'x', 3, 'y', 4, 'z', 5])).toEqual(5); + expect(run(['fn', 'xyz', ['z', 'aa'], ['[]', 'x', 'y', 'z', 'aa']])).toBeInstanceOf(Function); + expect(run(['xyz', 6])).toEqual([3, 4, 6, null]); + expect(run('z')).toEqual(5); + expect(run(['apply', 'xyz', ['[]', 7, 11]])).toEqual([3, 4, 7, 11]); + expect(run(['apply', 'xyz', ['[]', 8]])).toEqual([3, 4, 8, null]); + expect(run(['let', 'f', ['fn', ['x'], 'x']])).toBeInstanceOf(Function); + expect(run(['f', 7])).toEqual(7); + }); + + it('should abide variable scope', () => { + expect(run(['let', 'x', 3])).toEqual(3); + expect(run(['{}', ['let', 'x', 4], 'x'])).toEqual(4); + expect(run('x')).toEqual(3); + expect(run(['{}', ['=', 'x', 4], 'x'])).toEqual(4); + expect(run('x')).toEqual(4); + }); + + it('should throw for invalid references', () => { + expect(run(['let', 'x', 3])).toEqual(3); + expect(() => run(['x'])).toThrow(new TypeError('x is not a function')); + expect(() => run(['xyz'])).toThrow(new TypeError('xyz is not defined')); + expect(() => run([['@', ['\'', 'xyz']]])).toThrow(new ReferenceError('xyz is not defined')); + }); + + it.each([ + [['if', ['>', 3, 2], 200, ['unreachable']], 200], + [['if', ['boolean', true], 200], 200], + [[';', ['let', 'n', 0], ['while', ['<', 'n', 10], ['=', 'n', ['+', 1, 'n']]]], 10], + [[';', ['let', 'n', 0], ['for', 'x', ['[]', 1, 3, 5], ['=', 'n', ['+', 'x', 'n']]]], 9], + [['try', 200, ['catch', ['unreachable']]], 200], + [['try', ['throw', 'exception'], ['catch', 'msg', ['+', ['\'', 'caught: '], 'msg']]], 'caught: exception'], + ])('should evaluate control expressions correctly %#', (expr, expected) => { + expect(run(expr)).toEqual(expected); + }); + + it.each([ + [['if', ['&&'], ['unreachable'], 200], 200], + [['if', ['||', false, false], ['unreachable']], null], + [['if', ['&&', true, false, ['unreachable']], ['unreachable'], 200], 200], + [['if', ['||', false, true, ['unreachable']], 200], 200], + [['??', 200, ['unreachable']], 200] + ])('should short circuit evaluate correctly %#', (expr, expected) => { + expect(run(expr)).toEqual(expected); + }); + + it.each([ + [['if', ['&', true, false, ['unreachable']], 'never']], + [['if', ['|', false, true, ['unreachable']], 'never']], + ])('should eager evaluate and throw %#', (expr) => { + expect(() => run(expr)).toThrow('unreachable'); + }); + + it('should throw error from throw expression', () => { + expect(() => run(['throw'])).toThrow(); + expect(() => run(['throw', 'error'])).toThrow('error'); + }); + + const sumTR = ['fn', 'sum-tr', ['n', 'acc'], + ['if', ['===', 'n', 0], 'acc', [';', + ['=', 'acc', ['+', 'n', 'acc']], + ['sum-tr', ['-', 'n', 1], 'acc'], + ]] + ]; + const recurseA = ['fn', 'recurse-a', ['n'], ['if', ['<=', 'n', 0], 0, ['recurse-b', ['-', 'n', 1]]]]; + const recurseB = ['fn', 'recurse-b', ['n'], ['if', ['>=', 0, 'n'], 0, ['recurse-a', ['-', 'n', 2]]]]; + + it.each([ + [[sumTR, 10], 55], + [[sumTR, 10000], 50005000], + [[';', recurseB, recurseA, [recurseA[1], 10000]], 0], + ])('should evaluate recursive tail-call functions correctly %#', (expr, expected) => { + expect(run(expr)).toEqual(expected); + }); + + it('should evaluate async functions synchronously when without await', () => { + expect(run(['async', 'echo', ['n'], 'n'])).toBeInstanceOf(Function); + expect(run(['echo', 1])).toBe(1); + }); + + it('should return promise for async functions results', async () => { + expect(run(['async', 'next', ['x'], ['+', 1, ['await', ['promise', 'x']]]])).toBeInstanceOf(Function); + await expect(run(['next', 1])).resolves.toBe(2); + }); + + it('should be awaitable at global scope', async () => { + await expect(run(['+', 1, ['await', ['promise', 1]]])).resolves.toBe(2); + }); + + it('should throw when using await in sync fn', () => { + expect(run(['fn', 'wait', ['x'], ['await', 'x']])).toBeInstanceOf(Function); + expect(() => run(['wait', 1])).toThrow(new SyntaxError('await is only valid in async scope')); + }); + + it.each([ + [[';', ['const', 'x', 2], ['let', 'x', 3]]], + [[';', ['const', 'x', 2], ['=', 'x', 3]]], + ])('should throw when reassigning consts', (expr: JSONType) => { + expect(() => run(expr)).toThrow(/Cannot assign/); + }); + + it.each([ + [['\''], null], + [['\'', 'x'], 'x'], + [['\'', 7], 7], + [['\'', [1, ['-', 2, 3]]], [1, ['-', 2, 3]]], + [['\'', obj({ a: ['+', 3, 4] })], obj({ a: ['+', 3, 4] })], + [['`'], null], + [['`', 'x'], 'x'], + [['`', [1, [], 2]], [1, [], 2]], + [['`', obj({ a: ['+', 3, 4] })], obj({ a: ['+', 3, 4] })], + [['`', [',', 7]], 7], + [['`', [',', 'x']], 1], + [['`', ['+', 3, [',', 'x']]], ['+', 3, 1]], + [['`', [3, [',', ['+', 'x', 'x']], [',', 'x']]], [3, 2, 1]], + [['`', [3, [',@', ['[]', 'x', 'x']], [',', 'x']]], [3, 1, 1, 1]], + [['`', obj({ a: ['+', 3, 4], b: [',', ['+', 5, 6]] })], obj({ a: ['+', 3, 4], b: 11 })], + ])('should return quoted / quasiquoted results %#', (expr, expected) => { + run(['let', 'x', 1]); + expect(run(expr)).toEqual(expected); + }); + + it.each([ + [['macro', 'one', [], 1], ['one'], 1, 1], + [[';', ['let', 'a', 123], ['macro', 'identity', ['x'], 'x']], ['identity', 'a'], 'a', 123], + [ + ['macro', 'unless', ['pred', 'a', 'b'], ['`', ['if', [',', 'pred'], [',', 'b'], [',', 'a']]]], + ['unless', ['&&', true, false], 7, 8], ['if', ['&&', true, false], 8, 7], 7 + ], + [ + ['macro', 'unless2', ['pred', 'a', 'b'], ['[]', ['\'', 'if'], ['[]', ['\'', '!'], 'pred'], 'a', 'b']], + ['unless2', true, 7, 8], ['if', ['!', true], 7, 8], 8 + ], + [ + ['macro', 'cond', ['test', 'action', '...xs'], + ['[]', ['\'', 'if'], 'test', 'action', ['if', ['>', ['length', 'xs'], 0], ['...', ['\'', 'cond'], 'xs'], null]] + ], + [';', ['let', 'x', ['cond', false, 404, true, 200]], 'x'], + undefined, + 200 + ] + ])('should expand and run macro correctly %#', (macro, call, expanded, result) => { + expect(run(macro)).toBeInstanceOf(Function); + expanded !== undefined && expect(run(['macroexpand', call])).toEqual(expanded); + expect(run(call)).toEqual(result); + }); + }); + + function run(expr: JSONType, _env: Env = env) { + return interpreter.eval(expr, _env); + } +}); diff --git a/packages/jsonr/src/interpreter/control.ts b/packages/jsonr/src/interpreter/control.ts new file mode 100644 index 00000000..d01eeb9b --- /dev/null +++ b/packages/jsonr/src/interpreter/control.ts @@ -0,0 +1,31 @@ +import { symCtrl } from '../symbol.ts'; +import { AsyncValue } from '../types.ts'; + +export enum ControlFlag { + None = 0, + TailCall = 1 << 0, + Return = 1 << 1, + Break = 1 << 2, + Continue = 1 << 3, +} + +export type ControlValue = { + [symCtrl]: Flag; + value: AsyncValue; +} + +export function isControl(expr: AsyncValue): expr is ControlValue { + return expr !== null && typeof expr === 'object' && symCtrl in expr; +} + +export function isLoopControl(expr: AsyncValue): expr is ControlValue { + return isControl(expr) && !!(expr[symCtrl] & (ControlFlag.Break | ControlFlag.Continue)); +} + +export function isReturn(expr: AsyncValue): expr is ControlValue { + return isControl(expr) && !!(expr[symCtrl] & ControlFlag.Return); +} + +export function unwrapReturn(value: ControlValue, flags = ControlFlag.None) { + return (flags && ControlFlag.TailCall) ? value.value : value; +} diff --git a/packages/jsonr/src/interpreter/eval.ts b/packages/jsonr/src/interpreter/eval.ts new file mode 100644 index 00000000..7ae4bc6f --- /dev/null +++ b/packages/jsonr/src/interpreter/eval.ts @@ -0,0 +1,270 @@ +import { maybeAsync } from '@mithic/commons'; +import { symCtrl, symFn, symFnArgs, symFnBody } from '../symbol.ts'; +import { AsyncValue, Env, FnValue, ObjValue, Value } from '../types.ts'; +import { assertArray, assertFunction, assertString, isObj, isString } from '../utils.ts'; +import { ControlFlag, ControlValue, isControl, isLoopControl, isReturn, unwrapReturn } from './control.ts'; +import { assignArgs, createFnEnv, evalFn } from './fn.ts'; +import { deref, macroExpand, quasiquote } from './meta.ts'; + +/** Evaluates an expression. */ +export const evalExpr = maybeAsync(coEvalExpr); + +type FnForm = (expr: AsyncValue[], env: Env, flags?: ControlFlag) => AsyncValue; +const fnForm: Record = { + fn: (expr, env) => evalFn(expr, env), + async: (expr, env) => evalFn(expr, env, false, true), + macro: (expr, env) => evalFn(expr, env, true), + macroexpand: (expr, env) => macroExpand(expr[1], env, +(expr[2] as number) || Infinity), + '\'': (expr) => expr[1] ?? null, + throw: (expr) => { throw expr[1] ?? null; }, + break: evalBreakContinue.bind(null, ControlFlag.Break), + continue: evalBreakContinue.bind(null, ControlFlag.Continue), +}; + +type CoEvalForm = (expr: AsyncValue[], env: Env, flags?: ControlFlag) => + Generator; +const coroutineForm: Record = { + await: coEvalAwait, + return: coEvalReturn, + // TODO: add yield points to for/while to support abort signals + for: coEvalFor, + while: coEvalWhile, + const: (expr, env) => coEvalAssign(expr, env, true, true), + let: (expr, env) => coEvalAssign(expr, env, true), + '=': (expr, env) => coEvalAssign(expr, env), + '@': coEvalDeref, + '&&': coEvalAndOr, + '||': coEvalAndOr, + '??': coEvalNullCoalescing, +}; + +const specialForms = new Set([...Object.keys(fnForm), ...Object.keys(coroutineForm), + '`', '{}', ';', 'if', 'try', 'catch' // inline forms +]); + +/** Coroutine to evaluate an expression. */ +export function* coEvalExpr( + expr: AsyncValue | undefined, env: Env, flags = ControlFlag.None +): Generator { + loop: for (; ;) { + // 1. null + if (expr === null || expr === void 0) { return null; } + + // 2. function call + if (Array.isArray(expr)) { + // expand macros + expr = macroExpand(expr, env); + if (!Array.isArray(expr)) { continue loop; } + if (expr.length === 0) { return expr; } + + // evaluate special forms + if (isString(expr[0])) { + if (expr[0] in fnForm) { return fnForm[expr[0]](expr, env, flags); } + if (expr[0] in coroutineForm) { return yield* coroutineForm[expr[0]](expr, env, flags); } + + switch (expr[0]) { // inline forms below needs to be added to `specialForms` set + case '`': expr = quasiquote(expr[1] ?? null, env); + continue loop; + case '{}': env = env.push(); + // eslint-disable-next-line no-fallthrough + case ';': env.fn && (flags |= ControlFlag.Return); + for (let i = 1; i < expr.length - 1; ++i) { + const result = yield* coEvalExpr(expr[i], env, flags & ~ControlFlag.TailCall); + if (isReturn(result)) { return unwrapReturn(result, flags); } + if (isLoopControl(result)) { return result; } + } + expr = expr[expr.length - 1]; + continue loop; + case 'if': expr = ((yield* coEvalExpr(expr[1], env)) ? expr[2] : expr[3]) ?? null; + continue loop; + case 'try': try { + return yield* coEvalExpr(expr[1], env, flags & ~ControlFlag.TailCall); + } catch (e) { + expr = inlineEvalCatch(expr[2], env = env.push(), e); + continue loop; + } + } + } + + // inline function call + const [fn, args] = yield* coParseCall(expr, env); + if (!fn[symFn]) { return fn.apply(env.global, args) ?? null; } + assignArgs(fn[symFnArgs] || [], args, env = createFnEnv(fn, env)); + expr = fn[symFnBody]; + flags |= ControlFlag.TailCall; + continue loop; + } + + // 3. obj + if (isObj(expr)) { return yield* coEvalObj(expr, env); } + + // 4. symbol + if (isString(expr)) { + if (specialForms.has(expr)) { throw new SyntaxError(`Unexpected token '${expr}'`); } + return deref(expr, env, true); + } + + // 5. primitive or native or control type: return unchanged + return expr; + } +} + +export function* coEvalAwait(expr: AsyncValue[], env: Env): Generator { + if (!env.async) { throw new SyntaxError('await is only valid in async scope'); } + return (yield (yield* coEvalExpr(expr[1], env))) ?? null; +} + +export function* coEvalDeref(expr: AsyncValue[], env: Env): Generator { + const ref = yield* coEvalExpr(expr[1], env); + assertString(ref); + return deref(ref, env, true); +} + +export function* coEvalReturn( + expr: AsyncValue[], env: Env, flags = ControlFlag.None +): Generator { + if (!(flags & ControlFlag.Return)) { throw new SyntaxError('Illegal return statement'); } + const value = yield* coEvalExpr(expr[1], env); + return (flags & ControlFlag.TailCall) ? value : + { [symCtrl]: ControlFlag.Return, value } satisfies ControlValue; +} + +export function* coEvalObj(obj: ObjValue, env: Env): Generator { + const result = Object.create(null); + for (const [key, value] of Object.entries(obj)) { + result[key] = yield* coEvalExpr(value, env); + } + return result; +} + +export function* coEvalAssign( + expr: AsyncValue[], env: Env, define = false, readOnly = false +): Generator { + let result: AsyncValue = null; + for (let i = 1; i < expr.length; i += 2) { + const value = yield* coEvalExpr(expr[i + 1] ?? null, env); + setValue(expr[i], value, env, define, readOnly); + result = value; + } + return result; +} + +function setValue(lhs: AsyncValue, value: AsyncValue, env: Env, define = false, readOnly = false) { + // TODO: support obj destructuring + if (Array.isArray(lhs)) { + const iter = (value as AsyncValue[])?.[Symbol.iterator]?.(); + if (!iter) { throw new TypeError(`${value} is not iterable`); } + for (let j = 0, v = iter.next(); j < lhs.length && !v.done; ++j, v = iter.next()) { + let lval = lhs[j], rval = v.value; + if (j === lhs.length - 1 && isString(lval) && lval.startsWith('...')) { + lval = lval.substring(3); + rval = [rval]; + for (; !v.done; v = iter.next()) { rval.push(v.value); } + } + setValue(lval, rval, env, define, readOnly); + } + return; + } + + assertString(lhs); + if (define) { + env.setOwn(lhs, value, readOnly); + } else { + if (env.get(lhs) === void 0) { throw new ReferenceError(`${lhs} is not defined`); } + env.set(lhs, value); + } +} + +export function* coParseCall( + expr: AsyncValue[], env: Env +): Generator { + const isApply = expr[0] === 'apply'; + + const f = yield* coEvalExpr(expr[isApply ? 1 : 0], env); + assertFunction(f, expr[0]); + + let args: AsyncValue[] = []; + if (isApply) { + const argsArg = yield* coEvalExpr(expr[2], env); + assertArray(argsArg); + args = argsArg; + } else { + for (let i = 1; i < expr.length; ++i) { + args.push(yield* coEvalExpr(expr[i], env)); + } + } + return [f, args]; +} + +export function* coEvalAndOr(expr: AsyncValue[], env: Env): Generator { + if (expr.length === 1) { return null; } + const isAnd = expr[0] === '&&'; + let result: AsyncValue = isAnd ? true : false; + for (let i = 1; !(isAnd ? !result : result) && i < expr.length; ++i) { + const rhs = yield* coEvalExpr(expr[i], env); + result = isAnd ? result && rhs : result || rhs; + } + return result; +} + +export function* coEvalNullCoalescing(expr: AsyncValue[], env: Env): Generator { + let result: AsyncValue = null; + for (let i = 1; result === null && i < expr.length; ++i) { + const rhs = yield* coEvalExpr(expr[i], env); + result = result ?? rhs; + } + return result; +} + +export function* coEvalFor(expr: AsyncValue[], env: Env): Generator { + const flags = ControlFlag.Break | ControlFlag.Continue | (env.fn ? ControlFlag.Return : ControlFlag.None); + const name = expr[1]; + assertString(name); + const innerEnv = env.push(); + let result = null; + for (const val of (yield* coEvalExpr(expr[2], env)) as Iterable) { + innerEnv.setOwn(name, val); + result = yield* coEvalExpr(expr[3], innerEnv, flags); + if (isControl(result)) { + switch (result[symCtrl]) { + case ControlFlag.Return: return unwrapReturn(result, flags); + case ControlFlag.Break: return null; + case ControlFlag.Continue: result = null; + } + } + } + return result; +} + +export function* coEvalWhile(expr: AsyncValue[], env: Env): Generator { + const flags = ControlFlag.Break | ControlFlag.Continue | (env.fn ? ControlFlag.Return : ControlFlag.None); + let result = null; + while ((yield* coEvalExpr(expr[1], env))) { + result = yield* coEvalExpr(expr[2], env, flags); + if (isControl(result)) { + switch (result[symCtrl]) { + case ControlFlag.Return: return unwrapReturn(result, flags); + case ControlFlag.Break: return null; + case ControlFlag.Continue: result = null; + } + } + } + return result; +} + +export function inlineEvalCatch(catchExpr: AsyncValue, env: Env, err: unknown): AsyncValue { + if (!Array.isArray(catchExpr) || catchExpr[0] !== 'catch') { throw new SyntaxError('Missing catch after try'); } + if (catchExpr.length < 3) { return catchExpr[1] ?? null; } + assertString(catchExpr[1]); + env.setOwn(catchExpr[1], err as AsyncValue); + return catchExpr[2] ?? null; +} + +export function evalBreakContinue( + type: ControlFlag, expr: AsyncValue[], _: Env, flags = ControlFlag.None +): ControlValue { + if (!(flags & type) || (expr[1] && !isString(expr[1]))) { + throw new SyntaxError(`Illegal ${type & ControlFlag.Continue ? 'continue' : 'break'} statement`); + } + return { [symCtrl]: type, value: expr[1] } satisfies ControlValue; +} diff --git a/packages/jsonr/src/interpreter/fn.ts b/packages/jsonr/src/interpreter/fn.ts new file mode 100644 index 00000000..44e7d362 --- /dev/null +++ b/packages/jsonr/src/interpreter/fn.ts @@ -0,0 +1,66 @@ +import { symAsync, symEnv, symFn, symFnArgs, symFnBody, symMacro } from '../symbol.ts'; +import { AsyncValue, Env, FnValue } from '../types.ts'; +import { isString, assertStrings } from '../utils.ts'; +import { ControlFlag } from './control.ts'; +import { evalExpr } from './eval.ts'; + +export function evalFn( + expr: AsyncValue[], env: Env, isMacro = false, isAsync = false +): FnValue { + let fn: string | null = null, args: AsyncValue, body: AsyncValue; + if (expr.length < 3) { throw new SyntaxError(`Unexpected token 'fn'`) } + if (isString(expr[1])) { + [, fn, args, body] = expr; + } else { + [, args, body] = expr; + } + assertStrings(args); + const f = createFn(args, body, env, isMacro, isAsync); + return fn ? env.setOwn(fn, f) : f; +} + +export function createFn( + args: string[], body: AsyncValue, env: Env | null = null, + isMacro = false, isAsync = false +): FnValue { + const f: FnValue = function (this: Env, ...vals: AsyncValue[]): AsyncValue { + const env = createFnEnv(f, this); + assignArgs(f[symFnArgs] || [], vals, env); + return evalExpr(f[symFnBody], env, ControlFlag.TailCall); + }; + if (isMacro) { + f[symMacro] = true as const; + } else if (isAsync) { + f[symAsync] = true as const; + } + f[symFn] = true as const; + f[symEnv] = env; + f[symFnArgs] = args; + f[symFnBody] = body; + f.toString = () => JSON.stringify([ + isMacro ? "macro" : isAsync ? "async" : "fn", + args, + body + ]); + return f; +} + +export function createFnEnv(f: FnValue, env: Env): Env { + const newEnv = (f[symEnv] ?? env.global).push(); + newEnv.fn = true; + newEnv.async = !!f[symAsync]; + return newEnv; +} + +export function assignArgs(args: string[], vals: AsyncValue[], env: Env): void { + for (let i = 0; i < args.length - 1; ++i) { + env.setOwn(args[i], vals[i] ?? null); + } + const lastArg = args[args.length - 1]; + if (!lastArg) { return } + if (lastArg.startsWith('...')) { + env.setOwn(lastArg.substring(3), vals.slice(args.length - 1)); + } else { + env.setOwn(lastArg, vals[args.length - 1] ?? null); + } +} diff --git a/packages/jsonr/src/interpreter/index.ts b/packages/jsonr/src/interpreter/index.ts new file mode 100644 index 00000000..5f5071f3 --- /dev/null +++ b/packages/jsonr/src/interpreter/index.ts @@ -0,0 +1 @@ +export * from './interpreter.ts'; diff --git a/packages/jsonr/src/interpreter/interpreter.ts b/packages/jsonr/src/interpreter/interpreter.ts new file mode 100644 index 00000000..274a53f3 --- /dev/null +++ b/packages/jsonr/src/interpreter/interpreter.ts @@ -0,0 +1,14 @@ +import { AsyncValue, Env, Evaluator, FnValue, JSONType } from '../types.ts'; +import { evalExpr } from './eval.ts'; +import { createFn } from './fn.ts'; + +/** metael interpreter. */ +export class Interpreter implements Evaluator { + public compile(expr: JSONType, args: string[] = []): FnValue { + return createFn(args, expr) as FnValue; + } + + public eval(expr: JSONType, env: Env): AsyncValue { + return evalExpr(expr, env); + } +} diff --git a/packages/jsonr/src/interpreter/meta.ts b/packages/jsonr/src/interpreter/meta.ts new file mode 100644 index 00000000..36d53267 --- /dev/null +++ b/packages/jsonr/src/interpreter/meta.ts @@ -0,0 +1,59 @@ +import { symMacro } from '../symbol.ts'; +import { AsyncValue, Env, MacroValue, ObjValue } from '../types.ts'; +import { isFunction, isObj, isString } from '../utils.ts'; + +/** Try to deference symbol. */ +export function deref( + ref: string, env: Env, required: Required = false as Required +): Required extends true ? AsyncValue : AsyncValue | undefined { + const value = env.get(ref); + if (required && value === void 0) { throw new ReferenceError(`${ref} is not defined`); } + return value as AsyncValue; +} + +export function quasiquote(expr: AsyncValue, env: Env): AsyncValue { + if (Array.isArray(expr)) { + if (expr.length === 0) { return expr; } + if (expr[0] === ',') { return expr[1]; } + const result: AsyncValue[] = ['...']; + for (let i = 0; i < expr.length; ++i) { + const elt = expr[i]; + if (Array.isArray(elt) && elt[0] === ',@') { + result.push(elt[1]); + } else { + const qelt = quasiquote(elt, env); + const last = result[result.length - 1]; + if (Array.isArray(last) && last[0] === '[]') { + last.push(qelt); + } else { + result.push(['[]', qelt]); + } + } + } + return result; + } else if (isObj(expr)) { + return Object.entries(expr).reduce( + (obj, [key, value]) => ((obj[key] = quasiquote(value, env)), obj), + Object.create(null) as ObjValue + ); + } else if (isString(expr)) { + return ['\'', expr]; // quote strings + } + return expr; +} + +export function macroExpand(expr: AsyncValue, env: Env, times = Infinity): AsyncValue { + let macro: MacroValue | null = null; + for (let i = 0; i < times; ++i) { + if (!Array.isArray(expr) || !expr.length || (macro = getIfMacro(expr, env)) === null) { break; } + expr = macro.apply(env, expr.slice(1)); + } + return expr; +} + +function getIfMacro(expr: AsyncValue[], env: Env): MacroValue | null { + if (!isString(expr[0])) { return null; } + const f = env.get(expr[0]) ?? null; + if (!isFunction(f) || !f[symMacro]) { return null; } + return f as MacroValue; +} diff --git a/packages/jsonr/src/parser/__tests__/ast.spec.ts b/packages/jsonr/src/parser/__tests__/ast.spec.ts new file mode 100644 index 00000000..d10bdd91 --- /dev/null +++ b/packages/jsonr/src/parser/__tests__/ast.spec.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { JsonAstParser } from '../ast.ts'; +import { symAsync, symFn, symFnArgs, symFnBody, symMacro } from '../../symbol.ts'; +import { FnValue } from '../../types.ts'; + +describe(JsonAstParser.name, () => { + let parser: JsonAstParser; + + beforeEach(() => { + parser = new JsonAstParser(); + }); + + describe('parse', () => { + it('should parse JSON into object of null prototype', () => { + const ast = parser.parse('{"a":1}'); + expect(ast).toEqual({ a: 1 }); + expect(Object.getPrototypeOf(ast)).toBe(null); + }); + + it('should parse quoted strings', () => { + const ast = parser.parse('["+","\'test","ing"]'); + expect(ast).toEqual(['+', ['\'', 'test'], 'ing']); + }); + + it('should parse unquoted strings', () => { + const ast = parser.parse('["`",[",test","ing"]]'); + expect(ast).toEqual(['`', [[',', 'test'], 'ing']]); + }); + }); + + describe('print', () => { + it('should print compact quoted strings', () => { + const str = parser.print(['+', ['\'', 'test'], 'ing']); + expect(str).toEqual('["+","\'test","ing"]'); + }); + + it('should print compact unquoted strings', () => { + const str = parser.print(['`', [[',', 'test'], 'ing']]); + expect(str).toEqual('["`",[",test","ing"]]'); + }); + + it('should print native function by name', () => { + const f1 = () => true; + const str = parser.print([f1, () => null]); + expect(str).toEqual('["f1","fn#anonymous"]'); + }); + + it('should print user function or macro by their definition', () => { + const f1: FnValue = () => true; + f1[symFn] = true as const; + f1[symFnBody] = ['+', '3', '4']; + const f2: FnValue = () => true; + f2[symFn] = true as const; + f2[symAsync] = true as const; + f2[symFnArgs] = ['c', 'd']; + f2[symFnBody] = ['-', 'c', 'd']; + const m1: FnValue = () => true; + m1[symFn] = true as const; + m1[symMacro] = true as const; + m1[symFnArgs] = ['b']; + m1[symFnBody] = ['!', 'b']; + + const str = parser.print([f1, f2, m1]); + expect(str).toEqual('[["fn",[],["+","3","4"]],["async",["c","d"],["-","c","d"]],["macro",["b"],["!","b"]]]'); + }); + + it('should print promise as empty object', () => { + const str = parser.print([Promise.resolve(123)]); + expect(str).toEqual('[{}]'); + }); + + it('should print numbers correctly', () => { + const str = parser.print([1.2, 3, NaN, +Infinity, -Infinity]); + expect(str).toEqual('[1.2,3,["+","NaN"],["+","Infinity"],["-","Infinity"]]'); + }); + }); +}); diff --git a/packages/jsonr/src/parser/ast.ts b/packages/jsonr/src/parser/ast.ts new file mode 100644 index 00000000..7abcc4ce --- /dev/null +++ b/packages/jsonr/src/parser/ast.ts @@ -0,0 +1,55 @@ +import { symAsync, symFn, symFnArgs, symFnBody, symMacro } from '../symbol.ts'; +import { JSONType, AsyncValue, ObjValue, Parser } from '../types.ts'; +import { isFunction, isObj, isString } from '../utils.ts'; + +/** metael JSON-based AST Parser. */ +export class JsonAstParser implements Parser { + public parse(expr: string): JSONType { + return JSON.parse(expr, reviver); + } + + public print(expr: AsyncValue): string { + return JSON.stringify(expr, replacer); + } +} + +function reviver(_: string, value: JSONType): AsyncValue { + if (isString(value) && value.length > 1) { // handle quoted/unquoted strings + const first = value.charAt(0); + if (first === '\'' || first === ',') { + return [first, value.substring(1)]; + } + } + if (Array.isArray(value)) { return value; } + if (value && (typeof value === 'object')) { // remove object prototype + return Object.entries(value).reduce( + (obj, [key, value]) => ((obj[key] = value), obj), + Object.create(null) as ObjValue + ); + } + return value; +} + +function replacer(_: string, value: AsyncValue): AsyncValue { + if (isFunction(value)) { + if (!value[symFn]) { return value.name || 'fn#anonymous'; } // native function + return [ + value[symMacro] ? "macro" : value[symAsync] ? "async" : "fn", + value[symFnArgs] ?? [], + value[symFnBody] ?? null + ]; + } + if (Array.isArray(value)) { + if (value.length === 2 && (value[0] === '\'' || value[0] === ',') && isString(value[1])) { + return value[0] + value[1]; // handle quoted/unquoted strings + } + return value; + } + if (value && (typeof value === 'object') && !isObj(value)) { + return {}; // unknown object + } + if (typeof value === 'number' && !Number.isFinite(value)) { + return [value < 0 ? '-' : '+', `${Math.abs(value)}`]; // encode NaN, +-Infinity which are not a valid json + } + return value; +} diff --git a/packages/jsonr/src/parser/index.ts b/packages/jsonr/src/parser/index.ts new file mode 100644 index 00000000..cf4b98da --- /dev/null +++ b/packages/jsonr/src/parser/index.ts @@ -0,0 +1 @@ +export * from './ast.ts'; diff --git a/packages/jsonr/src/stdlib/__tests__/ops.spec.ts b/packages/jsonr/src/stdlib/__tests__/ops.spec.ts new file mode 100644 index 00000000..c41f7f9e --- /dev/null +++ b/packages/jsonr/src/stdlib/__tests__/ops.spec.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { DefaultEnv } from '../../env.ts'; +import { AsyncValue, Env } from '../../types.ts'; +import { obj } from '../../utils.ts'; +import { StdOp } from '../index.ts'; + +describe('StdOp', () => { + let env: Env; + + beforeEach(() => { + env = new DefaultEnv(); + }); + + it.each([ + ['in', () => [['test', 'ing', 's'], 2], () => true], + ['in', () => [['test', 'ing', 's'], 3], () => false], + ['in', () => [obj({ a: 1, b: 2 }), 'a'], () => true], + ['.', () => [123, 1], () => null], + ['.', () => [obj({ a: 'b' }), 'a'], () => 'b'], + ['.', () => [obj({ a: 'b' }), 'b'], () => null], + ['.', () => [['a', 'bc'], 1], () => 'bc'], + ['.', () => [['a', 'bc'], 1, 0], () => 'b'], + ['.=', () => [obj({ a: 'b' }), 'a', 'e'], (args: AsyncValue[]) => (expect(args[0]).toEqual(obj({ a: 'e' })), 'e')], + ['.=', () => [obj({ a: 'b' }), 'c', 'd'], (args: AsyncValue[]) => (expect(args[0]).toEqual(obj({ a: 'b', c: 'd' })), 'd')], + ['.=', () => [[1, 2], 0, 3], (args: AsyncValue[]) => (expect(args[0]).toEqual([3, 2]), 3)], + ['.=', () => [[1, 2], 'a', 3], (args: AsyncValue[]) => (expect(args[0]).toEqual([1, 2]), null)], + ['delete', () => [obj({ a: 'b', c: 'd' }), 'a'], (args) => (expect(args[0]).toEqual(obj({ c: 'd' })), true)], + ['delete', () => [obj({ a: 'b' }), 'c'], (args) => (expect(args[0]).toEqual(obj({ a: 'b' })), true)], + ['delete', () => [[1, 2], 0], (args) => (expect(args[0]).toEqual([1, 2]), false)], + ['~', () => [0], () => -1], + ['~', () => [1], () => -2], + ['^', () => [true, false], () => 1], + ['^', () => [true, true], () => 0], + ['&', () => [true, false], () => 0], + ['&', () => [true, true], () => 1], + ['|', () => [true, false], () => 1], + ['|', () => [false, false], () => 0], + ['**', () => [2, 3], () => 8], + ['!==', () => [false, 0], () => true], + ])('should return correct result for call %#: %s', (fn, args, result) => { + const argVals = args() as AsyncValue[]; + expect(StdOp[fn as keyof typeof StdOp].apply(env, argVals)).toEqual(result(argVals)); + }); +}); diff --git a/packages/jsonr/src/stdlib/__tests__/predicates.spec.ts b/packages/jsonr/src/stdlib/__tests__/predicates.spec.ts new file mode 100644 index 00000000..f18906fa --- /dev/null +++ b/packages/jsonr/src/stdlib/__tests__/predicates.spec.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { DefaultEnv } from '../../env.ts'; +import { Interpreter } from '../../interpreter/index.ts'; +import { AsyncValue, Env } from '../../types.ts'; +import { obj } from '../../utils.ts'; +import * as Predicates from '../predicates.ts'; + +const f = ['fn', 'f', ['n'], ['+', 'n', 1]]; +const m = ['macro', 'f', ['n'], ['+', 'n', 1]]; + +describe('StdType.Predicates', () => { + let env: Env; + let interpreter: Interpreter; + + beforeEach(() => { + env = new DefaultEnv(); + interpreter = new Interpreter(); + }); + + it.each([ + ['isEmpty', () => ['abc'], () => false], + ['isEmpty', () => [''], () => true], + ['isEmpty', () => [[]], () => true], + ['isEmpty', () => [['test', 'ing']], () => false], + ['isNull', () => ['abc'], () => false], + ['isNull', () => [null], () => true], + ['isBoolean', () => ['123'], () => false], + ['isBoolean', () => [false], () => true], + ['isString', () => ['123'], () => true], + ['isString', () => [false], () => false], + ['isNumber', () => ['123'], () => false], + ['isNumber', () => [123], () => true], + ['isNaN', () => ['123'], () => false], + ['isNaN', () => [123], () => false], + ['isNaN', () => [NaN], () => true], + ['isFinite', () => ['123'], () => false], + ['isFinite', () => [123], () => true], + ['isFinite', () => [-Infinity], () => false], + ['isArray', () => ['123'], () => false], + ['isArray', () => [['123']], () => true], + ['isObject', () => ['123'], () => false], + ['isObject', () => [obj({ a: 123 })], () => true], + ['isObject', () => [[123]], () => false], + ['isFunction', () => [obj()], () => false], + ['isFunction', () => [interpreter.eval(f, env)], () => true], + ['isMacro', () => [obj()], () => false], + ['isMacro', () => [interpreter.eval(f, env)], () => false], + ['isMacro', () => [interpreter.eval(m, env)], () => true], + ])('should return correct result for call %#: %s', (fn, args, result) => { + const argVals = args() as AsyncValue[]; + expect(Predicates[fn as keyof typeof Predicates].apply(env, argVals as [AsyncValue])).toEqual(result()); + }); +}); diff --git a/packages/jsonr/src/stdlib/__tests__/types.spec.ts b/packages/jsonr/src/stdlib/__tests__/types.spec.ts new file mode 100644 index 00000000..f03c8835 --- /dev/null +++ b/packages/jsonr/src/stdlib/__tests__/types.spec.ts @@ -0,0 +1,63 @@ +import { beforeEach, describe, expect, it } from '@jest/globals'; +import { DefaultEnv } from '../../env.ts'; +import { Interpreter } from '../../interpreter/index.ts'; +import { AsyncValue, Env } from '../../types.ts'; +import { obj } from '../../utils.ts'; +import * as Types from '../types.ts'; + +const bindings = { + test: () => true, +}; +const f = ['fn', 'f', ['n'], ['+', 'n', 1]]; + +describe('StdType', () => { + let env: Env; + let interpreter: Interpreter; + + beforeEach(() => { + env = new DefaultEnv(null, bindings); + interpreter = new Interpreter(); + }); + + it.each([ + ['object', () => [], () => ({})], + ['object', () => ['a', 2], () => ({ a: 2 })], + ['object', () => ['a', true, 'c'], () => ({ a: true, c: null })], + + ['string', () => [], () => 'null'], + ['string', () => [bindings.test], () => 'test'], + ['string', () => [() => null], () => 'fn#anonymous'], + ['string', () => [Promise.resolve(1)], () => '{}'], + ['string', () => [interpreter.eval(f, env)], (args: AsyncValue[]) => `${args[0]}`], + + ['array', () => [], () => []], + ['array', () => [1, 2], () => [1, 2]], + + ['keys', () => ['val'], () => [0, 1, 2]], + ['keys', () => [['test', 'ing', 's']], () => [0, 1, 2]], + ['keys', () => [obj({ a: 1, b: 2 })], () => ['a', 'b']], + + ['values', () => ['val'], () => ['v', 'a', 'l']], + ['values', () => [['test', 'ing', 's']], () => ['test', 'ing', 's']], + ['values', () => [obj({ a: 1, b: 2 })], () => [1, 2]], + + ['entries', () => ['val'], () => [[0, 'v'], [1, 'a'], [2, 'l']]], + ['entries', () => [['test', 'ing']], () => [[0, 'test'], [1, 'ing']]], + ['entries', () => [obj({ a: 1, b: 2 })], () => [['a', 1], ['b', 2]]], + + ['length', () => ['abc'], () => 3], + ['length', () => [null], () => 0], + ['length', () => [3], () => 1], + ['length', () => [['test', 'ing']], () => 2], + ['length', () => [obj({ a: 1, b: 2, c: 3 })], () => 3], + + ['map', () => [(s: string) => s + 'd', 'abc'], () => 'abcd'], + ['map', () => [(s: string) => s.substring(0, 2), ['test', 'ing']], () => ['te', 'in']], + + ['slice', () => [['a', 'bc', 'd'], 1, 3], () => ['bc', 'd']], + ['slice', () => ['abc', 1, 3], () => 'bc'], + ])('should return correct result for call %#: %s', (fn, args, result) => { + const argVals = args() as AsyncValue[]; + expect(Types[fn as keyof typeof Types].apply(env, argVals)).toEqual(result(argVals)); + }); +}); diff --git a/packages/jsonr/src/stdlib/index.ts b/packages/jsonr/src/stdlib/index.ts new file mode 100644 index 00000000..bfdee934 --- /dev/null +++ b/packages/jsonr/src/stdlib/index.ts @@ -0,0 +1,38 @@ +import { Bindings } from '../types.ts'; +import * as Ops from './ops.ts'; +import * as Predicates from './predicates.ts'; +import * as Types from './types.ts'; + +/** Standard type conversion bindings. */ +export const StdType = Object.freeze({ ...Types, ...Predicates }) satisfies Bindings; + +/** Standard operation bindings. */ +export const StdOp = Object.freeze({ + '+': Ops.plus, + '-': Ops.minus, + '*': Ops.times, + '/': Ops.div, + '**': Ops.pow, + '%': Ops.rem, + '^': Ops.xor, + '&': Ops.and, + '|': Ops.or, + '~': Ops.inv, + '!': Ops.not, + '<': Ops.lessThan, + '>': Ops.greaterThan, + '<=': Ops.lessEquals, + '>=': Ops.greaterEquals, + '===': Ops.equals, + '!==': Ops.notEquals, + 'delete': Ops.del, + 'in': Ops.contains, + '.': Ops.get, + '.=': Ops.set, + '.()': Ops.call, + '[]': Types.array, + '...': Types.concat, +}) satisfies Bindings; + +/** Standard library bindings. */ +export const Stdlib = Object.freeze({ ...StdType, ...StdOp }) satisfies Bindings; diff --git a/packages/jsonr/src/stdlib/ops.ts b/packages/jsonr/src/stdlib/ops.ts new file mode 100644 index 00000000..6545a0c1 --- /dev/null +++ b/packages/jsonr/src/stdlib/ops.ts @@ -0,0 +1,111 @@ +import { AsyncValue, Env, FnValue, ObjValue } from '../types.ts'; +import { isNumber, isObj, isString } from '../utils.ts'; + +export function plus(a: AsyncValue, ...args: AsyncValue[]): number | string { + if (!args.length) { return +(a as number); } + let result = a as number; + for (const arg of args) { result += arg as number; } + return result; +} + +export function minus(a: AsyncValue, b?: AsyncValue): number { + if (b === void 0) { return -(a as number); } + return (a as number) - (b as number); +} + +export function times(a: AsyncValue, b: AsyncValue): number { + return (a as number) * (b as number); +} + +export function div(a: AsyncValue, b: AsyncValue): number { + return (a as number) / (b as number); +} + +export function pow(a: AsyncValue, b: AsyncValue): number { + return (a as number) ** (b as number); +} + +export function rem(a: AsyncValue, b: AsyncValue): number { + return (a as number) % (b as number); +} + +export function xor(a: AsyncValue, b: AsyncValue): number { + return (a as number) ^ (b as number); +} + +export function and(a: AsyncValue, b: AsyncValue): number { + return (a as number) & (b as number); +} + +export function or(a: AsyncValue, b: AsyncValue): number { + return (a as number) | (b as number); +} + +export function inv(a: AsyncValue): number { + return ~(a as number); +} + +export function not(a: AsyncValue): boolean { + return !a; +} + +export function equals(a: AsyncValue, b: AsyncValue): boolean { + return a === b; +} + +export function notEquals(a: AsyncValue, b: AsyncValue): boolean { + return a !== b; +} + +export function lessThan(a: AsyncValue, b: AsyncValue): boolean { + return (a as number) < (b as number); +} + +export function lessEquals(a: AsyncValue, b: AsyncValue): boolean { + return (a as number) <= (b as number); +} + +export function greaterThan(a: AsyncValue, b: AsyncValue): boolean { + return (a as number) > (b as number); +} + +export function greaterEquals(a: AsyncValue, b: AsyncValue): boolean { + return (a as number) >= (b as number); +} + +export function del(target: AsyncValue, key: AsyncValue): boolean { + if (!isObj(target)) { return false; } + return (delete (target as ObjValue)[key as string]); +} + +export function contains(target: AsyncValue, key: AsyncValue): boolean { + return ((key as string) ?? '') in (target as ObjValue); +} + +export function get(val: AsyncValue, ...paths: AsyncValue[]): AsyncValue { + let result = val; + for (const arg of paths) { + if (Array.isArray(result) || isObj(result)) { + result = (result as ObjValue)[arg as string] as AsyncValue; + } else if (isString(result)) { + result = result.charAt(+(arg as number)); + } else { + return null; + } + } + return result ?? null; +} + +export function set(target: AsyncValue, key: AsyncValue, val: AsyncValue): AsyncValue { + if ((Array.isArray(target) && isNumber(key)) || isObj(target)) { + return ((target as ObjValue)[key as string] = val); + } + return null; +} + +export function call(this: Env, target: AsyncValue, key: AsyncValue, ...args: AsyncValue[]) { + if ((Array.isArray(target) && isNumber(key)) || isObj(target)) { + return ((target as ObjValue)[key as string] as FnValue)?.apply(this, args) ?? null; + } + return null; +} diff --git a/packages/jsonr/src/stdlib/predicates.ts b/packages/jsonr/src/stdlib/predicates.ts new file mode 100644 index 00000000..0e4bde9c --- /dev/null +++ b/packages/jsonr/src/stdlib/predicates.ts @@ -0,0 +1,41 @@ +import { symMacro } from '../symbol.ts'; +import { AsyncValue } from '../types.ts'; +import { isFunction, isNumber, isObj, isString } from '../utils.ts'; +import { length } from './types.ts'; + +/** Returns if value is empty. */ +export function isEmpty(iterable: AsyncValue): boolean { + return !length(iterable); +} + +/** Returns if value is null. */ +export function isNull(val: AsyncValue): boolean { + return val === null; +} + +/** Returns if value is true or false. */ +export function isBoolean(val: AsyncValue): boolean { + return val === true || val === false; +} + +/** Returns if value is an array. */ +export function isArray(val: AsyncValue): boolean { + return Array.isArray(val); +} + +/** Returns if value is a macro function. */ +export function isMacro(val: AsyncValue): boolean { + return isFunction(val) && !!val[symMacro]; +} + +/** Returns if value is NaN. */ +export function isNaN(val: AsyncValue): boolean { + return Number.isNaN(val); +} + +/** Returns if value is a finite number. */ +export function isFinite(val: AsyncValue): boolean { + return Number.isFinite(val); +} + +export { isFunction, isNumber, isObj as isObject, isString }; diff --git a/packages/jsonr/src/stdlib/types.ts b/packages/jsonr/src/stdlib/types.ts new file mode 100644 index 00000000..fa923e6e --- /dev/null +++ b/packages/jsonr/src/stdlib/types.ts @@ -0,0 +1,86 @@ +import { symFn } from '../symbol.ts'; +import { AsyncValue, Env, FnValue, ObjValue } from '../types.ts'; +import { assertFunction, isFunction, isObj, isString } from '../utils.ts'; + +/** Creates an array from elements. */ +export function array(...vals: AsyncValue[]): AsyncValue[] { + return vals; +} + +/** Creates an object from entries. */ +export function object(...entries: AsyncValue[]): ObjValue { + const result = Object.create(null); + for (let i = 0; i < entries.length; i += 2) { + result[entries[i] as string] = entries[i + 1] ?? null; + } + return result; +} + +/** Stringify given value, while sanitizing native objects and functions. */ +export function string(val: AsyncValue): string { + if (isFunction(val) && !val[symFn]) { return val.name || 'fn#anonymous'; } + if (val && !Array.isArray(val) && !isObj(val) && typeof val === 'object') { return '{}'; } + return `${val ?? null}`; +} + +/** Returns given value as boolean */ +export function boolean(a: AsyncValue): boolean { + return !!a; +} + +/** Converts a string to an integer number. */ +export const parseInt = global.parseInt as FnValue; + +/** Converts a string to a number. */ +export const parseFloat = global.parseFloat as FnValue; + +/** Merges arrays or objects. */ +export function concat(val: AsyncValue, ...vals: AsyncValue[]): AsyncValue { + if (!vals.length) { return val; } + if (isObj(val)) { return Object.assign(Object.create(null), val, ...vals); } + return ([] as AsyncValue[]).concat(val, ...vals); +} + +/** Returns a subarray or substring. */ +export function slice(val: AsyncValue, start: AsyncValue, end: AsyncValue): AsyncValue { + if (Array.isArray(val)) { return val.slice(start as number, end as number); } + if (isString(val)) { return val.slice(start as number, end as number); } + return val; +} + +/** Maps an array or value. */ +export function map(this: Env, fn: AsyncValue, val: AsyncValue): AsyncValue { + assertFunction(fn, 'arg0 of map'); + return Array.isArray(val) ? val.map(fn, this) : fn.call(this, val ?? null, 0, null); +} + +/** Returns length of an array, string or object. */ +export function length(iterable: AsyncValue): number { + if (Array.isArray(iterable) || isString(iterable)) { return iterable.length; } + if (isObj(iterable)) { return Object.keys(iterable).length; } + return iterable === null ? 0 : 1; +} + +/** Returns keys of an array, string or object. */ +export function keys(iterable: AsyncValue): (string | number)[] { + if (Array.isArray(iterable)) { return [...iterable.keys()]; } + if (isString(iterable)) { return Array.from({ length: iterable.length }, (_, k) => k); } + if (isObj(iterable)) { return Object.keys(iterable); } + return []; +} + +/** Returns values of an array, string or object. */ +export function values(iterable: AsyncValue): AsyncValue[] { + if (Array.isArray(iterable)) { return iterable; } + if (isString(iterable)) { return [...iterable]; } + if (isObj(iterable)) { return Object.values(iterable); } + return []; +} + +/** Returns entries of an array, string or object. */ +export function entries(iterable: AsyncValue): [string | number, AsyncValue][] { + if (Array.isArray(iterable)) { return [...iterable.entries()]; } + if (isString(iterable)) { return [...[...iterable].entries()]; } + if (isObj(iterable)) { return Object.entries(iterable); } + return []; +} diff --git a/packages/jsonr/src/symbol.ts b/packages/jsonr/src/symbol.ts new file mode 100644 index 00000000..f9b090fa --- /dev/null +++ b/packages/jsonr/src/symbol.ts @@ -0,0 +1,7 @@ +export const symEnv = Symbol.for('@metael/env'); +export const symFn = Symbol.for('@metael/fn'); +export const symFnArgs = Symbol.for('@metael/fn-args'); +export const symFnBody = Symbol.for('@metael/fn-body'); +export const symAsync = Symbol.for('@metael/async'); +export const symMacro = Symbol.for('@metael/macro'); +export const symCtrl = Symbol.for('@metael/ctrl'); diff --git a/packages/jsonr/src/types.ts b/packages/jsonr/src/types.ts new file mode 100644 index 00000000..670e430a --- /dev/null +++ b/packages/jsonr/src/types.ts @@ -0,0 +1,112 @@ +import { MaybePromise } from '@mithic/commons'; +import { symAsync, symEnv, symFn, symFnArgs, symFnBody, symMacro } from './symbol.ts'; + +/** metael AST evaluator. */ +export interface Evaluator { + /** Compiles given JSON AST into an unscoped function. */ + compile(expr: JSONType, args?: string[]): FnValue; + + /** Evaluates a JSON AST in given environment. Returns Promise for async code. */ + eval(expr: JSONType, env: Env): AsyncValue; +} + +/** metael expression parser. */ +export interface Parser { + /** Parses given expression string into JSON AST. */ + parse(expr: string): JSONType; + + /** Prints given value or AST as string. */ + print(expr: AsyncValue): string; +} + +/** Environment that defines variable scope. */ +export interface Env { + /** Returns the parent env scope, or null if the env is the global scope. */ + readonly parent: Env | null; + + /** Returns the global env scope. */ + readonly global: Env; + + /** Indicates if current scope is within a function. */ + fn: boolean; + + /** Indicates if the env is async. */ + async: boolean; + + /** Gets a variable. */ + get(name: string): AsyncValue | undefined; + + /** Gets a variable defined on current scope. */ + getOwn(name: string): AsyncValue | undefined; + + /** Sets a variable. */ + set(name: string, value: V): V; + + /** Sets a variable on current scope. */ + setOwn(name: string, value: V, readOnly?: boolean): V; + + /** Push a new scope to the stack and returns the new env. */ + push(): Env; +} + +/** Variable bindings. */ +export interface Bindings { + [key: string | symbol]: AsyncValue; +} + +/** Primitive types. */ +export type Primitive = string | number | boolean; + +/** Input value type. */ +export type JSONType = null | Primitive | JSONType[] | { [key: string]: JSONType }; + +/** Awaited value type. */ +export type Value = + null | Primitive | + AsyncValue[] | + ObjValue | + FnValue + /** | NativeObject */; + +/** A {@link Value} that may be wrapped with a Promise. */ +export type AsyncValue = MaybePromise; + +/** Object value type. */ +export type ObjValue = { [key: string]: AsyncValue }; + +/** Function value type. */ +export type FnValue + = ((this: Env, ...args: Args) => AsyncValue) & UserFnDef & AsyncFnDef & MacroFnDef; + +/** Async function value type. */ +export type AsyncFnValue = FnValue & Required; + +/** Macro function value type. */ +export type MacroValue = FnValue & Required; + +/** Macro function discriminator. */ +export interface MacroFnDef { + /** Discriminator for macros. */ + [symMacro]?: true; +} + +/** Async function discriminator. */ +export interface AsyncFnDef { + /** Discriminator for async function. */ + [symAsync]?: true; +} + +/** User function definitions. */ +export interface UserFnDef { + /** Discriminator for user defined functions. */ + [symFn]?: true; + + /** Env scope in which the function is declared in, or null for global scope. */ + [symEnv]?: Env | null; + + /** Argument names for this function. */ + [symFnArgs]?: string[]; + + /** Function body. */ + [symFnBody]?: AsyncValue; +} diff --git a/packages/jsonr/src/utils.ts b/packages/jsonr/src/utils.ts new file mode 100644 index 00000000..07bd4801 --- /dev/null +++ b/packages/jsonr/src/utils.ts @@ -0,0 +1,46 @@ +import { AsyncValue, FnValue, JSONType, ObjValue } from './types.ts'; + +export function assertString(arg: AsyncValue, msg?: string): asserts arg is string { + if (typeof arg !== 'string') { throw new SyntaxError(msg ?? `Unexpected ${typeof arg}, expect string`); } +} + +export function assertArray(arg: AsyncValue, msg?: string): asserts arg is AsyncValue[] { + if (!Array.isArray(arg)) { throw new SyntaxError(msg ?? `Unexpected ${typeof arg}, expect array`); } +} + +export function assertStrings(args: AsyncValue, msg?: string): asserts args is string[] { + if (!Array.isArray(args)) { throw new SyntaxError(msg ?? `Unexpected ${typeof args}, expect string[]`); } + for (const arg of args) { assertString(arg); } +} + +export function assertFunction(arg: AsyncValue, name = arg): asserts arg is FnValue { + if (typeof arg !== 'function') { throw new TypeError(`${name} is not a function`); } +} + +/** Returns if value is a number. */ +export function isNumber(arg: AsyncValue): arg is number { + return typeof arg === 'number'; +} + +/** Returns if value is a string. */ +export function isString(arg: AsyncValue): arg is string { + return typeof arg === 'string'; +} + +/** Returns if value is a function. */ +export function isFunction(arg: AsyncValue): arg is FnValue { + return typeof arg === 'function'; +} + +/** returns if given value is an object. */ +export function isObj(arg: AsyncValue): arg is ObjValue { + return (arg !== null && typeof arg === 'object' && Object.getPrototypeOf(arg) === null); +} + +/** Converts a native object into object. */ +export function obj(o: Record = {}): Record { + return Object.entries(o).reduce( + (o, [k, v]) => (o[k] = v, o), + Object.create(null) + ); +} diff --git a/packages/jsonr/tsconfig.build.json b/packages/jsonr/tsconfig.build.json new file mode 100644 index 00000000..d81366be --- /dev/null +++ b/packages/jsonr/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "**/__tests__/**" + ] +} diff --git a/packages/jsonr/tsconfig.json b/packages/jsonr/tsconfig.json new file mode 100644 index 00000000..77fb804f --- /dev/null +++ b/packages/jsonr/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": [ + "./src" + ] +} diff --git a/packages/jsonr/typedoc.json b/packages/jsonr/typedoc.json new file mode 100644 index 00000000..b6aba3cb --- /dev/null +++ b/packages/jsonr/typedoc.json @@ -0,0 +1,8 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "src/index.ts" + ] +} diff --git a/packages/messaging/package.json b/packages/messaging/package.json index ce7b9498..d82d39dd 100644 --- a/packages/messaging/package.json +++ b/packages/messaging/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/messaging/src/impl/simple.ts b/packages/messaging/src/impl/simple.ts index f589cfa8..d16c2512 100644 --- a/packages/messaging/src/impl/simple.ts +++ b/packages/messaging/src/impl/simple.ts @@ -11,13 +11,7 @@ export class SimpleMessageBus implements MessageBus { ) { } - public dispatch = maybeAsync(function* (this: SimpleMessageBus, message: Message, options?: MessageOptions) { - const topic = options?.topic ?? this.defaultTopic; - const handlers = this.handlers.get(topic) || []; - for (const handler of handlers) { - yield handler(message, { topic }); - } - }, this); + public dispatch = maybeAsync(this.coDispatch, this); public subscribe(handler: MessageHandler, options?: SubscribeOptions): Unsubscribe { const topic = options?.topic ?? this.defaultTopic; @@ -35,4 +29,12 @@ export class SimpleMessageBus implements MessageBus { index >= 0 && handlers?.splice(index, 1); }; } + + private * coDispatch(this: SimpleMessageBus, message: Message, options?: MessageOptions) { + const topic = options?.topic ?? this.defaultTopic; + const handlers = this.handlers.get(topic) || []; + for (const handler of handlers) { + yield handler(message, { topic }); + } + } } diff --git a/packages/plugins/denokv/package.json b/packages/plugins/denokv/package.json index f66f64a9..48b407ca 100644 --- a/packages/plugins/denokv/package.json +++ b/packages/plugins/denokv/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts,.tsx", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/plugins/ipfs/package.json b/packages/plugins/ipfs/package.json index ba07f7eb..83bc9313 100644 --- a/packages/plugins/ipfs/package.json +++ b/packages/plugins/ipfs/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts,.tsx", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/plugins/level/package.json b/packages/plugins/level/package.json index 49fe9b5d..3f9b84d3 100644 --- a/packages/plugins/level/package.json +++ b/packages/plugins/level/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts,.tsx", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc" diff --git a/packages/plugins/redis/package.json b/packages/plugins/redis/package.json index 8bd60c86..9db1b1d0 100644 --- a/packages/plugins/redis/package.json +++ b/packages/plugins/redis/package.json @@ -18,7 +18,7 @@ "prebuild": "npm run lint", "build": "npm run tsc && npm run babel", "lint": "eslint src --ext .ts,.tsx", - "babel": "babel src -d dist -x '.ts' --out-file-extension .js --root-mode upward", + "babel": "babel src -d dist -x '.ts' --root-mode upward", "tsc": "tsc --project tsconfig.build.json", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "doc": "typedoc"