diff --git a/.github/workflows/coverall.yml b/.github/workflows/coverall.yml index f62897f..8b10e4d 100644 --- a/.github/workflows/coverall.yml +++ b/.github/workflows/coverall.yml @@ -7,17 +7,15 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - - name: Use Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x + - name: Set up Bun + uses: oven-sh/setup-bun@v2 - name: Install dependencies - run: npm ci + run: bun install --frozen-lockfile - name: Build grammar - run: npm run grammar + run: bun run grammar - name: run coverage - run: npm run coverage + run: bun run coverage - name: Coveralls uses: coverallsapp/github-action@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e655ce7..7e86c5a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,24 +12,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 - strategy: - matrix: - node-version: [20.x] - steps: - name: "Checkout current repository" - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} + uses: actions/checkout@v6 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + - name: Set up Biome + uses: biomejs/setup-biome@v2 - name: Install dependencies - run: npm ci - - name: Build grammar - run: npm run grammar + run: bun install --frozen-lockfile - name: Ensure build ok - run: npm run build + run: bun run build - name: Generate definitions - run: npm run definitions + run: bun run definitions - name: Run all tests - run: npm test + run: bun run test diff --git a/README.md b/README.md index e31020b..086d064 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,26 @@ const env1 = new Environment(proxy1, { lib }); const env2 = new Environment(proxy2, { lib }); ``` +## Development + +Building and testing `lambdascript` requires a system installation of +[`bun`](https://bun.com). Bun is deliberately not included in the project's +dependencies. + +```sh +bun install --frozen-lockfile +bun run grammar +bun run test +bun run build +bun run definitions +``` + +[`biome`](https://biomejs.dev) is also deliberately not included as a project +dependency. Install it as a system binary if you want to run `bun run format` +or `bun run check`. The standard `bun run build` script formats the source +before bundling, so it also expects `biome` to be available on `PATH`; direct +use of `bun build` does not require Biome. + # Design & Philosophy We aim for ease of use, correction and security, so chasing down any bug is diff --git a/biome.json b/biome.json index 9778858..1f39334 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,8 @@ { - "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - "organizeImports": { - "enabled": true + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "files": { + "includes": ["**", "!src/parser/λs.js", "!src/parser/λs.terms.js"] }, "formatter": { "indentStyle": "space", @@ -15,7 +16,7 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "preset": "recommended" } } -} \ No newline at end of file +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..7816398 --- /dev/null +++ b/bun.lock @@ -0,0 +1,31 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@okcontract/lambdascript", + "dependencies": { + "@lezer/lr": "^1.4.2", + "@okcontract/cells": "^0.4.0", + "@okcontract/graph": "^0.2.0", + }, + "devDependencies": { + "@lezer/generator": "^1.6.0", + "typescript": "^5.5.4", + }, + }, + }, + "packages": { + "@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + + "@lezer/generator": ["@lezer/generator@1.8.0", "", { "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" }, "bin": { "lezer-generator": "src/lezer-generator.cjs" } }, "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg=="], + + "@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="], + + "@okcontract/cells": ["@okcontract/cells@0.4.0", "", { "dependencies": { "@okcontract/graph": "^0.2.0" } }, "sha512-jRniMpaiWw+uCPBi4598tExiOzIBPPJyApKMRywIvuteZuBEY7HelsCN5N/rPsqUKkk6Zao+NHdkhmO6dD69vw=="], + + "@okcontract/graph": ["@okcontract/graph@0.2.0", "", {}, "sha512-UCYVIyg6m+z2oveyWmO+YMLWvh9OlpXaBtqN9XZ/v5WinIGQZ5EjczInGiuohWS7iJTBfuxHfVi3G9EDqvpQBA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 3783e95..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2535 +0,0 @@ -{ - "name": "@okcontract/lambdascript", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@okcontract/lambdascript", - "version": "0.1.0", - "license": "Apache-2.0", - "dependencies": { - "@lezer/lr": "^1.4.2", - "@okcontract/cells": "^0.3.3", - "@okcontract/graph": "^0.1.5" - }, - "devDependencies": { - "@biomejs/biome": "^1.8.3", - "@lezer/generator": "^1.6.0", - "@types/node": "^22.5.4", - "@vitest/coverage-v8": "^2.0.5", - "happy-dom": "^15.7.3", - "immer": "^10.0.3", - "terser": "^5.31.6", - "typescript": "^5.5.4", - "vite": "^5.2.11", - "vitest": "^2.0.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.3" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", - "dev": true, - "hasInstallScript": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lezer/common": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", - "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", - "license": "MIT" - }, - "node_modules/@lezer/generator": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.2.tgz", - "integrity": "sha512-CwgULPOPPmH54tv4gki18bElLCdJ1+FBC+nGVSVD08vFWDsMjS7KEjNTph9JOypDnet90ujN3LzQiW3CyVODNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.1.0", - "@lezer/lr": "^1.3.0" - }, - "bin": { - "lezer-generator": "src/lezer-generator.cjs" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@okcontract/cells": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@okcontract/cells/-/cells-0.3.3.tgz", - "integrity": "sha512-mSsHQECvwVnwv/oFOskZeotxW7FRDZ3cp9LCv9aVai3AwxRvhNlucENea3MgaK9KgYdKntDOdbghEvFgnIUuuA==", - "license": "MIT", - "dependencies": { - "@okcontract/graph": "^0.1.5" - } - }, - "node_modules/@okcontract/graph": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@okcontract/graph/-/graph-0.1.5.tgz", - "integrity": "sha512-styBdPq2zqovzRDBkByDgWB+l5poahxESq1KisOaV4FtwYsjD3q3jg7lsEWnhwYoKviHGl5Qa7UffrU7zfYDrg==", - "license": "MIT" - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz", - "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz", - "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz", - "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz", - "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz", - "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz", - "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz", - "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz", - "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz", - "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz", - "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz", - "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz", - "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz", - "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz", - "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz", - "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz", - "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz", - "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz", - "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz", - "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.20.0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", - "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "2.1.8", - "vitest": "2.1.8" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", - "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", - "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", - "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.8", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", - "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.8", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", - "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", - "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.8", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", - "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/happy-dom": { - "version": "15.11.7", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz", - "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^4.5.0", - "webidl-conversions": "^7.0.0", - "whatwg-mimetype": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/loupe": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", - "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz", - "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.29.1", - "@rollup/rollup-android-arm64": "4.29.1", - "@rollup/rollup-darwin-arm64": "4.29.1", - "@rollup/rollup-darwin-x64": "4.29.1", - "@rollup/rollup-freebsd-arm64": "4.29.1", - "@rollup/rollup-freebsd-x64": "4.29.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", - "@rollup/rollup-linux-arm-musleabihf": "4.29.1", - "@rollup/rollup-linux-arm64-gnu": "4.29.1", - "@rollup/rollup-linux-arm64-musl": "4.29.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", - "@rollup/rollup-linux-riscv64-gnu": "4.29.1", - "@rollup/rollup-linux-s390x-gnu": "4.29.1", - "@rollup/rollup-linux-x64-gnu": "4.29.1", - "@rollup/rollup-linux-x64-musl": "4.29.1", - "@rollup/rollup-win32-arm64-msvc": "4.29.1", - "@rollup/rollup-win32-ia32-msvc": "4.29.1", - "@rollup/rollup-win32-x64-msvc": "4.29.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", - "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", - "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", - "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.8", - "@vitest/mocker": "2.1.8", - "@vitest/pretty-format": "^2.1.8", - "@vitest/runner": "2.1.8", - "@vitest/snapshot": "2.1.8", - "@vitest/spy": "2.1.8", - "@vitest/utils": "2.1.8", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.8", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.8", - "@vitest/ui": "2.1.8", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - } - } -} diff --git a/package.json b/package.json index 87b4d8c..f586b93 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,11 @@ "description": "lambdascript is a reactive functional scripting language", "private": false, "version": "0.1.0", - "main": "dist/lambdascript.js", + "main": "dist/lambdascript.umd.cjs", + "module": "dist/lambdascript.js", "type": "module", + "packageManager": "bun@1.3.14", + "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/lambdascript.js", @@ -18,33 +21,24 @@ "LICENSE" ], "devDependencies": { - "@biomejs/biome": "^1.8.3", "@lezer/generator": "^1.6.0", - "@types/node": "^22.5.4", - "@vitest/coverage-v8": "^2.0.5", - "happy-dom": "^15.7.3", - "immer": "^10.0.3", - "terser": "^5.31.6", - "typescript": "^5.5.4", - "vite": "^5.2.11", - "vitest": "^2.0.5" + "typescript": "^5.5.4" }, "dependencies": { "@lezer/lr": "^1.4.2", - "@okcontract/cells": "^0.3.3", - "@okcontract/graph": "^0.1.5" + "@okcontract/cells": "^0.4.0", + "@okcontract/graph": "^0.2.0" }, "scripts": { "clean": "rm -rf dist", "grammar": "lezer-generator src/parser/λs.grammar -o src/parser/λs.js", - "build": "npm run format && vite build", - "test": "vitest run", - "coverage": "vitest run --coverage", + "build": "bun run grammar && bun run format && bun build src/index.ts --outfile dist/lambdascript.js --target browser --format esm --conditions=source --minify && bun build src/index.ts --outfile dist/lambdascript.umd.cjs --target browser --format cjs --conditions=source --minify", + "test": "bun test --parallel", + "coverage": "bun test --coverage --coverage-reporter=lcov", "definitions": "tsc --project tsconfig.build.json", - "prepublishOnly": "npm test && npm run build && npm run check && npm run definitions", - "check": "npx @biomejs/biome check src", - "format": "npx @biomejs/biome format src --write && npx @biomejs/biome check src --write", - "formatReadme": "prettier README.md --prose-wrap always --print-width 78 -w" + "prepublishOnly": "bun run grammar && bun run test && bun run build && bun run check && bun run definitions", + "check": "biome check src", + "format": "biome check src --write" }, "repository": { "type": "git", @@ -56,4 +50,4 @@ "url": "https://github.com/okcontract/lambdascript/issues" }, "homepage": "https://hbbio.github.io/lambdascript" -} \ No newline at end of file +} diff --git a/src/ast.ts b/src/ast.ts index b70bd44..996206d 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -1,6 +1,7 @@ import { Rational } from "./rational"; export type NodeType = ASTNode["type"]; +export type SourceRange = { from: number; to: number }; export const NameApplication = "app"; export const NameConstant = "const"; @@ -37,20 +38,31 @@ export type ASTNode = | TupleNode // TemporaryNodes - | { type: "named"; key: string; value: ASTNode }; + | { type: "named"; key: string; value: ASTNode; range?: SourceRange }; export type ApplicationNode = { type: typeof NameApplication; function: ASTNode; params: ASTNode[]; + range?: SourceRange; }; +export const apply = (fn: ASTNode, params: ASTNode[]): ApplicationNode => ({ + type: NameApplication, + function: fn, + params +}); + +export const applyName = (fn: string, params: ASTNode[]): ApplicationNode => + apply(newVariable(fn), params); + // RawConstant without extensions (e.g. Address in `@okcontract/sdk`) export type RawConstant = string | boolean | Rational; export interface ConstantNode { type: typeof NameConstant; value: RawConstant | Extensions; + range?: SourceRange; } export const newNumber = ( @@ -76,6 +88,7 @@ export const newConstant = ( export interface VariableNode { type: typeof NameVariable; name: string; + range?: SourceRange; } export const newVariable = (name: string): VariableNode => ({ @@ -86,11 +99,13 @@ export const newVariable = (name: string): VariableNode => ({ export interface ListNode { type: typeof NameList; elements: ASTNode[]; + range?: SourceRange; } export interface TupleNode { type: typeof NameTuple; elements: ASTNode[]; + range?: SourceRange; } export const newList = (elements: ASTNode[]): ListNode => ({ @@ -106,6 +121,7 @@ export const newTuple = (elements: ASTNode[]): TupleNode => ({ export interface ObjectNode { type: typeof NameObject; values: { [key: string]: ASTNode }; + range?: SourceRange; } export const newObject = (values: { [key: string]: ASTNode }): ObjectNode => ({ @@ -127,18 +143,22 @@ export const newFromJS = (value: unknown): ASTNode => typeof value === "bigint" || typeof value === "boolean" ? newConstant(value) - : // @todo fail? - newConstant(null); + : { + type: NameError, + value: `Unsupported JS value: ${String(value)}` + }; export type FieldNode = { type: typeof NameField; expr: ASTNode; + range?: SourceRange; } & ({ field: string } | { sub: ASTNode }); export interface LambdaNode { type: typeof NameLambda; parameter: string; body: ASTNode; + range?: SourceRange; } export const newLambda = (parameter: string, body: ASTNode): LambdaNode => ({ @@ -159,4 +179,5 @@ export const newLambdaMulti = ( export interface ErrorNode { type: typeof NameError; value: string; + range?: SourceRange; } diff --git a/src/deps.test.ts b/src/deps.test.ts index fb0adfb..52354cd 100644 --- a/src/deps.test.ts +++ b/src/deps.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { type ApplicationNode, diff --git a/src/env.test.ts b/src/env.test.ts index 0d93f9e..123313e 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -1,16 +1,15 @@ +import { beforeAll, describe, expect, it, test } from "bun:test"; import { writeFileSync } from "node:fs"; -import { beforeAll, describe, expect, it, test } from "vitest"; - import { Debugger, Sheet, SheetProxy, uncellify } from "@okcontract/cells"; import { Environment } from "./env"; import { isEqual } from "./equal"; +import { toEqualWithRationals } from "./extend.test"; import { parseExpression } from "./parse"; import { Program } from "./program"; import { Rational } from "./rational"; import { defaultLibrary } from "./stdlib"; -import { toEqualWithRationals } from "./extend.test"; beforeAll(() => { toEqualWithRationals(); }); @@ -21,7 +20,9 @@ test("env withValue", async () => { const proxy = new SheetProxy(sheet); const env = new Environment(proxy); const env2 = env.withValue("x", proxy.new("foo")); - expect(env2.value("x").get()).resolves.toBe("foo"); + const valueCell = env2.value("x"); + if (!valueCell) throw new Error("Value not found in environment"); + await expect(valueCell.get()).resolves.toBe("foo"); }); test("evaluate string", async () => { @@ -46,11 +47,15 @@ test("case insensitive values", async () => { await env.addExpression("test", await parseExpression("10")); expect(env.case("test")).toBe("test"); for (const s of ["test", "Test", "TEST"]) - await expect(env.evaluateStringValue(s)).resolves.toEqual(new Rational(10)); + await expect(env.evaluateStringValue(s)).resolves.toEqualRational( + new Rational(10) + ); await env.addExpression("TEST", await parseExpression("11")); expect(env.case("TEST")).toBe("test"); for (const s of ["test", "Test", "TEST"]) - await expect(env.evaluateStringValue(s)).resolves.toEqual(new Rational(11)); + await expect(env.evaluateStringValue(s)).resolves.toEqualRational( + new Rational(11) + ); }); describe("eval programs", async () => { @@ -60,6 +65,15 @@ describe("eval programs", async () => { { foo: new Rational(1), bar: new Rational(1) }, "get" ], + [ + ["let foo = 1", "and bar = foo + 1", "in bar", "", "baz = bar + 1"], + { + foo: new Rational(1), + bar: new Rational(2), + baz: new Rational(3) + }, + "let_component" + ], [ ["y:x*x", "foo:x+1", "bar:y*x", "x:2"], { @@ -140,7 +154,7 @@ describe("eval programs", async () => { writeFileSync(`${pt[2]}.dot`, debug.dot(`${pt[0][0]}...`)); for (const [key, expectedValue] of Object.entries(pt[1])) { const computedValue = up.value(key); - await expect(uncellify(computedValue)).resolves.toEqual( + await expect(uncellify(computedValue)).resolves.toEqualRational( expectedValue ); } diff --git a/src/env.ts b/src/env.ts index 60a1190..ba986a2 100644 --- a/src/env.ts +++ b/src/env.ts @@ -6,16 +6,16 @@ import { } from "@okcontract/cells"; import type { ASTNode } from "./ast"; -import { type Value, evaluateAST } from "./eval"; +import { evaluateAST, type Value } from "./eval"; import type { ParserExtension } from "./highLevel"; import { inferType } from "./infer"; import { mergeWithFirstPriority } from "./merge"; import { ObjectMapKey } from "./objectMap"; import { type ParseOptions, parseExpression } from "./parse"; import { + defaultLibrary, type LibraryElement, type StandardLibrary, - defaultLibrary, typeBoolean } from "./stdlib"; import type { TypeScheme } from "./typeScheme"; @@ -26,6 +26,8 @@ export type ValueMap = { [key: string]: Value; }; +export type TypeMap = { [key: string]: TypeScheme }; + export type ValueDefinition = [string, Value, TypeScheme]; /** @@ -35,7 +37,7 @@ export type EnvironmentOptions = { id?: string; lib?: StandardLibrary; values?: ValueMap; - types?: { [key: string]: TypeScheme }; + types?: TypeMap; cases?: { [key: string]: string }; parseOptions?: ParseOptions; extensions?: ParserExtension[]; @@ -45,6 +47,11 @@ export type EnvironmentOptions = { export const valuesCase = (initialValues: ValueMap) => ObjectMapKey((key, _) => [key.toLowerCase(), key], initialValues); +const hasBless = ( + value: Value +): value is Value & { bless: (name: string) => void } => + typeof (value as { bless?: unknown }).bless === "function"; + export class Environment { /** static library: resolved first, containing both value and type information */ private _lib: { [key: string]: LibraryElement }; @@ -54,9 +61,9 @@ export class Environment { * However, `inferType` for `NameLambda` uses an Environment updated * with the type of the bound variable in the lambda. **/ - _values: ValueMap; + private _values: ValueMap; /** type expressions (for values) */ - private _types: { [key: string]: TypeScheme }; + private _types: TypeMap; /** original cases for values and types */ private _originalCases: { [key: string]: string }; @@ -64,13 +71,17 @@ export class Environment { initialValues = {} as const; - initialTypes: { [key: string]: TypeScheme } = { + initialTypes: TypeMap = { true: { vars: [], type: typeBoolean }, false: { vars: [], type: typeBoolean } }; options: EnvironmentOptions; + get values() { + return this._values; + } + constructor(proxy: SheetProxy, options: EnvironmentOptions = {}) { envCounter = envCounter + 1; // console.log("new env", { id, envCounter }); @@ -93,7 +104,16 @@ export class Environment { } _updateLib(lib: StandardLibrary) { - this._lib = lib; + // convert to lowercase, convert keys to lowercase + this._originalCases = { + ...Object.fromEntries( + Object.entries(lib).map(([k, _v]) => [k.toLowerCase(), k]) + ), + ...this._originalCases + }; + this._lib = Object.fromEntries( + Object.entries(lib).map(([k, v]) => [k.toLowerCase(), v]) + ); } // getters @@ -165,7 +185,7 @@ export class Environment { // console.log("addExpression", { name, expr }); const type = await this.infer(expr); const value = this.eval(expr); - value.bless(`eval:${name}`); + if (hasBless(value)) value.bless(`eval:${name}`); const nameLow = name.toLowerCase(); // Value already exists, it must have same type. // @todo check type @@ -217,6 +237,13 @@ export class Environment { return overWrite; }; + removeKey = (key: string) => { + const keyLow = key.toLowerCase(); + delete this._types[keyLow]; + delete this._values[keyLow]; + delete this._originalCases[keyLow]; + }; + /** * evaluateString evaluates an expression as string. * @param expr diff --git a/src/equal.test.ts b/src/equal.test.ts new file mode 100644 index 0000000..25d4387 --- /dev/null +++ b/src/equal.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from "bun:test"; +import { Sheet } from "@okcontract/cells"; + +import { Environment } from "./env"; +import { allDiff, firstDiff, isEqual } from "./equal"; + +test("isEqual", () => { + expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBeTruthy(); + expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1, c: 2 })).toBeFalsy(); +}); + +test("errors", () => { + const err = new Error("foo"); + expect(isEqual(err, err)).toBeTruthy(); + expect(isEqual(err, new Error("foo"))).toBeTruthy(); +}); + +test("undefined", () => { + expect(isEqual(undefined, undefined)).toBeTruthy(); + expect(isEqual(undefined, null)).toBeFalsy(); + expect(isEqual(undefined, 1)).toBeFalsy(); + expect(isEqual(1, undefined)).toBeFalsy(); +}); + +test("array", () => { + expect(isEqual([], [])).toBeTruthy(); + expect(isEqual([], null)).toBeFalsy(); + expect(isEqual([1, 2, 3], [1, 2, 3])).toBeTruthy(); + expect(isEqual([1, 2, 3, 4], [1, 2, 3])).toBeFalsy(); + expect(isEqual([1, 2, 3], [1, 2, 3, 4])).toBeFalsy(); + expect(isEqual([1, 2, [1, 2, 3]], [1, 2, [1, 2, 3]])).toBeTruthy(); +}); + +test("cells", () => { + const proxy = new Sheet().newProxy(); + const a = proxy.new(1); + expect(isEqual(1, a)).toBeFalsy(); + const b = proxy.new(1); + expect(isEqual(a, b)).toBeFalsy(); +}); + +test("env", () => { + const proxy = new Sheet().newProxy(); + const v = proxy.new(1); + const w = proxy.new(1); + const a = new Environment(proxy, { values: { v } }); + const b = new Environment(proxy, { values: { v } }); + const c = new Environment(proxy, { values: { w } }); + expect(isEqual(a, b)).toBeTruthy(); + expect(isEqual(b, c)).toBeFalsy(); +}); + +test("set deep equality", () => { + const a = new Set([{ id: 1 }, { nested: [1, 2, 3] }]); + const b = new Set([{ nested: [1, 2, 3] }, { id: 1 }]); + const c = new Set([{ id: 1 }, { nested: [1, 2, 4] }]); + expect(isEqual(a, b)).toBeTruthy(); + expect(isEqual(a, c)).toBeFalsy(); +}); + +test("set deep equality with duplicate deep-equal values", () => { + const a = new Set([{ id: 1 }, { id: 1 }]); + const b = new Set([{ id: 1 }, { id: 1 }]); + const c = new Set([{ id: 1 }, { id: 2 }]); + expect(isEqual(a, b)).toBeTruthy(); + expect(isEqual(a, c)).toBeFalsy(); +}); + +test("map deep equality", () => { + const a = new Map([ + [{ id: 1 }, { nested: [1, 2, 3] }], + ["tags", new Set([{ value: 1 }])] + ]); + const b = new Map([ + ["tags", new Set([{ value: 1 }])], + [{ id: 1 }, { nested: [1, 2, 3] }] + ]); + const c = new Map([ + ["tags", new Set([{ value: 1 }])], + [{ id: 1 }, { nested: [1, 2, 4] }] + ]); + expect(isEqual(a, b)).toBeTruthy(); + expect(isEqual(a, c)).toBeFalsy(); +}); + +test("map deep equality with duplicate deep-equal keys", () => { + const a = new Map([ + [{ id: 1 }, "one"], + [{ id: 1 }, "two"] + ]); + const b = new Map([ + [{ id: 1 }, "two"], + [{ id: 1 }, "one"] + ]); + const c = new Map([ + [{ id: 1 }, "one"], + [{ id: 1 }, "three"] + ]); + expect(isEqual(a, b)).toBeTruthy(); + expect(isEqual(a, c)).toBeFalsy(); +}); + +test("set diffs", () => { + const a = new Set([{ count: 1 }]); + const b = new Set([{ count: 2 }]); + expect(firstDiff(a, b)).toBe( + "Set value difference: Key 'count' difference: Value difference: 1 vs 2" + ); + expect(allDiff(a, b)).toEqual([ + "/Set value difference: Key 'count' difference: Value difference: 1 vs 2" + ]); +}); + +test("map diffs", () => { + const a = new Map([["count", { total: 1 }]]); + const b = new Map([["count", { total: 2 }]]); + expect(firstDiff(a, b)).toBe( + "Map value for key \"count\": Key 'total' difference: Value difference: 1 vs 2" + ); + expect(allDiff(a, b)).toEqual([ + '/Map value for key "count"/total/Value: 1 vs 2' + ]); +}); diff --git a/src/equal.ts b/src/equal.ts index 8498436..3a2e6fb 100644 --- a/src/equal.ts +++ b/src/equal.ts @@ -3,8 +3,114 @@ import { Cell } from "@okcontract/cells"; import { Environment } from "./env"; import { Rational } from "./rational"; +function constructorNameOf(value: object) { + return ( + (value as { constructor?: { name?: string } }).constructor?.name ?? "Object" + ); +} + +function isPlainObject(value: object) { + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +} + +function valuePreview(value: unknown) { + if (typeof value === "string") return JSON.stringify(value); + if (typeof value === "symbol") return value.toString(); + return String(value); +} + +function canUseNativeScalarEquality(value: unknown) { + return (value === null || typeof value !== "object") && !Number.isNaN(value); +} + +function findMatchingSetValueIndex( + values: readonly unknown[], + candidate: unknown, + used: readonly boolean[] +) { + for (let index = 0; index < values.length; index++) { + if (used[index]) continue; + if (isEqual(candidate, values[index])) return index; + } + return -1; +} + +function findMatchingMapEntryIndex( + entries: readonly (readonly [unknown, unknown])[], + key: unknown, + value: unknown, + used: readonly boolean[] +) { + for (let index = 0; index < entries.length; index++) { + if (used[index]) continue; + const [otherKey, otherValue] = entries[index]; + if (isEqual(key, otherKey) && isEqual(value, otherValue)) return index; + } + return -1; +} + +function findMatchingMapKeyIndex( + entries: readonly (readonly [unknown, unknown])[], + key: unknown, + used: readonly boolean[] +) { + for (let index = 0; index < entries.length; index++) { + if (used[index]) continue; + if (isEqual(key, entries[index][0])) return index; + } + return -1; +} + +function setsEqual(a: Set, b: Set) { + if (a.size !== b.size) return false; + + const deferredValuesA: unknown[] = []; + const deferredValuesB: unknown[] = []; + + for (const value of a) { + if (canUseNativeScalarEquality(value)) { + if (!b.has(value)) return false; + } else { + deferredValuesA.push(value); + } + } + + if (deferredValuesA.length === 0) return true; + + for (const value of b) { + if (!canUseNativeScalarEquality(value)) deferredValuesB.push(value); + } + + if (deferredValuesA.length !== deferredValuesB.length) return false; + + const used = new Array(deferredValuesB.length).fill(false); + + for (const value of deferredValuesA) { + const matchIndex = findMatchingSetValueIndex(deferredValuesB, value, used); + if (matchIndex === -1) return false; + used[matchIndex] = true; + } + + return true; +} + +function mapsEqual(a: Map, b: Map) { + if (a.size !== b.size) return false; + const entriesB = Array.from(b.entries()); + const used = new Array(entriesB.length).fill(false); + + for (const [key, value] of a) { + const matchIndex = findMatchingMapEntryIndex(entriesB, key, value, used); + if (matchIndex === -1) return false; + used[matchIndex] = true; + } + + return true; +} + export const allDiff = (a: unknown, b: unknown) => { - const differences = []; + const differences: string[] = []; const aux = (a: unknown, b: unknown, path = "") => { if (a === b) return; @@ -15,7 +121,8 @@ export const allDiff = (a: unknown, b: unknown) => { } if (a instanceof Environment && b instanceof Environment) { - aux(a._values, b._values, `${path}/Environment`); + aux(a.values, b.values, `${path}/Environment`); + return; } if (a instanceof Rational && b instanceof Rational) { @@ -32,17 +139,19 @@ export const allDiff = (a: unknown, b: unknown) => { typeof b !== "object" || b === null ) { - differences.push(`${path}/Type: ${typeof a} vs ${typeof b}`); + if (typeof a !== typeof b) + differences.push(`${path}/Type: ${typeof a} vs ${typeof b}`); + else differences.push(`${path}/Value: ${String(a)} vs ${String(b)}`); return; } - if ( - a.constructor.name !== "Object" && - a.constructor.name === b.constructor.name && - a.toString - ) { - if (a.toString() !== b.toString()) - differences.push(`${path}/${a.constructor.name}: ${a} vs ${b}`); + const constructorNameA = constructorNameOf(a); + const constructorNameB = constructorNameOf(b); + + if (constructorNameA !== constructorNameB) { + differences.push( + `${path}/Constructor: ${constructorNameA} vs ${constructorNameB}` + ); return; } @@ -55,8 +164,107 @@ export const allDiff = (a: unknown, b: unknown) => { return; } - const keysA = Object.keys(a); - const keysB = Object.keys(b); + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) + differences.push(`${path}/Set size: ${a.size} vs ${b.size}`); + + const valuesB = Array.from(b); + const used = new Array(valuesB.length).fill(false); + + for (const value of a) { + const matchIndex = findMatchingSetValueIndex(valuesB, value, used); + if (matchIndex !== -1) { + used[matchIndex] = true; + continue; + } + + const diffIndex = used.findIndex((taken) => !taken); + if (diffIndex === -1) { + differences.push( + `${path}/Missing value in second set: ${valuePreview(value)}` + ); + continue; + } + + used[diffIndex] = true; + differences.push( + `${path}/Set value difference: ${ + firstDiff(value, valuesB[diffIndex]) ?? valuePreview(value) + }` + ); + } + + for (let index = 0; index < valuesB.length; index++) { + if (!used[index]) { + differences.push( + `${path}/Unexpected value in second set: ${valuePreview(valuesB[index])}` + ); + } + } + return; + } + + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) + differences.push(`${path}/Map size: ${a.size} vs ${b.size}`); + + const entriesB = Array.from(b.entries()); + const used = new Array(entriesB.length).fill(false); + + for (const [key, value] of a) { + const matchIndex = findMatchingMapEntryIndex( + entriesB, + key, + value, + used + ); + if (matchIndex !== -1) { + used[matchIndex] = true; + continue; + } + + const keyIndex = findMatchingMapKeyIndex(entriesB, key, used); + if (keyIndex !== -1) { + used[keyIndex] = true; + aux( + value, + entriesB[keyIndex][1], + `${path}/Map value for key ${valuePreview(key)}` + ); + continue; + } + + differences.push( + `${path}/Missing map key in second map: ${valuePreview(key)}` + ); + } + + for (let index = 0; index < entriesB.length; index++) { + if (!used[index]) { + differences.push( + `${path}/Unexpected map key in second map: ${valuePreview( + entriesB[index][0] + )}` + ); + } + } + return; + } + + if ( + !isPlainObject(a) && + typeof a.toString === "function" && + typeof b.toString === "function" + ) { + if (a.toString() !== b.toString()) + differences.push(`${path}/${constructorNameA}: ${a} vs ${b}`); + return; + } + + const objA = a as Record; + const objB = b as Record; + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); if (keysA.length !== keysB.length) differences.push( @@ -67,7 +275,7 @@ export const allDiff = (a: unknown, b: unknown) => { if (!keysB.includes(key)) { differences.push(`${path}/Missing key in second object: ${key}`); } else { - aux(a[key], b[key], `${path}/${key}`); + aux(objA[key], objB[key], `${path}/${key}`); } } }; @@ -86,7 +294,7 @@ export function firstDiff(a: unknown, b: unknown): string | null { } if (a instanceof Environment && b instanceof Environment) { - const diff = firstDiff(a._values, b._values); + const diff = firstDiff(a.values, b.values); if (diff) return `Environment difference: ${diff}`; return null; } @@ -96,25 +304,21 @@ export function firstDiff(a: unknown, b: unknown): string | null { return null; } - if ( - typeof a === "object" && - typeof b === "object" && - a.constructor.name !== "Object" && - a.constructor.name === b.constructor.name && - a.toString - ) { - if (a.toString() !== b.toString()) - return `${a.constructor.name}: ${a} vs ${b}`; - return null; - } - if ( typeof a !== "object" || - a === null || typeof b !== "object" || + a === null || b === null ) - return `Type difference: ${typeof a} vs ${typeof b}`; + return typeof a !== typeof b + ? `Type difference: ${typeof a} vs ${typeof b}` + : `Value difference: ${String(a)} vs ${String(b)}`; + + const constructorNameA = constructorNameOf(a); + const constructorNameB = constructorNameOf(b); + + if (constructorNameA !== constructorNameB) + return `Constructor difference: ${constructorNameA} vs ${constructorNameB}`; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) @@ -126,15 +330,78 @@ export function firstDiff(a: unknown, b: unknown): string | null { return null; } - const keysA = Object.keys(a); - const keysB = Object.keys(b); + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) return `Set size difference: ${a.size} vs ${b.size}`; + + const valuesB = Array.from(b); + const used = new Array(valuesB.length).fill(false); + + for (const value of a) { + const matchIndex = findMatchingSetValueIndex(valuesB, value, used); + if (matchIndex !== -1) { + used[matchIndex] = true; + continue; + } + + const diffIndex = used.findIndex((taken) => !taken); + if (diffIndex === -1) + return `Missing value in second set: ${valuePreview(value)}`; + + return `Set value difference: ${ + firstDiff(value, valuesB[diffIndex]) ?? valuePreview(value) + }`; + } + + return null; + } + + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) return `Map size difference: ${a.size} vs ${b.size}`; + + const entriesB = Array.from(b.entries()); + const used = new Array(entriesB.length).fill(false); + + for (const [key, value] of a) { + const matchIndex = findMatchingMapEntryIndex(entriesB, key, value, used); + if (matchIndex !== -1) { + used[matchIndex] = true; + continue; + } + + const keyIndex = findMatchingMapKeyIndex(entriesB, key, used); + if (keyIndex !== -1) { + const diff = firstDiff(value, entriesB[keyIndex][1]); + return diff + ? `Map value for key ${valuePreview(key)}: ${diff}` + : `Map entry difference for key ${valuePreview(key)}`; + } + return `Missing map key in second map: ${valuePreview(key)}`; + } + + return null; + } + + if ( + !isPlainObject(a) && + typeof a.toString === "function" && + typeof b.toString === "function" + ) { + if (a.toString() !== b.toString()) + return `${constructorNameA}: ${a} vs ${b}`; + return null; + } + + const objA = a as Record; + const objB = b as Record; + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); if (keysA.length !== keysB.length) return `Object keys length difference: ${keysA.length} vs ${keysB.length}`; for (const key of keysA) { if (!keysB.includes(key)) return `Missing key in second object: ${key}`; - const diff = firstDiff(a[key], b[key]); + const diff = firstDiff(objA[key], objB[key]); if (diff) return `Key '${key}' difference: ${diff}`; } @@ -146,20 +413,21 @@ export function isEqual(a: unknown, b: unknown): boolean { if (a === b) return true; // Cells are identical if same cell or one cell points to another. - if (a instanceof Cell && b instanceof Cell) - return a.id === b.id || isEqual(a.value, b.value); + if (a instanceof Cell && b instanceof Cell) return a.id === b.id; + // || isEqual(a.value, b.value); // return a.id === b.id || a?.pointed === b.id || a.id === b?.pointed; // Environments are equals if the hold the same values. if (a instanceof Environment && b instanceof Environment) { - const eq = isEqual(a._values, b._values); - // console.log({ - // isEqual: "env", - // a: Object.keys(a._values), - // b: Object.keys(b._values), - // eq, - // reason: allDiff(a, b), - // }); + const eq = isEqual(a.values, b.values); + // if (!eq) + // console.log({ + // isEqual: "env", + // a: Object.keys(a._values), + // b: Object.keys(b._values), + // eq, + // reason: allDiff(a, b) + // }); return eq; } @@ -184,22 +452,30 @@ export function isEqual(a: unknown, b: unknown): boolean { return true; } + if (a instanceof Set && b instanceof Set) return setsEqual(a, b); + if (a instanceof Map && b instanceof Map) return mapsEqual(a, b); + + // Fast rejection for mismatched object kinds before deeper comparison. + if (constructorNameOf(a) !== constructorNameOf(b)) return false; + if ( - a.constructor.name !== "Object" && - a.constructor.name === b.constructor.name && - a.toString + !isPlainObject(a) && + typeof a.toString === "function" && + typeof b.toString === "function" ) return a.toString() === b.toString(); // Compare objects - const keysA = Object.keys(a); - const keysB = Object.keys(b); + const objA = a as Record; + const objB = b as Record; + const keysA = Object.keys(objA); - if (keysA.length !== keysB.length) return false; + if (keysA.length !== Object.keys(objB).length) return false; - for (const key of keysA) { - if (!keysB.includes(key)) return false; // Different shape - if (!isEqual(a[key], b[key])) return false; // Different value + for (let index = 0; index < keysA.length; index++) { + const key = keysA[index]; + if (!Object.hasOwn(objB, key)) return false; // Different shape + if (!isEqual(objA[key], objB[key])) return false; // Different value } return true; diff --git a/src/eval.test.ts b/src/eval.test.ts index 94df6c2..75f410a 100644 --- a/src/eval.test.ts +++ b/src/eval.test.ts @@ -1,5 +1,4 @@ -import { beforeAll, describe, expect, it, test } from "vitest"; - +import { beforeAll, describe, expect, it, test } from "bun:test"; import { type AnyCell, Sheet, @@ -10,21 +9,23 @@ import { import { NameLambda } from "./ast"; import { Environment } from "./env"; import { isEqual } from "./equal"; +import { toEqualWithRationals } from "./extend.test"; import { parseExpression } from "./parse"; import { Rational } from "./rational"; -import { type LibraryElement, defaultLibrary, typeString } from "./stdlib"; +import { defaultLibrary, type LibraryElement, typeString } from "./stdlib"; -import { toEqualWithRationals } from "./extend.test"; beforeAll(() => { toEqualWithRationals(); }); +// Avoid overriding expect globally; default toEqual is sufficient here. + test("eval 1+1", async () => { const proxy = new Sheet(isEqual).newProxy(); const env = new Environment(proxy); const ast = await parseExpression("1 + 1"); const result = env.eval(ast); - await expect(result.get()).resolves.toEqual(new Rational(2)); + await expect(result.get()).resolves.toEqualRational(new Rational(2)); }); describe("eval valid expressions", async () => { @@ -79,7 +80,7 @@ describe("eval valid expressions", async () => { async () => { const ast = await parseExpression(expr); const res = env.eval(ast); - await expect(uncellify(res)).resolves.toEqual(expected); + await expect(uncellify(res)).resolves.toEqualRational(expected); }, { timeout: 500 } ); @@ -120,9 +121,9 @@ test("evaluated expressions are reactive", async () => { const env = new Environment(proxy, { values: { foo } }); const ast = await parseExpression("foo + 1"); const expr = env.eval(ast) as AnyCell; - await expect(expr.get()).resolves.toEqual(new Rational(2)); + await expect(expr.get()).resolves.toEqualRational(new Rational(2)); foo.set(new Rational(2)); - await expect(expr.get()).resolves.toEqual(new Rational(3)); + await expect(expr.get()).resolves.toEqualRational(new Rational(3)); }); test("eval outputs cannot be evaluated before env value definition", async () => { diff --git a/src/eval.ts b/src/eval.ts index 0e31ae9..89bc9d4 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -1,5 +1,7 @@ import type { AnyCell } from "@okcontract/cells"; +const DEV = false; + import { type ASTNode, NameApplication, @@ -46,9 +48,9 @@ export const evaluateAST = if (!v) // @todo agrep to suggest similar names? throw new Error( - `Undefined variable: ${node.name} in ${JSON.stringify( - env.keys(false) - )}` + `Undefined variable: ${node.name}${ + DEV ? ` in ${JSON.stringify(env.keys(false))}` : "" + }` ); return v; } diff --git a/src/extend.test.ts b/src/extend.test.ts index 685fa44..c331c37 100644 --- a/src/extend.test.ts +++ b/src/extend.test.ts @@ -1,19 +1,51 @@ -import { expect, test } from "vitest"; - +import { expect, test } from "bun:test"; import { jsonStringify } from "@okcontract/cells"; import { isEqual } from "./equal"; import { Rational } from "./rational"; -// biome-ignore lint/suspicious/noExportsInTest: used only in tests -export const toEqualWithRationals = () => +declare module "bun:test" { + interface Matchers { + toEqualRational(expected: T | unknown): void; + } + + interface AsymmetricMatchers { + toEqualRational(expected: unknown): void; + } +} + +// Provide a dedicated matcher to avoid overriding global toEqual +const isPlainObject = (value: unknown): value is Record => { + if (!value || typeof value !== "object") { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +}; + +const stripRanges = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(stripRanges); + } + if (isPlainObject(value)) { + const entries = Object.entries(value).filter(([key]) => key !== "range"); + return Object.fromEntries( + entries.map(([key, entryValue]) => [key, stripRanges(entryValue)]) + ); + } + return value; +}; + +export const toEqualRational = () => expect.extend({ - toEqual(received, expected) { - const pass = isEqual(received, expected); + toEqualRational(received, expected) { + const normalizedReceived = stripRanges(received); + const normalizedExpected = stripRanges(expected); + const pass = isEqual(normalizedReceived, normalizedExpected); return { message: () => - `expected ${jsonStringify(received)} to be equal to ${jsonStringify( - expected + `expected ${jsonStringify(normalizedReceived)} to be equal to ${jsonStringify( + normalizedExpected )}`, pass, expected, @@ -22,7 +54,10 @@ export const toEqualWithRationals = () => } }); -test("test toEqualWithRationals", () => { - toEqualWithRationals(); - expect(new Rational(1)).toEqual(new Rational([1n, 1n, false])); +// Backward-compatible helper used by other tests +export const toEqualWithRationals = toEqualRational; + +test("test toEqualRational", () => { + toEqualRational(); + expect(new Rational(1)).toEqualRational(new Rational([1n, 1n, false])); }); diff --git a/src/filter.test.ts b/src/filter.test.ts index 7154e9a..4afd9b6 100644 --- a/src/filter.test.ts +++ b/src/filter.test.ts @@ -1,13 +1,13 @@ -import { beforeAll, expect, test } from "vitest"; - +import { beforeAll, expect, test } from "bun:test"; import { type AnyCell, + filterPredicateCell, Sheet, - SheetProxy, - filterPredicateCell + SheetProxy } from "@okcontract/cells"; import { toEqualWithRationals } from "./extend.test"; + beforeAll(() => { toEqualWithRationals(); }); diff --git a/src/find.test.ts b/src/find.test.ts new file mode 100644 index 0000000..02f7c4a --- /dev/null +++ b/src/find.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test"; +import type { AnyCell } from "@okcontract/cells"; +import { mapArrayCell, Sheet, SheetProxy } from "@okcontract/cells"; + +test("mapArrayCell does not delete externally retained mapped cells", async () => { + const sheet = new Sheet(); + const proxy = new SheetProxy(sheet); + const values = proxy.new([proxy.new(1), proxy.new(2)]); + const fn = proxy.new((cell: AnyCell) => + cell.map((value) => value + 1) + ); + const mapped = mapArrayCell(proxy, values, fn, "mapped"); + + const [firstMapped] = await mapped.get(); + const derived = proxy.map([firstMapped], (value) => value * 10); + await expect(derived.get()).resolves.toBe(20); + + values.set([proxy.new(2)]); + await proxy.wait(); + sheet.collection(); + + const [currentMapped] = await mapped.get(); + expect(currentMapped?.id).not.toBe(firstMapped.id); + expect(sheet.get(firstMapped.id)).toBeDefined(); + expect(sheet.get(derived.id)).toBeDefined(); +}); diff --git a/src/find.ts b/src/find.ts index 1388ec5..2874ae3 100644 --- a/src/find.ts +++ b/src/find.ts @@ -1,50 +1,10 @@ import { type AnyCell, type CellArray, - type MapCell, - type SheetProxy, - collector + mapArrayCell, + type SheetProxy } from "@okcontract/cells"; -/** - * mapArrayCell is a variant of `mapArray` with a function taking - * element cells as arguments. - * @param proxy - * @param arr - * @param fn - * @returns mapped array cell - */ -export const mapArrayCell = ( - proxy: SheetProxy, - arr: CellArray, - fn: AnyCell<(v: AnyCell, idx?: number) => AnyCell>, - name = "map", - nf?: NF -): MapCell[], NF> => { - let prevFn: (v: AnyCell) => AnyCell; - return proxy.map( - [arr, fn], - (cells, _fn, prev) => { - const set = new Set((prev || []).map((cell) => cell.id)); - const arr = cells.map((cell, i) => { - const reuse = - _fn === prevFn && - prev?.find((_c) => _c.dependencies?.[0] === cell.id); - if (reuse) { - set.delete(reuse.id); - return reuse; - } - return _fn(cell, i) as MapCell; - }); - prevFn = _fn; - proxy._sheet.collect(...[...set]); - return arr; - }, - name, - nf - ); -}; - export const findCell = ( proxy: SheetProxy, arr: CellArray, @@ -54,14 +14,11 @@ export const findCell = ( nf?: NF ) => { const keep = mapArrayCell(proxy, arr, predicate, "keep"); - const coll = collector>(proxy); return proxy.map( [arr, keep], (cells, _keep) => - coll( - proxy.mapNoPrevious(_keep, (..._flat) => - findFunction.call(cells, (_, i) => _flat[i]) - ) + proxy.mapNoPrevious(_keep, (..._flat) => + findFunction.call(cells, (_, i) => _flat[i]) ), name, nf diff --git a/src/format.test.ts b/src/format.test.ts new file mode 100644 index 0000000..29d55b0 --- /dev/null +++ b/src/format.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "bun:test"; + +import { formatExpression, formatProgram, Separator } from "./format"; + +describe("formatExpression", () => { + it("formats flat expressions while preserving raw numeric literals", () => { + const formatted = formatExpression("f(1;foo:0xa,bar:{0x1})", { + separator: Separator.Space, + spaces: 2, + trailingCommas: true, + width: 120 + }); + + expect(formatted).toBe("f(1; foo: 0xa, bar: { 0x1 })"); + }); +}); + +describe("formatProgram", () => { + it("formats multiline call arguments with trailing commas", () => { + const source = `let $value = $arg0 +in $write( + 0xd66362fd3da0170e26e474de2ee0a2f6edec74aab8bb3f24dd92849da9f0a5e5, + 0xa1903eab, + {0x99c7485e8f6ec45063fad412f5c65b454ba9ce41}, +)`; + + const formatted = formatProgram(source, { + separator: Separator.Space, + spaces: 2, + trailingCommas: true, + width: 80 + }); + + expect(formatted).toBe(`let $value = $arg0 +in $write( + 0xd66362fd3da0170e26e474de2ee0a2f6edec74aab8bb3f24dd92849da9f0a5e5, + 0xa1903eab, + { 0x99c7485e8f6ec45063fad412f5c65b454ba9ce41 }, +)`); + }); + + it("respects indentation and trailing comma settings", () => { + const source = `let $value = $arg0 +in $write( + 0xd66362fd3da0170e26e474de2ee0a2f6edec74aab8bb3f24dd92849da9f0a5e5, + 0xa1903eab, + {0x99c7485e8f6ec45063fad412f5c65b454ba9ce41}, +)`; + + const formatted = formatProgram(source, { + separator: Separator.Space, + spaces: 4, + trailingCommas: false, + width: 80 + }); + + expect(formatted).toBe(`let $value = $arg0 +in $write( + 0xd66362fd3da0170e26e474de2ee0a2f6edec74aab8bb3f24dd92849da9f0a5e5, + 0xa1903eab, + { 0x99c7485e8f6ec45063fad412f5c65b454ba9ce41 } +)`); + }); +}); diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..741c9fd --- /dev/null +++ b/src/format.ts @@ -0,0 +1,951 @@ +import { type LowLevelAST, toLowLevelAST } from "./lowLevel"; +import { parser } from "./parser/λs"; + +export const Separator = { + Space: " " +} as const; + +export type Separator = (typeof Separator)[keyof typeof Separator]; + +export type FormatSettings = { + separator: Separator; + spaces: number; + trailingCommas: boolean; + width: number; +}; + +export const defaultFormatSettings: FormatSettings = { + separator: Separator.Space, + spaces: 2, + trailingCommas: false, + width: 80 +}; + +type FormatOptions = Partial; + +type Doc = + | { kind: "text"; text: string } + | { kind: "concat"; parts: Doc[] } + | { kind: "group"; doc: Doc } + | { kind: "nest"; spaces: number; doc: Doc } + | { kind: "line"; mode: "regular" | "soft" | "hard" } + | { kind: "ifBreak"; broken: Doc; flat: Doc }; + +type RenderMode = "flat" | "break"; + +type RenderCommand = { + indent: number; + mode: RenderMode; + doc: Doc; +}; + +const emptyDoc = (): Doc => ({ kind: "concat", parts: [] }); + +const text = (value: string): Doc => + value.length === 0 ? emptyDoc() : { kind: "text", text: value }; + +const concatDocs = (parts: Doc[]): Doc => { + const flat: Doc[] = []; + for (const part of parts) { + if (part.kind === "concat") { + flat.push(...part.parts); + continue; + } + if (part.kind === "text" && part.text.length === 0) { + continue; + } + flat.push(part); + } + if (flat.length === 0) return emptyDoc(); + if (flat.length === 1) return flat[0]; + return { kind: "concat", parts: flat }; +}; + +const group = (doc: Doc): Doc => ({ kind: "group", doc }); + +const nest = (spaces: number, doc: Doc): Doc => + spaces === 0 ? doc : { kind: "nest", spaces, doc }; + +const lineDoc: Doc = { kind: "line", mode: "regular" }; +const softlineDoc: Doc = { kind: "line", mode: "soft" }; +const hardlineDoc: Doc = { kind: "line", mode: "hard" }; + +const ifBreak = (broken: Doc, flat: Doc = emptyDoc()): Doc => ({ + kind: "ifBreak", + broken, + flat +}); + +const joinDocs = (separator: Doc, docs: Doc[]): Doc => { + if (docs.length === 0) return emptyDoc(); + const parts: Doc[] = [docs[0]]; + for (let i = 1; i < docs.length; i++) { + parts.push(separator, docs[i]); + } + return concatDocs(parts); +}; + +const normalizeSettings = (options: FormatOptions = {}): FormatSettings => ({ + ...defaultFormatSettings, + ...options +}); + +const throwParseError = (value: string | undefined): never => { + if (value && value.length > 0) { + throw new Error(`Parse error: ${value}`); + } + throw new Error("Parse error."); +}; + +const findParseError = (node: LowLevelAST): string | undefined => { + if (node.name === "⚠") return node.value; + for (const child of node.children ?? []) { + const value = findParseError(child); + if (value !== undefined) return value; + } + return undefined; +}; + +const parseSource = (source: string): LowLevelAST => { + const root = toLowLevelAST(source, parser.parse(source)); + const error = findParseError(root); + if (error !== undefined) { + throwParseError(error); + } + return root; +}; + +const needsParens = (parentPrecedence: number, precedence: number): boolean => + precedence < parentPrecedence; + +const wrapParens = (doc: Doc): Doc => concatDocs([text("("), doc, text(")")]); + +const wrapIfNeeded = ( + doc: Doc, + parentPrecedence: number, + precedence: number +): Doc => (needsParens(parentPrecedence, precedence) ? wrapParens(doc) : doc); + +const binaryPrecedence = (operator: string): number => { + switch (operator) { + case "||": + return 10; + case "&&": + return 20; + case "in": + case "isa": + case "=": + case "!=": + case "<": + case "<=": + case ">": + case ">=": + return 30; + case "+": + case "-": + return 40; + case "*": + case "/": + case "%": + return 50; + case "^": + return 60; + default: + return 30; + } +}; + +const precedenceOf = (node: LowLevelAST): number => { + switch (node.name) { + case "FunctionExpression": + case "PairExpression": + return 5; + case "TernaryExpression": + return 8; + case "BinaryExpression": { + const operator = node.children?.[1]?.value ?? ""; + return binaryPrecedence(operator); + } + case "UnaryExpression": + return 70; + case "SpreadExpression": + return 75; + case "InterpolationExpression": + return 80; + case "FieldExpression": + case "SubscriptExpression": + case "CallExpression": + return 90; + default: + return 100; + } +}; + +const formatDelimited = ( + open: string, + close: string, + items: Doc[], + settings: FormatSettings, + options: { bracketSpacing: boolean; trailingCommas?: boolean } +): Doc => { + if (items.length === 0) { + return concatDocs([text(open), text(close)]); + } + const boundary = options.bracketSpacing ? lineDoc : softlineDoc; + const trailingComma = + options.trailingCommas && settings.trailingCommas + ? ifBreak(text(",")) + : emptyDoc(); + return group( + concatDocs([ + text(open), + nest( + settings.spaces, + concatDocs([ + boundary, + joinDocs(concatDocs([text(","), lineDoc]), items), + trailingComma + ]) + ), + boundary, + text(close) + ]) + ); +}; + +const formatWrapped = ( + open: string, + close: string, + content: Doc, + settings: FormatSettings, + mode: "soft" | "regular" +): Doc => { + const boundary = mode === "regular" ? lineDoc : softlineDoc; + return group( + concatDocs([ + text(open), + nest(settings.spaces, concatDocs([boundary, content])), + boundary, + text(close) + ]) + ); +}; + +const formatChildren = ( + node: LowLevelAST, + settings: FormatSettings, + parentPrecedence = 0 +): Doc[] => + (node.children ?? []).map((child) => + formatExpressionNode(child, settings, parentPrecedence) + ); + +const formatBinding = (node: LowLevelAST, settings: FormatSettings): Doc => { + const [nameNode, exprNode] = node.children ?? []; + if (!nameNode || !exprNode) { + throw new Error("Invalid binding"); + } + return group( + concatDocs([ + text(nameNode.value ?? ""), + text(" = "), + formatExpressionNode(exprNode, settings) + ]) + ); +}; + +const formatNamedField = (node: LowLevelAST, settings: FormatSettings): Doc => { + const [nameNode, valueNode] = node.children ?? []; + if (!nameNode || !valueNode) { + throw new Error("Invalid named field"); + } + return group( + concatDocs([ + text(nameNode.value ?? ""), + text(": "), + formatExpressionNode(valueNode, settings) + ]) + ); +}; + +const formatNamedArgument = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + const [field] = node.children ?? []; + if (!field) { + throw new Error("Invalid named argument"); + } + return formatNamedField(field, settings); +}; + +const formatArgumentList = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + const children = node.children ?? []; + if (children.length === 0) { + return concatDocs([text("("), text(")")]); + } + const positional = children.filter((child) => child.name !== "NamedArgument"); + const named = children.filter((child) => child.name === "NamedArgument"); + const positionalDoc = joinDocs( + concatDocs([text(","), lineDoc]), + positional.map((child) => formatExpressionNode(child, settings)) + ); + const namedDoc = joinDocs( + concatDocs([text(","), lineDoc]), + named.map((child) => formatNamedArgument(child, settings)) + ); + let content = emptyDoc(); + if (positional.length > 0) { + content = positionalDoc; + } + if (named.length > 0) { + content = + positional.length > 0 + ? concatDocs([content, text(";"), lineDoc, namedDoc]) + : concatDocs([text(";"), lineDoc, namedDoc]); + } + return group( + concatDocs([ + text("("), + nest( + settings.spaces, + concatDocs([ + softlineDoc, + content, + settings.trailingCommas ? ifBreak(text(",")) : emptyDoc() + ]) + ), + softlineDoc, + text(")") + ]) + ); +}; + +const formatFunctionParameters = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + if (node.name === "Identifier") { + return text(node.value ?? ""); + } + if (node.name !== "ArgumentList") { + throw new Error(`Unsupported function parameter list: ${node.name}`); + } + const items = (node.children ?? []).map((child) => + formatExpressionNode(child, settings) + ); + return formatDelimited("(", ")", items, settings, { + bracketSpacing: false + }); +}; + +const formatComprehensionClause = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => + joinDocs( + lineDoc, + (node.children ?? []).map((child) => formatExpressionNode(child, settings)) + ); + +const formatExpressionNode = ( + node: LowLevelAST, + settings: FormatSettings, + parentPrecedence = 0 +): Doc => { + switch (node.name) { + case "Identifier": + case "BooleanValue": + case "Character": + case "NumberValue": + case "StringValue": + case "TripleString": + case "CommandString": + return text(node.value ?? ""); + + case "SymbolValue": { + const [identifier] = node.children ?? []; + return concatDocs([text(":"), text(identifier?.value ?? "")]); + } + + case "PrefixedString": { + const [prefix, value] = node.children ?? []; + return concatDocs([text(prefix?.value ?? ""), text(value?.value ?? "")]); + } + + case "InterpolationExpression": { + const [valueNode] = node.children ?? []; + if (!valueNode) throw new Error("Invalid interpolation expression"); + if (valueNode.name === "Identifier") { + return concatDocs([text("#"), text(valueNode.value ?? "")]); + } + return concatDocs([ + text("#("), + formatExpressionNode(valueNode, settings), + text(")") + ]); + } + + case "ParenthesizedExpression": { + const [content] = node.children ?? []; + if (!content) { + return concatDocs([text("("), text(")")]); + } + return formatWrapped( + "(", + ")", + formatExpressionNode(content, settings), + settings, + "soft" + ); + } + + case "TupleExpression": + return formatDelimited( + "{", + "}", + formatChildren(node, settings), + settings, + { bracketSpacing: true, trailingCommas: true } + ); + + case "ArrayExpression": + return formatDelimited( + "[", + "]", + formatChildren(node, settings), + settings, + { bracketSpacing: false, trailingCommas: true } + ); + + case "ObjectExpression": + return formatDelimited( + "{", + "}", + (node.children ?? []).map((child) => formatNamedField(child, settings)), + settings, + { bracketSpacing: true, trailingCommas: true } + ); + + case "NamedField": + return formatNamedField(node, settings); + + case "CallExpression": { + const [callee, args] = node.children ?? []; + if (!callee || !args) throw new Error("Invalid call expression"); + const doc = concatDocs([ + formatExpressionNode(callee, settings, precedenceOf(node)), + formatArgumentList(args, settings) + ]); + return wrapIfNeeded(doc, parentPrecedence, precedenceOf(node)); + } + + case "FieldExpression": { + const [target, field] = node.children ?? []; + if (!target || !field) throw new Error("Invalid field expression"); + const doc = concatDocs([ + formatExpressionNode(target, settings, precedenceOf(node)), + text("."), + field.name === "Identifier" + ? text(field.value ?? "") + : formatExpressionNode(field, settings) + ]); + return wrapIfNeeded(doc, parentPrecedence, precedenceOf(node)); + } + + case "SubscriptExpression": { + const [target, ...indexes] = node.children ?? []; + if (!target) throw new Error("Invalid subscript expression"); + const indexDoc = formatDelimited( + "[", + "]", + indexes.map((child) => formatExpressionNode(child, settings)), + settings, + { bracketSpacing: false, trailingCommas: true } + ); + const doc = concatDocs([ + formatExpressionNode(target, settings, precedenceOf(node)), + indexDoc + ]); + return wrapIfNeeded(doc, parentPrecedence, precedenceOf(node)); + } + + case "UnaryExpression": { + const [left, right] = node.children ?? []; + if (!left || !right) throw new Error("Invalid unary expression"); + const precedence = precedenceOf(node); + if (left.value !== undefined && left.name.includes("Operator")) { + const doc = concatDocs([ + text(left.value), + formatExpressionNode(right, settings, precedence) + ]); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + const doc = concatDocs([ + formatExpressionNode(left, settings, precedence), + text(right.value ?? "") + ]); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "BinaryExpression": { + const [left, operator, right] = node.children ?? []; + if (!left || !operator || !right) { + throw new Error("Invalid binary expression"); + } + const precedence = binaryPrecedence(operator.value ?? ""); + const doc = group( + concatDocs([ + formatExpressionNode(left, settings, precedence), + text(` ${operator.value ?? ""}`), + nest( + settings.spaces, + concatDocs([ + lineDoc, + formatExpressionNode(right, settings, precedence + 1) + ]) + ) + ]) + ); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "TernaryExpression": { + const [condition, thenNode, elseNode] = node.children ?? []; + if (!condition || !thenNode || !elseNode) { + throw new Error("Invalid ternary expression"); + } + const precedence = precedenceOf(node); + const doc = group( + concatDocs([ + formatExpressionNode(condition, settings, precedence), + text(" ?"), + nest( + settings.spaces, + concatDocs([ + lineDoc, + formatExpressionNode(thenNode, settings, precedence), + text(" :"), + lineDoc, + formatExpressionNode(elseNode, settings, precedence) + ]) + ) + ]) + ); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "FunctionExpression": { + const [parameters, body] = node.children ?? []; + if (!parameters || !body) throw new Error("Invalid function expression"); + const precedence = precedenceOf(node); + const doc = group( + concatDocs([ + formatFunctionParameters(parameters, settings), + text(" =>"), + nest( + settings.spaces, + concatDocs([ + lineDoc, + formatExpressionNode(body, settings, precedence) + ]) + ) + ]) + ); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "PairExpression": { + const [left, right] = node.children ?? []; + if (!left || !right) throw new Error("Invalid pair expression"); + const precedence = precedenceOf(node); + const doc = group( + concatDocs([ + formatExpressionNode(left, settings, precedence), + text(" =>"), + nest( + settings.spaces, + concatDocs([ + lineDoc, + formatExpressionNode(right, settings, precedence) + ]) + ) + ]) + ); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "SpreadExpression": { + const [valueNode] = node.children ?? []; + if (!valueNode) throw new Error("Invalid spread expression"); + const precedence = precedenceOf(node); + const doc = concatDocs([ + formatExpressionNode(valueNode, settings, precedence), + text("...") + ]); + return wrapIfNeeded(doc, parentPrecedence, precedence); + } + + case "RangeExpression": { + const [start, end] = node.children ?? []; + if (!start || !end) throw new Error("Invalid range expression"); + const doc = group( + concatDocs([ + formatExpressionNode(start, settings, precedenceOf(node)), + text("::"), + nest( + settings.spaces, + concatDocs([ + lineDoc, + formatExpressionNode(end, settings, precedenceOf(node) + 1) + ]) + ) + ]) + ); + return wrapIfNeeded(doc, parentPrecedence, precedenceOf(node)); + } + + case "ArrayComprehensionExpression": { + const [valueNode, ...clauses] = node.children ?? []; + if (!valueNode) throw new Error("Invalid array comprehension"); + return formatWrapped( + "[", + "]", + concatDocs([ + formatExpressionNode(valueNode, settings), + text(" "), + formatComprehensionClause( + { + name: "comprehensionClause", + children: clauses, + from: node.from, + to: node.to + }, + settings + ) + ]), + settings, + "soft" + ); + } + + case "GeneratorExpression": { + const [valueNode, ...clauses] = node.children ?? []; + if (!valueNode) throw new Error("Invalid generator expression"); + return formatWrapped( + "(", + ")", + concatDocs([ + formatExpressionNode(valueNode, settings), + text(" "), + formatComprehensionClause( + { + name: "comprehensionClause", + children: clauses, + from: node.from, + to: node.to + }, + settings + ) + ]), + settings, + "soft" + ); + } + + case "ForClause": { + const bindings = (node.children ?? []).filter( + (child) => child.name === "ForBinding" + ); + return group( + concatDocs([ + text("for "), + joinDocs( + concatDocs([text(","), lineDoc]), + bindings.map((child) => formatExpressionNode(child, settings)) + ) + ]) + ); + } + + case "ForBinding": { + const [identifier, operator, valueNode] = node.children ?? []; + if (!identifier || !operator || !valueNode) { + throw new Error("Invalid for binding"); + } + return group( + concatDocs([ + text(identifier.value ?? ""), + text(` ${operator.value ?? ""} `), + formatExpressionNode(valueNode, settings) + ]) + ); + } + + case "IfClause": { + const [, condition] = node.children ?? []; + if (!condition) throw new Error("Invalid if clause"); + return group( + concatDocs([text("if "), formatExpressionNode(condition, settings)]) + ); + } + + case "SourceFile": + return formatSourceFile(node, settings); + + case "Program": + case "ProgramComponent": + case "LetComponent": + case "TopLevelDefinition": + case "ComponentBinding": + return formatProgramNode(node, settings); + + default: + throw new Error(`Unsupported formatter node: ${node.name}`); + } +}; + +const formatProgramComponent = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + const [inner] = node.children ?? []; + if (!inner) return emptyDoc(); + return formatProgramNode(inner, settings); +}; + +const formatLetComponent = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + const children = node.children ?? []; + const bindings: LowLevelAST[] = []; + let resultNode: LowLevelAST | undefined; + for (const child of children) { + if (child.name === "ComponentBinding") { + bindings.push(child); + } + if (child.name === "in") { + resultNode = children[children.indexOf(child) + 1]; + break; + } + } + if (bindings.length === 0 || !resultNode) { + throw new Error("Invalid let component"); + } + const [firstBinding, ...restBindings] = bindings; + return group( + concatDocs([ + text("let "), + formatBinding(firstBinding, settings), + ...restBindings.flatMap((binding) => [ + lineDoc, + text("and "), + formatBinding(binding, settings) + ]), + lineDoc, + text("in "), + formatExpressionNode(resultNode, settings) + ]) + ); +}; + +const formatProgramNode = ( + node: LowLevelAST, + settings: FormatSettings +): Doc => { + switch (node.name) { + case "Program": + return joinDocs( + hardlineDoc, + (node.children ?? []).map((child) => + formatProgramComponent(child, settings) + ) + ); + case "ProgramComponent": + return formatProgramComponent(node, settings); + case "LetComponent": + return formatLetComponent(node, settings); + case "TopLevelDefinition": { + const [binding] = node.children ?? []; + if (!binding) throw new Error("Invalid top level definition"); + return formatBinding(binding, settings); + } + case "ComponentBinding": + return formatBinding(node, settings); + default: + return formatExpressionNode(node, settings); + } +}; + +const formatSourceFile = (node: LowLevelAST, settings: FormatSettings): Doc => { + const children = node.children ?? []; + if (children.length === 0) return emptyDoc(); + if (children.length === 1) { + const [onlyChild] = children; + if (!onlyChild) return emptyDoc(); + if (onlyChild.name === "Program") { + return formatProgramNode(onlyChild, settings); + } + return formatExpressionNode(onlyChild, settings); + } + return joinDocs( + hardlineDoc, + children.map((child) => + child.name === "Program" + ? formatProgramNode(child, settings) + : formatExpressionNode(child, settings) + ) + ); +}; + +const fits = ( + remainingWidth: number, + commands: RenderCommand[], + settings: FormatSettings +): boolean => { + const queue = [...commands]; + while (remainingWidth >= 0 && queue.length > 0) { + const next = queue.pop(); + if (!next) break; + const { indent, mode, doc } = next; + switch (doc.kind) { + case "text": + remainingWidth -= doc.text.length; + break; + case "concat": + for (let i = doc.parts.length - 1; i >= 0; i--) { + queue.push({ indent, mode, doc: doc.parts[i] }); + } + break; + case "group": + queue.push({ indent, mode: "flat", doc: doc.doc }); + break; + case "nest": + queue.push({ indent: indent + doc.spaces, mode, doc: doc.doc }); + break; + case "line": + if (doc.mode === "hard") { + return mode === "break"; + } + if (mode === "break") { + return true; + } + if (doc.mode === "regular") { + remainingWidth -= settings.separator.length; + } + break; + case "ifBreak": + queue.push({ + indent, + mode, + doc: mode === "break" ? doc.broken : doc.flat + }); + break; + } + } + return remainingWidth >= 0; +}; + +const renderDoc = (doc: Doc, settings: FormatSettings): string => { + const output: string[] = []; + const stack: RenderCommand[] = [{ indent: 0, mode: "break", doc }]; + let column = 0; + while (stack.length > 0) { + const command = stack.pop(); + if (!command) break; + const { indent, mode, doc: current } = command; + switch (current.kind) { + case "text": + output.push(current.text); + column += current.text.length; + break; + case "concat": + for (let i = current.parts.length - 1; i >= 0; i--) { + stack.push({ indent, mode, doc: current.parts[i] }); + } + break; + case "group": { + const flatCommand: RenderCommand = { + indent, + mode: "flat", + doc: current.doc + }; + if (fits(settings.width - column, [...stack, flatCommand], settings)) { + stack.push(flatCommand); + } else { + stack.push({ indent, mode: "break", doc: current.doc }); + } + break; + } + case "nest": + stack.push({ + indent: indent + current.spaces, + mode, + doc: current.doc + }); + break; + case "line": + if (current.mode === "hard" || mode === "break") { + output.push(`\n${" ".repeat(indent)}`); + column = indent; + break; + } + if (current.mode === "regular") { + output.push(settings.separator); + column += settings.separator.length; + } + break; + case "ifBreak": + stack.push({ + indent, + mode, + doc: mode === "break" ? current.broken : current.flat + }); + break; + } + } + return output.join(""); +}; + +export const formatSource = ( + source: string, + options: FormatOptions = {} +): string => { + const settings = normalizeSettings(options); + const root = parseSource(source); + return renderDoc(formatSourceFile(root, settings), settings); +}; + +export const formatExpression = ( + source: string, + options: FormatOptions = {} +): string => { + const settings = normalizeSettings(options); + const root = parseSource(source); + const child = root.children?.[0]; + if ( + !child || + child.name === "Program" || + (root.children?.length ?? 0) !== 1 + ) { + throw new Error("Expected expression source"); + } + return renderDoc(formatExpressionNode(child, settings), settings); +}; + +export const formatProgram = ( + source: string, + options: FormatOptions = {} +): string => { + const settings = normalizeSettings(options); + const root = parseSource(source); + const programNode = root.children?.find((child) => child.name === "Program"); + if (!programNode) { + throw new Error("Expected program definition"); + } + return renderDoc(formatProgramNode(programNode, settings), settings); +}; diff --git a/src/highLevel.test.ts b/src/highLevel.test.ts index fec73d2..f8f2416 100644 --- a/src/highLevel.test.ts +++ b/src/highLevel.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it, test } from "vitest"; +import { beforeAll, describe, expect, it } from "bun:test"; import { type ASTNode, @@ -11,12 +11,12 @@ import { NameTuple, NameVariable } from "./ast"; +import { toEqualWithRationals } from "./extend.test"; import { toHighLevelAST } from "./highLevel"; import { toLowLevelAST } from "./lowLevel"; import { parser } from "./parser/λs"; import { Rational } from "./rational"; -import { toEqualWithRationals } from "./extend.test"; beforeAll(() => { toEqualWithRationals(); }); @@ -282,15 +282,12 @@ const highLevelData: [string, ASTNode][] = [ ] } ], - // Test for a dual element tuple with a trailing comma (x,) + // Test for a singleton tuple with a trailing comma (x,) [ - "{x, y, }", + "{x,}", { type: NameTuple, - elements: [ - { type: NameVariable, name: "x" }, - { type: NameVariable, name: "y" } - ] + elements: [{ type: NameVariable, name: "x" }] } ], // Test for a tuple with an expression (x + 1, 2) @@ -343,7 +340,7 @@ describe("high-level parsing", () => { const tree = parser.parse(expr); const low = toLowLevelAST(expr, tree); const high = toHighLevelAST(low); - expect(high).toEqual(ast); + expect(high).toEqualRational(ast); }); } }); diff --git a/src/highLevel.ts b/src/highLevel.ts index 9ee81ca..6a2ea0c 100644 --- a/src/highLevel.ts +++ b/src/highLevel.ts @@ -9,7 +9,8 @@ import { NameObject, NameTuple, NameVariable, - type RawConstant + type RawConstant, + type SourceRange } from "./ast"; import type { LowLevelAST } from "./lowLevel"; import { Rational } from "./rational"; @@ -23,64 +24,93 @@ export type ParserExtension = { type: TypeConst; }; +const rangeFor = (node?: LowLevelAST): SourceRange | undefined => + node ? { from: node.from, to: node.to } : undefined; + +const throwRangedError = (message: string, node?: LowLevelAST): never => { + const error = new Error(message) as Error & { range?: SourceRange }; + if (node) { + error.range = rangeFor(node); + } + throw error; +}; + export function toHighLevelAST( parseTree: LowLevelAST, extensions: ParserExtension[] = [] ): ASTNode { - if (!parseTree || !parseTree.name) { - throw new Error("Invalid parse tree"); + if (!parseTree?.name) { + return throwRangedError("Invalid parse tree", parseTree); } // @check Extensions - for (const ext of extensions) - if (parseTree.name === ext.elt && ext.pat(parseTree.value)) + for (const ext of extensions) { + if ( + parseTree.name === ext.elt && + typeof parseTree.value === "string" && + ext.pat(parseTree.value) + ) return { type: NameConstant, - value: ext.rewrite(parseTree.value) as RawConstant + value: ext.rewrite(parseTree.value) as RawConstant, + range: rangeFor(parseTree) }; + } switch (parseTree.name) { case "SourceFile": if (parseTree.children?.length === 1) return toHighLevelAST(parseTree.children[0], extensions); - throw new Error("empty source"); + return throwRangedError("empty source", parseTree); case "ParenthesizedExpression": if (parseTree.children?.length === 1) return toHighLevelAST(parseTree.children[0], extensions); - throw new Error("empty parenthesis"); + return throwRangedError("empty parenthesis", parseTree); case "TupleExpression": { const elements = parseTree.children?.map((c) => toHighLevelAST(c, extensions)) || []; if (elements.length === 0) { - throw new Error("Empty tuples are not allowed"); + throwRangedError("Empty tuples are not allowed", parseTree); } return { type: NameTuple, - elements + elements, + range: rangeFor(parseTree) }; } case "Identifier": - return { type: NameVariable, name: parseTree.value as string }; + return { + type: NameVariable, + name: parseTree.value as string, + range: rangeFor(parseTree) + }; case "NumberValue": return { type: NameConstant, - value: new Rational(parseTree.value as string | number | bigint) + value: new Rational(parseTree.value as string | number | bigint), + range: rangeFor(parseTree) }; case "StringValue": return { type: NameConstant, - value: JSON.parse(parseTree.value as string) + value: JSON.parse(parseTree.value as string), + range: rangeFor(parseTree) }; //.slice(1, -1) }; case "BooleanValue": - return { type: NameConstant, value: parseTree.value === "true" }; + return { + type: NameConstant, + value: parseTree.value === "true", + range: rangeFor(parseTree) + }; case "ArrayExpression": return { type: NameList, elements: - parseTree.children?.map((c) => toHighLevelAST(c, extensions)) || [] + parseTree.children?.map((c) => toHighLevelAST(c, extensions)) || [], + range: rangeFor(parseTree) }; case "ObjectExpression": @@ -90,129 +120,161 @@ export function toHighLevelAST( parseTree.children ?.map((c) => toHighLevelAST(c, extensions)) ?.map((node) => - // @ts-ignore @todo check node is temporary "named" + // @ts-expect-error @todo check node is temporary "named" [node.key, node.value] ) || [] - ) + ), + range: rangeFor(parseTree) }; case "NamedField": if (parseTree.children?.length === 2) return { type: "named", key: parseTree.children[0].value as string, - value: toHighLevelAST(parseTree.children[1], extensions) + value: toHighLevelAST(parseTree.children[1], extensions), + range: rangeFor(parseTree) }; - throw new Error("wrong NamedField length"); + return throwRangedError("wrong NamedField length", parseTree); case "FieldExpression": - case "SubscriptExpression": - if (parseTree.children?.length !== 2) - throw new Error("wrong FieldExpression length"); + case "SubscriptExpression": { + const children = parseTree.children; + if (children?.length !== 2) + return throwRangedError("wrong FieldExpression length", parseTree); // @todo support field expressions - if (parseTree.children[1].name === "Identifier") + if (children[1].name === "Identifier") return { type: NameField, - expr: toHighLevelAST(parseTree.children[0], extensions), - field: parseTree.children[1].value as string + expr: toHighLevelAST(children[0], extensions), + field: children[1].value as string, + range: rangeFor(parseTree) }; return { type: NameField, - expr: toHighLevelAST(parseTree.children[0], extensions), - sub: toHighLevelAST(parseTree.children[1], extensions) + expr: toHighLevelAST(children[0], extensions), + sub: toHighLevelAST(children[1], extensions), + range: rangeFor(parseTree) }; + } case "FunctionExpression": { - if (parseTree.children?.length !== 2) - throw new Error("wrong function length"); + const children = parseTree.children; + if (children?.length !== 2) + return throwRangedError("wrong function length", parseTree); let parameters: LowLevelAST[]; - if (parseTree.children[0].name === "ArgumentList") { - parameters = parseTree.children[0].children ?? []; + if (children[0].name === "ArgumentList") { + parameters = children[0].children ?? []; } else { - parameters = [parseTree.children[0]]; + parameters = [children[0]]; } - if (!parameters.length) throw new Error("Function with no arguments"); - let body = toHighLevelAST(parseTree.children[1], extensions); + if (!parameters.length) + return throwRangedError("Function with no arguments", parseTree); + let body = toHighLevelAST(children[1], extensions); for (const parameter of parameters.reverse()) { body = { type: NameLambda, parameter: parameter.value as string, - body + body, + range: rangeFor(parseTree) }; } return body; } case "CallExpression": { - if (parseTree.children?.length !== 2) - throw new Error("wrong CallExpression length"); - if (parseTree.children[1].name !== "ArgumentList") - throw new Error("wrong CallExpression arguments"); + const children = parseTree.children; + if (children?.length !== 2) + return throwRangedError("wrong CallExpression length", parseTree); + if (children[1].name !== "ArgumentList") + return throwRangedError("wrong CallExpression arguments", children[1]); const params = - parseTree.children[1].children?.map((c) => - toHighLevelAST(c, extensions) - ) || []; - switch (parseTree.children[0].name) { + children[1].children?.map((c) => toHighLevelAST(c, extensions)) || []; + switch (children[0].name) { case "Identifier": return { type: NameApplication, function: { type: NameVariable, - name: parseTree.children[0].value as string + name: children[0].value as string, + range: rangeFor(children[0]) }, - params + params, + range: rangeFor(parseTree) }; case "ParenthesizedExpression": case "CallExpression": return { type: NameApplication, - function: toHighLevelAST(parseTree.children[0], extensions), - params + function: toHighLevelAST(children[0], extensions), + params, + range: rangeFor(parseTree) }; } - throw new Error("unknown CallExpression"); + return throwRangedError("unknown CallExpression", parseTree); } - case "UnaryExpression": - if (parseTree.children?.length !== 2) - throw new Error("wrong UnaryExpression length"); - if (!parseTree.children[0].name.includes("Operator")) - throw new Error("not an unary operator"); + case "UnaryExpression": { + const children = parseTree.children; + if (children?.length !== 2) + return throwRangedError("wrong UnaryExpression length", parseTree); + if (!children[0].name.includes("Operator")) + return throwRangedError("not an unary operator", children[0]); return { type: NameApplication, function: { type: NameVariable, - name: parseTree.children[0].value as string + name: children[0].value as string, + range: rangeFor(children[0]) }, - params: [toHighLevelAST(parseTree.children[1], extensions)] + params: [toHighLevelAST(children[1], extensions)], + range: rangeFor(parseTree) }; + } - case "BinaryExpression": - if (parseTree.children?.length !== 3) - throw new Error("wrong BinaryExpression length"); + case "BinaryExpression": { + const children = parseTree.children; + if (children?.length !== 3) + return throwRangedError("wrong BinaryExpression length", parseTree); return { type: NameApplication, function: { type: NameVariable, - name: parseTree.children[1].value as string + name: children[1].value as string, + range: rangeFor(children[1]) }, params: [ - toHighLevelAST(parseTree.children[0], extensions), - toHighLevelAST(parseTree.children[2], extensions) - ] + toHighLevelAST(children[0], extensions), + toHighLevelAST(children[2], extensions) + ], + range: rangeFor(parseTree) }; + } case "TernaryExpression": { - if (parseTree.children?.length !== 3) - throw new Error("wrong TernaryExpression length"); + const children = parseTree.children; + if (children?.length !== 3) + return throwRangedError("wrong TernaryExpression length", parseTree); return { type: NameApplication, - function: { type: NameVariable, name: "IF" }, - params: parseTree.children.map((c) => toHighLevelAST(c, extensions)) + function: { + type: NameVariable, + name: "IF", + range: rangeFor(parseTree) + }, + params: children.map((c) => toHighLevelAST(c, extensions)), + range: rangeFor(parseTree) }; } case "⚠": - return { type: NameError, value: parseTree.value as string }; + return { + type: NameError, + value: parseTree.value as string, + range: rangeFor(parseTree) + }; default: - throw new Error(`Unknown parse tree node: ${parseTree.name}`); + return throwRangedError( + `Unknown parse tree node: ${parseTree.name}`, + parseTree + ); } } diff --git a/src/index.ts b/src/index.ts index faecc26..4ed51d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,14 @@ // utils -export { length, plural } from "./utils"; export { + type ApplicationNode, + type ASTNode, + apply, + applyName, + type ConstantNode, + type FieldNode, + type LambdaNode, + type ListNode, NameApplication, NameConstant, NameError, @@ -9,9 +16,9 @@ export { NameLambda, NameList, NameObject, + Names, NameTuple, NameVariable, - Names, newConstant, newFromJS, newLambda, @@ -19,56 +26,76 @@ export { newList, newNumber, newObject, + newTuple, newVariable, - type ASTNode, - type ApplicationNode, - type ConstantNode, - type FieldNode, - type LambdaNode, - type ListNode, type ObjectNode, + type SourceRange, + type TupleNode, type VariableNode } from "./ast"; -export { newTypeScheme, type TypeScheme } from "./typeScheme"; +// stdlib, environment and program export { - newTypeConst, - newTypeObject, - newTypeTuple, - newTypeVar, - typeAny, - typeFunction, - typeList, - type MonoType, - type TypeAny, - type TypeConst -} from "./types"; - + Environment, + type EnvironmentOptions, + type TypeMap, + type ValueDefinition, + type ValueMap +} from "./env"; // parse and print export { allDiff, firstDiff, isEqual } from "./equal"; -export { toHighLevelAST, type ParserExtension } from "./highLevel"; +export type { Value } from "./eval"; +export { + defaultFormatSettings, + type FormatSettings, + formatExpression, + formatProgram, + formatSource, + Separator +} from "./format"; +export { type ParserExtension, toHighLevelAST } from "./highLevel"; export { toLowLevelAST } from "./lowLevel"; -export { parseExpression, reservedKeywords } from "./parse"; +export { + type ParsedProgram, + type ProgramBinding, + type ProgramComponent, + parseExpression, + parseProgram, + reservedKeywords +} from "./parse"; export { parser } from "./parser/λs"; export { prettyPrint, prettyPrintType } from "./print"; - -// stdlib, environment and program -export { Environment, type ValueDefinition } from "./env"; -export { EvalOption, Program } from "./program"; +export { type Binding, EvalOption, Program } from "./program"; export { - Rational, + isRational, isRationalString, + Rational, rationalNumberRegex, zero } from "./rational"; +export { evaluateASTSingle } from "./singleEval"; export { defaultLibrary, + type FirstClassValue, + type LibraryElement, rationalNumber, + type StandardLibrary, typeBoolean, typeBytes, typeFromJSValue, typeNumber, - typeString, - type FirstClassValue, - type LibraryElement, - type StandardLibrary + typeString } from "./stdlib"; +export { newTypeScheme, type TypeScheme } from "./typeScheme"; +export { + type MonoType, + newTypeConst, + newTypeObject, + newTypeTuple, + newTypeVar, + type TypeAny, + type TypeConst, + typeAny, + typeFunction, + typeList +} from "./types"; +export { length, plural } from "./utils"; diff --git a/src/infer.ts b/src/infer.ts index e078b51..28ae540 100644 --- a/src/infer.ts +++ b/src/infer.ts @@ -8,7 +8,8 @@ import { NameList, NameObject, NameTuple, - NameVariable + NameVariable, + type SourceRange } from "./ast"; import type { Environment } from "./env"; import type { ParserExtension } from "./highLevel"; @@ -22,6 +23,14 @@ import { import { type MonoType, newTypeConst, newTypeVar } from "./types"; import { TypeSubstitution } from "./unify"; +const throwNodeError = (message: string, node?: ASTNode): never => { + const error = new Error(message) as Error & { range?: SourceRange }; + if (node?.range) { + error.range = node.range; + } + throw error; +}; + export const detectConstantType = ( v: boolean | string | Rational, extensions: ParserExtension[] = [] @@ -58,7 +67,7 @@ export const inferType = } const typeScheme = env.type(node.name); if (!typeScheme) { - throw new Error(`Unbound variable: ${node.name}`); + return throwNodeError(`Unbound variable: ${node.name}`, node); } return Instantiate(typeScheme); } @@ -119,26 +128,29 @@ export const inferType = if ( !(exprType.kind === NameObject && !isNumber) && !(exprType.kind === NameList && isNumber) && + !(exprType.kind === NameTuple && isNumber) && // not yet resolved exprType.kind !== NameVariable ) { - throw new Error( + throwNodeError( `In ${prettyPrint(node)}: Expected ${ isNumber ? "a list" : "an object" - } or variable, got ${exprType.kind}` + } or variable, got ${exprType.kind}`, + node ); } if (typeof fieldType !== "string" && !isNumber) { - throw new Error( + throwNodeError( `In ${prettyPrint(node)}: Expected a field ${ isNumber ? "index" : "name" - }, got ${prettyPrintType(fieldType)}` + }, got ${prettyPrintType(fieldType)}`, + node ); } if (exprType.kind === NameVariable) { if (!isNumber && !("field" in node)) - throw new Error(`No field in ${prettyPrint(node)}`); + throwNodeError(`No field in ${prettyPrint(node)}`, node); // console.log({ // step: "first time", // field: node.field, @@ -164,8 +176,22 @@ export const inferType = return subst._apply(parameterType); } if (isNumber) { - // @ts-expect-error exprType.kind === NameList - return subst._apply(exprType.elementType); + if (exprType.kind === NameList) + return subst._apply(exprType.elementType); + if (exprType.kind === NameTuple) { + const subNode = "sub" in node ? node.sub : null; + if ( + !subNode || + !("value" in subNode) || + !(subNode.value instanceof Rational) + ) + return throwNodeError("tuple index must be a number", node); + // console.log({ fieldType, sub:node.sub.value }); + return subst._apply( + exprType.elementTypes[subNode.value.toNumber()] + ); + } + throwNodeError("unkown type", node); } // @ts-expect-error exprType.kind === NameObject if (!(fieldType in exprType.fields)) { @@ -173,8 +199,9 @@ export const inferType = // console.log({ exprType: JSON.stringify(exprType) }); // @ts-expect-error exprType.kind === NameObject if (!exprType.open) - throw new Error( - `In ${prettyPrint(node)}: Unknown field: ${fieldType}` + throwNodeError( + `In ${prettyPrint(node)}: Unknown field: ${fieldType}`, + node ); // Assign a new type variable to the unknown field // FIXME: HERE update the field and propagate @@ -214,7 +241,10 @@ export const inferType = } default: - throw new Error(`Unknown node type: ${prettyPrint(node)}`); + return throwNodeError( + `Unknown node type: ${prettyPrint(node)}`, + node + ); } }; @@ -227,10 +257,11 @@ export const inferType = if (substitutedResult !== result) { // If this happens, it means a subst._apply call may have been forgotten // or we need a better unification algorithm - throw new Error( + throwNodeError( `In ${prettyPrint( node - )}: Non-substituted free variable in ${prettyPrintType(result)}` + )}: Non-substituted free variable in ${prettyPrintType(result)}`, + node ); } diff --git a/src/json.test.ts b/src/json.test.ts index 772a737..9ab1c13 100644 --- a/src/json.test.ts +++ b/src/json.test.ts @@ -1,18 +1,22 @@ -import { beforeAll, expect, test } from "vitest"; - +import { beforeAll, expect, test } from "bun:test"; import { jsonStringify } from "@okcontract/cells"; -import { NameApplication, NameConstant, NameVariable } from "./ast"; +import { + type ASTNode, + NameApplication, + NameConstant, + NameVariable +} from "./ast"; +import { toEqualWithRationals } from "./extend.test"; import { parseNode } from "./json"; import { Rational } from "./rational"; -import { toEqualWithRationals } from "./extend.test"; beforeAll(() => { toEqualWithRationals(); }); test("output is fine", () => { - const v = { + const v: ASTNode = { type: NameApplication, function: { type: NameVariable, name: "+" }, params: [ @@ -34,7 +38,7 @@ test("output is fine", () => { } ] }; - expect(parseNode(v)).toEqual(exp); + expect(parseNode(v)).toEqualRational(exp); }); const values: unknown[] = [ diff --git a/src/json.ts b/src/json.ts index 3187301..981d9f8 100644 --- a/src/json.ts +++ b/src/json.ts @@ -1,6 +1,6 @@ import { - type ASTNode, type ApplicationNode, + type ASTNode, type ConstantNode, type FieldNode, type LambdaNode, @@ -18,7 +18,7 @@ import { type VariableNode } from "./ast"; import type { ParserExtension } from "./highLevel"; -import { Rational, isRationalString } from "./rational"; +import { isRationalString, Rational } from "./rational"; export function parseNode( node: ASTNode, diff --git a/src/lowLevel.test.ts b/src/lowLevel.test.ts index 806428c..3876da8 100644 --- a/src/lowLevel.test.ts +++ b/src/lowLevel.test.ts @@ -1,8 +1,20 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { type LowLevelAST, toLowLevelAST } from "./lowLevel"; import { parser } from "./parser/λs"; +const stripPositions = (node: LowLevelAST): unknown => { + const { from: _from, to: _to, children, ...rest } = node; + return { + ...rest, + ...(children + ? { + children: children.map((child) => stripPositions(child)) + } + : {}) + }; +}; + const lowLevelData: [string, LowLevelAST][] = [ [ "1.+.3", @@ -99,14 +111,14 @@ test("low-level parsing", () => { for (let i = 0; i < lowLevelData.length; i++) { const [expr, ast] = lowLevelData[i]; const tree = parser.parse(expr); - expect(toLowLevelAST(expr, tree)).toEqual(ast); + expect(stripPositions(toLowLevelAST(expr, tree))).toEqual(ast); } }); test("parse error", () => { const expr = "1+"; const tree = parser.parse(expr); - expect(toLowLevelAST(expr, tree)).toEqual({ + expect(stripPositions(toLowLevelAST(expr, tree))).toEqual({ children: [ { children: [ diff --git a/src/lowLevel.ts b/src/lowLevel.ts index 5ee9a11..c5d140c 100644 --- a/src/lowLevel.ts +++ b/src/lowLevel.ts @@ -4,6 +4,8 @@ import type { SyntaxNode, Tree } from "@lezer/common"; export type LowLevelAST = { /** name of Node type */ name: string; + from: number; + to: number; /** value for terminal Nodes */ value?: string; /** children for non-terminal Nodes */ @@ -18,7 +20,7 @@ function isError(name: string): boolean { export function toLowLevelAST(input: string, tree: Tree) { const aux = (node: SyntaxNode): LowLevelAST => { const name = node?.type?.name; - const res: LowLevelAST = { name }; + const res: LowLevelAST = { name, from: node.from, to: node.to }; let child = node.firstChild; if (child) { diff --git a/src/merge.ts b/src/merge.ts index 4fc31e4..f9f6543 100644 --- a/src/merge.ts +++ b/src/merge.ts @@ -4,9 +4,10 @@ export const mergeWithFirstPriority = < >( objects: T[] ): T => { - const result = {} as T; - for (const obj of objects) - for (const [k, v] of Object.entries(obj)) + const result = {} as Record; + for (const obj of objects) { + for (const [k, v] of Object.entries(obj) as [Key, unknown][]) if (result[k] === undefined) result[k] = v; - return result; + } + return result as T; }; diff --git a/src/objectMap.test.ts b/src/objectMap.test.ts index 52301df..03e2c80 100644 --- a/src/objectMap.test.ts +++ b/src/objectMap.test.ts @@ -1,10 +1,10 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { ObjectMap, ObjectMapKey } from "./objectMap"; test("ObjectMap", () => { const m = { a: 1, b: 2, c: 3 }; - const fn = (key: string, v: number) => v + 1; + const fn = (_key: string, v: number) => v + 1; expect(ObjectMap(fn, {})).toEqual({}); expect(ObjectMap(fn, m)).toEqual({ a: 2, b: 3, c: 4 }); }); @@ -18,7 +18,7 @@ test("ObjectMapKey", () => { test("ObjectMapKey with different type", () => { const m = { a: 1, b: 2, c: 3 }; - const fn = (key: string, v: number): [string, string] => [`${key}a`, key]; + const fn = (key: string, _v: number): [string, string] => [`${key}a`, key]; expect(ObjectMapKey(fn, {})).toEqual({}); expect(ObjectMapKey(fn, m)).toEqual({ aa: "a", ba: "b", ca: "c" }); }); diff --git a/src/parse.test.ts b/src/parse.test.ts new file mode 100644 index 0000000..ca172d6 --- /dev/null +++ b/src/parse.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from "bun:test"; + +import { + type ASTNode, + type ConstantNode, + NameConstant, + NameTuple, + NameVariable +} from "./ast"; +import { parseExpression, parseProgram } from "./parse"; +import { Rational } from "./rational"; + +function assertConstantNode( + node: ASTNode +): asserts node is ConstantNode { + if (node.type !== NameConstant) { + throw new Error(`Expected constant node, got ${node.type}`); + } +} + +describe("parseExpression scientific notation", () => { + it("parses positive exponents", async () => { + const ast = await parseExpression("1e5"); + assertConstantNode(ast); + expect(ast.value).toBeInstanceOf(Rational); + expect((ast.value as Rational).toString()).toBe("100000"); + }); + + it("parses decimal mantissa with positive exponent", async () => { + const ast = await parseExpression("1.23e3"); + assertConstantNode(ast); + expect(ast.value).toBeInstanceOf(Rational); + expect((ast.value as Rational).toString()).toBe("1230"); + }); + + it("parses negative exponents", async () => { + const ast = await parseExpression("1.23e-3"); + assertConstantNode(ast); + expect(ast.value).toBeInstanceOf(Rational); + expect((ast.value as Rational).toNumber()).toBeCloseTo(0.00123); + }); + + it("parses leading decimal notation", async () => { + const ast = await parseExpression(".5E2"); + assertConstantNode(ast); + expect(ast.value).toBeInstanceOf(Rational); + expect((ast.value as Rational).toString()).toBe("50"); + }); + + it("parses zero mantissa", async () => { + const ast = await parseExpression("0e5"); + assertConstantNode(ast); + expect(ast.value).toBeInstanceOf(Rational); + expect((ast.value as Rational).toString()).toBe("0"); + }); +}); + +describe("parseProgram", () => { + it("parses top-level definitions", async () => { + const parsed = await parseProgram("foo = 1\nbar = foo + 1"); + expect(parsed.components).toHaveLength(0); + expect(parsed.definitions).toEqual([ + expect.objectContaining({ name: "foo" }), + expect.objectContaining({ name: "bar" }) + ]); + }); + + it("parses let components with results", async () => { + const parsed = await parseProgram(` +let foo = 1 +and bar = foo + 1 +in { foo, bar } +`); + expect(parsed.components).toHaveLength(1); + const component = parsed.components[0]; + expect(component.bindings.map((binding) => binding.name)).toEqual([ + "foo", + "bar" + ]); + expect(component.result.type).toBe(NameTuple); + expect(parsed.definitions).toHaveLength(0); + }); + + it("returns variable result for simple let-in", async () => { + const parsed = await parseProgram(` +let foo = 1 +in foo +`); + expect(parsed.components).toHaveLength(1); + expect(parsed.components[0].result.type).toBe(NameVariable); + }); +}); diff --git a/src/parse.ts b/src/parse.ts index 103a0e3..fd1bf94 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,14 +1,213 @@ -import type { ASTNode } from "./ast"; +import type { ASTNode, SourceRange } from "./ast"; import { type ParserExtension, toHighLevelAST } from "./highLevel"; -import { toLowLevelAST } from "./lowLevel"; +import { type LowLevelAST, toLowLevelAST } from "./lowLevel"; import { parser } from "./parser/λs"; -export const reservedKeywords = ["true", "false", "if", "for", "in"]; +export const reservedKeywords = [ + "true", + "false", + "if", + "for", + "in", + "let", + "and" +]; export type ParseOptions = { ext?: ParserExtension[]; }; +export type ProgramBinding = { + name: string; + expr: ASTNode; + range?: SourceRange; +}; + +export type ProgramComponent = { + bindings: ProgramBinding[]; + result: ASTNode; +}; + +export type ParsedProgram = { + components: ProgramComponent[]; + definitions: ProgramBinding[]; +}; + +const rangeFor = (node?: LowLevelAST): SourceRange | undefined => + node ? { from: node.from, to: node.to } : undefined; + +const throwRangedError = (message: string, node?: LowLevelAST): never => { + const error = new Error(message) as Error & { range?: SourceRange }; + if (node) { + error.range = rangeFor(node); + } + throw error; +}; + +const scientificNotationPattern = + /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))[eE]([+-]?\d+)$/; + +const normalizeScientificNotation = (value: string): string => { + const sanitized = value.replace(/_/g, ""); + const match = sanitized.match(scientificNotationPattern); + if (!match) return value; + + const sign = match[1] === "-" ? "-" : ""; + const integerDigits = match[2] ?? ""; + const fractionDigits = + match[3] && match[3] !== "" ? match[3] : (match[4] ?? ""); + const exponent = Number(match[5]); + + const mantissa = integerDigits + fractionDigits; + const digits = mantissa === "" ? "0" : mantissa; + const decimalIndex = integerDigits.length; + const shiftedIndex = decimalIndex + exponent; + + if (/^0+$/.test(digits)) { + return "0"; + } + + let result: string; + + if (exponent >= 0) { + if (shiftedIndex >= digits.length) { + result = digits + "0".repeat(shiftedIndex - digits.length); + } else { + const whole = digits.slice(0, shiftedIndex) || "0"; + const fraction = digits.slice(shiftedIndex); + result = fraction ? `${whole}.${fraction}` : whole; + } + } else if (shiftedIndex <= 0) { + const zeros = "0".repeat(-shiftedIndex); + result = `0.${zeros}${digits}`; + } else { + const whole = digits.slice(0, shiftedIndex) || "0"; + const fraction = digits.slice(shiftedIndex); + result = `${whole}.${fraction}`; + } + + if (result.includes(".")) { + result = result.replace(/^0+(?=\d)/, ""); + if (result.startsWith(".")) { + result = `0${result}`; + } + } else { + result = result.replace(/^0+(?=\d)/, ""); + } + + const final = sign + result; + return final === "-0" ? "0" : final; +}; + +const normalizeNumbers = (node: LowLevelAST): void => { + if (node.name === "NumberValue" && typeof node.value === "string") { + node.value = normalizeScientificNotation(node.value); + } + node.children?.forEach(normalizeNumbers); +}; + +const toProgramBinding = ( + node: LowLevelAST, + extensions: ParserExtension[] +): ProgramBinding => { + if (node.name !== "ComponentBinding") + throwRangedError(`Unexpected node in binding: ${node.name}`, node); + const [identifier, exprNode] = node.children ?? []; + if (identifier?.name !== "Identifier") + throwRangedError("Program bindings must start with an identifier", node); + if (!exprNode) + throwRangedError( + `Missing expression for program binding "${identifier.value ?? ""}"`, + identifier + ); + return { + name: identifier.value as string, + expr: toHighLevelAST(exprNode, extensions), + range: rangeFor(identifier) + }; +}; + +const toProgramComponent = ( + node: LowLevelAST, + extensions: ParserExtension[] +): ProgramComponent => { + if (node.name !== "LetComponent") + throwRangedError(`Unexpected node in component: ${node.name}`, node); + const bindings: ProgramBinding[] = []; + let result: ASTNode | undefined; + const children = node.children ?? []; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (!child) continue; + switch (child.name) { + case "ComponentBinding": + bindings.push(toProgramBinding(child, extensions)); + break; + case "in": { + const exprNode = children[i + 1]; + if (!exprNode) + throwRangedError("Missing result expression after `in`", child); + result = toHighLevelAST(exprNode, extensions); + i++; // Skip expression node, already processed + break; + } + case "let": + case "and": + break; + default: + throwRangedError( + `Unknown element in let component: ${child.name}`, + child + ); + } + } + if (!bindings.length) + return throwRangedError( + "Let components must declare at least one binding", + node + ); + const resolvedResult = result; + if (!resolvedResult) + return throwRangedError( + "Let components must provide a result after `in`", + node + ); + return { bindings, result: resolvedResult }; +}; + +const toParsedProgram = ( + node: LowLevelAST, + extensions: ParserExtension[] +): ParsedProgram => { + if (node.name !== "Program") + return throwRangedError(`Unexpected node for program: ${node.name}`, node); + const components: ProgramComponent[] = []; + const definitions: ProgramBinding[] = []; + for (const component of node.children ?? []) { + if (component.name !== "ProgramComponent") continue; + const [inner] = component.children ?? []; + if (!inner) continue; + if (inner.name === "LetComponent") { + components.push(toProgramComponent(inner, extensions)); + continue; + } + if (inner.name === "TopLevelDefinition") { + const binding = inner.children?.find( + (child) => child.name === "ComponentBinding" + ); + if (!binding) + return throwRangedError( + "Top level definition missing binding content", + inner + ); + definitions.push(toProgramBinding(binding, extensions)); + continue; + } + throwRangedError(`Unknown program component type: ${inner.name}`, inner); + } + return { components, definitions }; +}; + export const parseExpression = async ( expr: string, options: ParseOptions = {} @@ -19,6 +218,7 @@ export const parseExpression = async ( const tree = parser.parse(expr); // 2. low-level AST const lowLevelAST = toLowLevelAST(expr, tree); + normalizeNumbers(lowLevelAST); // 3. high-level AST const highLevelAST = toHighLevelAST(lowLevelAST, options.ext); resolve(highLevelAST); @@ -27,3 +227,30 @@ export const parseExpression = async ( } }); }; + +export const parseProgram = async ( + source: string, + options: ParseOptions = {} +): Promise => { + return new Promise((resolve, reject) => { + try { + const { ext = [] } = options; + const tree = parser.parse(source); + const lowLevelAST = toLowLevelAST(source, tree); + normalizeNumbers(lowLevelAST); + const programNode = lowLevelAST.children?.find( + (child) => child.name === "Program" + ); + if (!programNode) { + if (source.trim() === "") { + resolve({ components: [], definitions: [] }); + return; + } + return throwRangedError("Expected program definition", lowLevelAST); + } + resolve(toParsedProgram(programNode, ext)); + } catch (error) { + reject(error); + } + }); +}; diff --git a/src/parser/index.tokens.js b/src/parser/index.tokens.js index 9ee67a4..ab42750 100644 --- a/src/parser/index.tokens.js +++ b/src/parser/index.tokens.js @@ -116,9 +116,8 @@ function combineSurrogates(input, offset) { return s; } -export const Identifier = new ExternalTokenizer((input, stack) => { +export const Identifier = new ExternalTokenizer((input, _stack) => { let start = true; - const ok = true; let offset = 0; let eat = 1; while (true) { @@ -183,7 +182,7 @@ const isStringInterpolation = (input, offset) => { }; const makeStringContent = ({ till, term }) => { - return new ExternalTokenizer((input, stack) => { + return new ExternalTokenizer((input, _stack) => { let offset = 0; let eatNext = false; while (true) { diff --git "a/src/parser/\316\273s.d.ts" "b/src/parser/\316\273s.d.ts" new file mode 100644 index 0000000..122742b --- /dev/null +++ "b/src/parser/\316\273s.d.ts" @@ -0,0 +1,3 @@ +import type { LRParser } from "@lezer/lr"; + +export const parser: LRParser; diff --git "a/src/parser/\316\273s.grammar" "b/src/parser/\316\273s.grammar" index ac1b019..02ac066 100644 --- "a/src/parser/\316\273s.grammar" +++ "b/src/parser/\316\273s.grammar" @@ -17,6 +17,7 @@ range @left pipe @left pipeback @right + componentIn contains @left arrow @right band @right @@ -32,11 +33,36 @@ } @top SourceFile { - expressionList | "" + Program | expressionList | "" } @skip { whitespace } +Program { + ProgramComponent + (!regular0 terminator + (!regular0 ProgramComponent))* + terminator? +} + +ProgramComponent { + LetComponent | TopLevelDefinition +} + +LetComponent { + kw<"let"> ComponentBinding + (!regular0 terminator? kw<"and"> ComponentBinding)* + !regular0 terminator !componentIn kw<"in"> e +} + +ComponentBinding { + Identifier !regular0 "=" !regular0 e +} + +TopLevelDefinition { + ComponentBinding +} + // TOP LEVEL expressionList { @@ -60,7 +86,7 @@ e { } TupleExpression { - "{" !regular1 e ("," e)+ !regular1 ","? "}" + "{" !regular1 e ("," e)* !regular1 ","? "}" } pe { @@ -323,7 +349,7 @@ BooleanValue { @specialize } "."? ("^") } TimesOperator { - "."? ("*" | "/") + "."? ("*" | "/" | "%") } ComparisonOperator { "."? (">" | "<" | ">=" | "<=" | "==" | "!=" ) diff --git "a/src/parser/\316\273s.test.js" "b/src/parser/\316\273s.test.js" index 67b5216..340c664 100644 --- "a/src/parser/\316\273s.test.js" +++ "b/src/parser/\316\273s.test.js" @@ -1,9 +1,8 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { parser } from "./λs"; -test("it parses", () => { - const input = "min(1,2)"; +function collectNodeNames(input) { const tree = parser.parse(input); const nodes = []; const cursor = tree.cursor(); @@ -11,8 +10,13 @@ test("it parses", () => { if (cursor.name && cursor.name !== "⚠") { nodes.push(cursor.name); } - // console.log(`Node ${cursor.name} from ${cursor.from} to ${cursor.to}`); } while (cursor.next()); + return nodes; +} + +test("it parses", () => { + const input = "min(1,2)"; + const nodes = collectNodeNames(input); expect(nodes).toEqual([ "SourceFile", "CallExpression", @@ -22,3 +26,25 @@ test("it parses", () => { "NumberValue" ]); }); + +test("it parses singleton tuple call arguments with trailing commas cleanly", () => { + const input = `foo( + {x,}, +)`; + const tree = parser.parse(input); + const nodes = []; + const cursor = tree.cursor(); + do { + if (cursor.name) { + nodes.push(cursor.name); + } + } while (cursor.next()); + expect(nodes).toEqual([ + "SourceFile", + "CallExpression", + "Identifier", + "ArgumentList", + "TupleExpression", + "Identifier" + ]); +}); diff --git a/src/print.test.ts b/src/print.test.ts index eb90377..04c1212 100644 --- a/src/print.test.ts +++ b/src/print.test.ts @@ -1,14 +1,16 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { type ASTNode, + applyName, NameApplication, NameConstant, NameField, NameLambda, NameList, NameObject, - NameVariable + NameVariable, + newVariable } from "./ast"; import { prettyPrint, prettyPrintType } from "./print"; import { Rational } from "./rational"; @@ -140,3 +142,12 @@ test("prettyPrint infix operators", () => { const result = prettyPrint(ast); expect(result).toBe("x => ({a: [x, x], b: y => (y + 1)})"); }); + +test("prettyPrint preserves right-nested modulo grouping", () => { + const ast = applyName("%", [ + newVariable("$arg0"), + applyName("%", [newVariable("$arg1"), newVariable("$arg2")]) + ]); + + expect(prettyPrint(ast)).toBe("$arg0 % ($arg1 % $arg2)"); +}); diff --git a/src/print.ts b/src/print.ts index db7a431..9f8a75d 100644 --- a/src/print.ts +++ b/src/print.ts @@ -52,7 +52,8 @@ function getPrettyTypeVar(m: Map, typeVar: TypeVar): string { : `${Math.floor(newIndex / greekLetters.length)}`); m.set(typeVar.type, greekLetter); } - return m.get(typeVar.type); + const pretty = m.get(typeVar.type); + return pretty ?? typeVar.type; } export function prettyPrintType(type: MonoType): string { @@ -93,6 +94,7 @@ const operatorPrecedence: { [key: string]: number } = { "-": 1, "*": 2, "/": 2, + "%": 2, "^": 3, "=": 0, "<": 0, @@ -100,14 +102,17 @@ const operatorPrecedence: { [key: string]: number } = { "!": 0 }; -function isInteger(str) { +const nonAssociativeOperators = new Set(["-", "/", "%", "=", "<", ">"]); + +function isInteger(str: string) { const num = Number.parseInt(str, 10); return !Number.isNaN(num) && Number.isInteger(num) && num.toString() === str; } -function toBoldUnicode(text: string) { - const getBoldChar = (char) => { +function _toBoldUnicode(text: string) { + const getBoldChar = (char: string) => { const code = char.codePointAt(0); + if (code === undefined) return char; // Uppercase letters A-Z if (code >= 0x41 && code <= 0x5a) { @@ -163,13 +168,17 @@ export function prettyPrint(node: ASTNode, parentPrecedence = 0): string { // Treat as infix operator if name starts with specified characters. let name: string; if (node.function.type === NameVariable) { - name = node.function.name; - if (name !== "!" && /^[\+\-\*\/=<>!^&|]/.test(name)) { - const params = node.params.map((p) => - prettyPrint(p, operatorPrecedence[name]) + name = node.function.name === "IF" ? "if" : node.function.name; + if (name !== "!" && /^[+\-*/%=<>!^&|]/.test(name)) { + const precedence = operatorPrecedence[name]; + const rightPrecedence = nonAssociativeOperators.has(name) + ? precedence + 1 + : precedence; + const params = node.params.map((p, index) => + prettyPrint(p, index === 1 ? rightPrecedence : precedence) ); const expression = `${params.join(` ${name} `)}`; - if (parentPrecedence > operatorPrecedence[name]) { + if (parentPrecedence > precedence) { return `(${expression})`; } return expression; @@ -184,7 +193,7 @@ export function prettyPrint(node: ASTNode, parentPrecedence = 0): string { case NameConstant: { const value = node.value; // Stringable values - if (typeof value === "object" && "toString" in value) + if (value !== null && typeof value === "object" && "toString" in value) return value.toString(); return jsonStringify(value); } @@ -238,12 +247,16 @@ export function prettyPrintHTML(node: ASTNode, parentPrecedence = 0): string { let name: string; if (node.function.type === NameVariable) { name = node.function.name; - if (name !== "!" && /^[\+\-\*\/=<>!^&|]/.test(name)) { - const params = node.params.map((p) => - prettyPrintHTML(p, operatorPrecedence[name]) + if (name !== "!" && /^[+\-*/%=<>!^&|]/.test(name)) { + const precedence = operatorPrecedence[name]; + const rightPrecedence = nonAssociativeOperators.has(name) + ? precedence + 1 + : precedence; + const params = node.params.map((p, index) => + prettyPrintHTML(p, index === 1 ? rightPrecedence : precedence) ); const expression = `${params.join(` ${name} `)}`; - if (parentPrecedence > operatorPrecedence[name]) { + if (parentPrecedence > precedence) { return `(${expression})`; } return expression; @@ -259,7 +272,7 @@ export function prettyPrintHTML(node: ASTNode, parentPrecedence = 0): string { case NameConstant: { const value = node.value; - if (typeof value === "object" && "toString" in value) + if (value !== null && typeof value === "object" && "toString" in value) return value.toString(); return `${jsonStringify(value)}`; } diff --git a/src/program.test.ts b/src/program.test.ts index f19759e..50d7ebc 100644 --- a/src/program.test.ts +++ b/src/program.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "bun:test"; import { type ApplicationNode, @@ -61,7 +61,7 @@ describe("order_program correctly orders expressions and identifies external dep describe("deps_for correctly computes required dependencies", async () => { // No dependencies - const constantNode: ConstantNode = { + const constantNode: ConstantNode = { type: NameConstant, value: "test" }; @@ -109,3 +109,22 @@ describe("deps_for correctly computes required dependencies", async () => { expect(program.dependencies(["expr2", "expr3"])).toEqual(["func1", "var2"]); }); }); + +describe("Program parsing syntax", () => { + it("parses let components and exposes component metadata", async () => { + const program = await new Program().set(` +let foo = 1 +and bar = foo + 1 +in bar +`); + expect(program.get("foo")).toBeDefined(); + expect(program.get("bar")).toBeDefined(); + expect(program.components).toHaveLength(1); + expect(program.components[0].result.type).toBe(NameVariable); + }); + + it("supports legacy colon notation during transition", async () => { + const program = await new Program().set("foo:1"); + expect(program.get("foo")).toBeDefined(); + }); +}); diff --git a/src/program.ts b/src/program.ts index c6c1e6d..f01a816 100644 --- a/src/program.ts +++ b/src/program.ts @@ -3,7 +3,12 @@ import { Graph } from "@okcontract/graph"; import type { ASTNode } from "./ast"; import { exprDependencies } from "./deps"; import type { Environment } from "./env"; -import { type ParseOptions, parseExpression } from "./parse"; +import { + type ParsedProgram, + type ProgramComponent as ParsedProgramComponent, + type ParseOptions, + parseProgram as parseProgramSource +} from "./parse"; import { prettyPrint } from "./print"; import { plural } from "./utils"; @@ -21,6 +26,8 @@ type ProgramOptions = { parseOptions?: ParseOptions; }; +export type Binding = { name: string; expr: ASTNode }; + /** * Program defines a lambdascript program. * @usage await new Program().set([defs...]) @@ -28,6 +35,7 @@ type ProgramOptions = { export class Program { private _graph: Graph; _defs: { [key: string]: ASTNode }; + _components: ParsedProgramComponent[]; _options: ProgramOptions; /** @@ -40,15 +48,20 @@ export class Program { ) { this._graph = new Graph(); this._defs = defs; + this._components = []; this._options = options; for (const key in defs) { this._addDef(key, defs[key]); } } - private _resetProgram(defs: { [key: string]: ASTNode }) { + private _resetProgram( + defs: { [key: string]: ASTNode }, + components: ParsedProgramComponent[] = [] + ) { this._graph = new Graph(); this._defs = defs; + this._components = components; for (const key in defs) { this._addDef(key, defs[key]); } @@ -67,8 +80,10 @@ export class Program { } // Set resets the Program with definitions from unparsed expressions (chainable). - set = async (...l: string[]): Promise => - this._resetProgram(await this.parseProgram(l)); + set = async (...segments: string[]): Promise => { + const { defs, components } = await this.parseProgramSegments(segments); + return this._resetProgram(defs, components); + }; get = (key: string): ASTNode | undefined => this._defs[key]; @@ -169,7 +184,7 @@ export class Program { partialReduce = async ( env: Environment ): Promise<[Environment, string[], Set]> => { - const [order, set] = this.order; + const [order, _set] = this.order; const remainingExpressions: string[] = []; const unresolvedDeps: Set = new Set(); @@ -208,23 +223,44 @@ export class Program { * @param l list of definitions in the form "name:expr" * @returns Program (may throw error) */ - parseProgram = async (l: string[]): Promise<{ [key: string]: ASTNode }> => - Object.fromEntries( - await Promise.all( - l.map(async (def) => { - const pos = def.indexOf(":"); - const ast = await parseExpression( - def.substring(pos + 1), - this._options.parseOptions - ).catch((err) => { - throw err; - }); - return [def.substring(0, pos), ast]; - }) + private parseProgramSegments = async ( + segments: string[] + ): Promise<{ + defs: { [key: string]: ASTNode }; + components: ParsedProgramComponent[]; + }> => { + const normalized = segments.map((segment) => + segment.replace( + /^(\s*[$@A-Za-z_][$\w@]*)\s*:(.*)$/s, + (_match, name: string, expr: string) => `${name} =${expr}` ) ); + const source = normalized.join("\n"); + const parsed: ParsedProgram = await parseProgramSource( + source, + this._options.parseOptions + ); + const entries = [ + ...parsed.definitions, + ...parsed.components.flatMap((component) => component.bindings) + ].map(({ name, expr }) => [name, expr] as const); + return { + defs: Object.fromEntries(entries), + components: parsed.components + }; + }; get expressions() { - return this.order[0].map((key) => prettyPrint(this._defs[key])); + return this.order[0].map( + (key) => `${key} = ${prettyPrint(this._defs[key])}` + ); + } + + get bindings(): readonly Binding[] { + return this.order[0].map((name) => ({ name, expr: this._defs[name] })); + } + + get components(): readonly ParsedProgramComponent[] { + return this._components; } } diff --git a/src/rational.test.ts b/src/rational.test.ts index 70dc14d..4c8acb1 100644 --- a/src/rational.test.ts +++ b/src/rational.test.ts @@ -1,16 +1,16 @@ -import { describe, expect, it, test } from "vitest"; +import { describe, expect, it, test } from "bun:test"; import { - NewRational, - Rational, - type RationalNumber, add, divide, floatToRational, getNumberParts, isRationalString, multiply, + NewRational, power, + Rational, + type RationalNumber, rootNth, simplifyRational, subtract @@ -218,7 +218,7 @@ describe("power function", () => { }); test("from JSON", () => { - const v = JSON.parse('{"numid": "23448594291968334"}', (key, value) => + const v = JSON.parse('{"numid": "23448594291968334"}', (_key, value) => typeof value === "string" && /^\d+$/.test(value) ? BigInt(value) : value ); expect(new Rational(v.numid).toString()).toBe("23448594291968334"); diff --git a/src/rational.ts b/src/rational.ts index b3cdbc2..692a605 100644 --- a/src/rational.ts +++ b/src/rational.ts @@ -50,6 +50,8 @@ export class Rational { return s ? -a / b : a / b; }; + toHex = () => `0x${this.toBigInt().toString(16)}` as const; + toNumber = () => { const [a, b, s] = this._value; return s ? Number(-a) / Number(b) : Number(a) / Number(b); @@ -67,7 +69,7 @@ export class Rational { max = (...args: Rational[]) => new Rational(max(this._value, ...args.map((v) => v._value))); - compare = (op: string, b: Rational) => { + compare = (op: "<" | ">" | "<=" | ">=" | "==" | "!=", b: Rational) => { const aSimplified = this.simplify(); const bSimplified = b.simplify(); // console.log({ aSimplified, bSimplified }); @@ -126,6 +128,8 @@ export const printRational = (r: RationalNumber): string => { export const NewRational = (num: number | bigint | string): RationalNumber => { // console.log({ num }); switch (typeof num) { + case "undefined": + return [0n, 1n, false]; case "bigint": return [num < 0n ? -num : num, 1n, num < 0n]; case "number": @@ -344,3 +348,6 @@ export function max( } export const zero = new Rational(0); + +export const isRational = (v: unknown): v is Rational => + v !== null && typeof v === "object" && "simplify" in v; diff --git a/src/rationalMinMax.test.ts b/src/rationalMinMax.test.ts index 7415155..b7a7bcd 100644 --- a/src/rationalMinMax.test.ts +++ b/src/rationalMinMax.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "bun:test"; import { compareRationalNumbers, max, min } from "./rational"; diff --git a/src/singleEval.test.ts b/src/singleEval.test.ts new file mode 100644 index 0000000..9c089ae --- /dev/null +++ b/src/singleEval.test.ts @@ -0,0 +1,182 @@ +import { beforeAll, describe, expect, it, test } from "bun:test"; +import { + type AnyCell, + Sheet, + type SheetProxy, + uncellify +} from "@okcontract/cells"; + +import { NameLambda } from "./ast"; +import { Environment } from "./env"; +import { isEqual } from "./equal"; +import { toEqualWithRationals } from "./extend.test"; +import { parseExpression } from "./parse"; +import { Rational } from "./rational"; +import { evaluateASTSingle } from "./singleEval"; +import { defaultLibrary, type LibraryElement, typeString } from "./stdlib"; + +beforeAll(() => { + toEqualWithRationals(); +}); + +test("single-eval 1+1", async () => { + const proxy = new Sheet(isEqual).newProxy(); + const env = new Environment(proxy); + const ast = await parseExpression("1 + 1"); + const result = evaluateASTSingle(env)(ast); + await expect(result.get()).resolves.toEqualRational(new Rational(2)); +}); + +describe("single-eval valid expressions", async () => { + const proxy = new Sheet(isEqual).newProxy(); + const foo = proxy.new(new Rational(1)); + const bar = proxy.new(new Rational(2)); + const values = { + foo, + bar, + three: proxy.new(new Rational(3)), + obj: proxy.new({ a: new Rational(1), b: new Rational(2) }), + $test: proxy.new(new Rational(10)), + numbers: proxy.new([foo, bar]) + }; + const env = new Environment(proxy, { values }); + + const exprs: { [e: string]: unknown } = { + "1": new Rational(1), + "1+2": new Rational(3), + "5*1+1": new Rational(6), + "5*(1+1)": new Rational(10), + "foo+bar": new Rational(3), + '"foo"': "foo", + "[1,2]": [new Rational(1), new Rational(2)], + "{a:1, b:2}": { a: new Rational(1), b: new Rational(2) }, + "{a:1, b:2}.a": new Rational(1), + "obj.a": new Rational(1), + "(obj).a": new Rational(1), + "$max(1,2,3)": new Rational(3), // defaultLibrary + "$last([[3,2,1]])": new Rational(1), // defaultLibrary + "true && true": true, + "false || true": true, + "1<2": true, + $test: new Rational(10), + "[1,2].(0)": new Rational(1), + "$min(1,2,3)>=foo&&$max(1,2,3)<=three": true, + "(1000*1000)/(1000*1000)": new Rational(1), + "(x=>x+1)(1)": new Rational(2), + "(x=>y=>x+y)(1)(2)": new Rational(3), + "((x,y)=>x+y)(1)(2)": new Rational(3), + // "(x=>y=>x+y)(1,2)": new Rational(3), + // "((x,y)=>x+y)(1,2)": new Rational(3), + "0x1234": new Rational(4660), + '$concat(1,"a")': "1a", + "({a:numbers}).a[0]": new Rational(1), + "(x=>({a:x}))(numbers).a[0]": new Rational(1) + }; + + for (const [expr, expected] of Object.entries(exprs)) { + it( + `single-eval evaluates ${expr}`, + async () => { + const ast = await parseExpression(expr); + const res = evaluateASTSingle(env)(ast); + await expect(uncellify(res)).resolves.toEqualRational(expected); + }, + { timeout: 500 } + ); + } +}); + +test("single-eval parse errors", async () => { + const pErrors: string[] = [ + "1+", // unfinished op + "[1," // open array + ]; + const proxy = new Sheet(isEqual).newProxy(); + const env = new Environment(proxy); + for (const expr of pErrors) { + const ast = await parseExpression(expr); + expect(() => evaluateASTSingle(env)(ast)).toThrow("parse error"); + } +}); + +// This test mirrors eval errors: these should be caught by type checking normally. +test("single-eval errors", async () => { + const evErrors: { [e: string]: string } = { + "foo+a": "Undefined variable: foo", + "kk(1)": "Undefined variable: kk" + }; + for (const [expr, err] of Object.entries(evErrors)) { + const proxy = new Sheet(isEqual).newProxy(); + const env = new Environment(proxy); + const ast = await parseExpression(expr); + expect(() => evaluateASTSingle(env)(ast)).toThrow(err); + } +}); + +test("single-eval expressions are reactive", async () => { + const proxy = new Sheet(isEqual).newProxy(); + const foo = proxy.new(new Rational(1)); + const env = new Environment(proxy, { values: { foo } }); + const ast = await parseExpression("foo + 1"); + const expr = evaluateASTSingle(env)(ast) as AnyCell; + await expect(expr.get()).resolves.toEqualRational(new Rational(2)); + foo.set(new Rational(2)); + await expect(expr.get()).resolves.toEqualRational(new Rational(3)); +}); + +test("single-eval outputs cannot be evaluated before env value definition", async () => { + const proxy = new Sheet(isEqual).newProxy(); + const env = new Environment(proxy); + const ast = await parseExpression("foo + 1"); + expect(() => evaluateASTSingle(env)(ast)).toThrow("Undefined variable: foo"); +}); + +test("single-eval outputs are reactive with errors", async () => { + const proxy = new Sheet(isEqual).newProxy(); + const foo = proxy.new(""); + const test: LibraryElement = { + f: proxy.new((_: SheetProxy, input: AnyCell) => + input.map((_in) => { + if (!_in) throw new Error("empty"); + return _in[0]; + }) + ), + t: { + vars: [], + type: { + kind: NameLambda, + argTypes: [typeString], + returnType: typeString + } + } + }; + const env = new Environment(proxy, { + lib: { ...defaultLibrary(proxy), test: test }, + values: { foo } + }); + const ast = await parseExpression("test(foo)"); + const expr = evaluateASTSingle(env)(ast) as AnyCell; + await expect(expr.get()).resolves.toBeInstanceOf(Error); + foo.set("hello"); + await expect(expr.get()).resolves.toBe("h"); +}); + +// Cell count checks (like in cells/array.test.ts) +describe("single-eval cell counts", () => { + test("variable-only expression creates one cell and stays stable on updates", async () => { + const sheet = new Sheet(isEqual); + const proxy = sheet.newProxy(); + const foo = proxy.new(new Rational(1)); + const env = new Environment(proxy, { values: { foo } }); + + const before = sheet.stats.count; + const ast = await parseExpression("foo"); + const expr = evaluateASTSingle(env)(ast) as AnyCell; + await expect(expr.get()).resolves.toEqualRational(new Rational(1)); + expect(sheet.stats.count).toBe(before + 1); // one map cell + + foo.set(new Rational(2)); + await expect(expr.get()).resolves.toEqualRational(new Rational(2)); + expect(sheet.stats.count).toBe(before + 1); // no new cells on update + }); +}); diff --git a/src/singleEval.ts b/src/singleEval.ts new file mode 100644 index 0000000..df3d1b5 --- /dev/null +++ b/src/singleEval.ts @@ -0,0 +1,244 @@ +import { type AnyCell, Cell } from "@okcontract/cells"; + +import { + type ASTNode, + NameApplication, + NameConstant, + NameError, + NameField, + NameLambda, + NameList, + NameObject, + NameTuple, + NameVariable +} from "./ast"; +import type { Environment } from "./env"; +import type { Value } from "./eval"; +import { traverseAST } from "./traverse"; + +// Helper: collect free variables (not bound by lambdas), using traverseAST +const collectFreeVariables = (node: ASTNode): Set => { + const free = new Set(); + traverseAST(node, (n, bound) => { + if (n.type === NameVariable) { + const nameLow = n.name.toLowerCase(); + if (!bound.has(nameLow)) free.add(nameLow); + } + }); + return free; +}; + +// Helper: detect parse errors anywhere in the AST, return message if found +const findParseError = (node: ASTNode): string | undefined => { + let msg: string | undefined; + traverseAST(node, (n) => { + if (msg !== undefined) return; + if (n.type === NameError) + msg = String((n as unknown as { value?: unknown }).value ?? ""); + }); + return msg; +}; + +// Simple check for AnyCell instance +const isCell = (v: unknown): v is AnyCell => v instanceof Cell; + +/** + * Evaluate an AST as a single cell by mapping once over all used + * environment values and library functions, then computing the value + * inside the mapping closure. + */ +export const evaluateASTSingle = + (env: Environment) => + (node: ASTNode): Value => { + // Early parse error propagation (match legacy behavior) + const err = findParseError(node); + if (err !== undefined) throw new Error(`parse error: ${err}`); + // Collect free (unbound) variable names used in the expression + const free = collectFreeVariables(node); + + // Partition between user-defined environment values and library items + const envNames: string[] = []; + const envCells: AnyCell[] = []; + const libNames: string[] = []; + const libCells: AnyCell[] = []; + + for (const nameLow of free) { + const v = env.value(nameLow); + if (v !== undefined) { + envNames.push(nameLow); + envCells.push(v); + continue; + } + const lib = env.library(nameLow); + if (lib?.f) { + libNames.push(nameLow); + libCells.push(lib.f); + continue; + } + // Unknown identifier at evaluation time + // Use original casing if available + throw new Error(`Undefined variable: ${nameLow}`); + } + + // If there are no dependencies at all (constants-only), evaluate once + // and wrap into a constant cell. + if (envCells.length === 0 && libCells.length === 0) { + const value = interpret(node, env, {}); + return env.proxy.new(value, "single-eval:const"); + } + + const allNames = [...envNames, ...libNames]; + const allCells = [...envCells, ...libCells]; + + const mapFn = env.proxy.map.bind(env.proxy) as ( + cells: AnyCell[], + fn: (...values: unknown[]) => T, + id?: string + ) => AnyCell; + return mapFn( + allCells, + // values are unwrapped underlying values (env values and functions) + (...values: unknown[]) => { + const scope: Record = {}; + for (let i = 0; i < allNames.length; i++) + scope[allNames[i]] = values[i]; + return interpret(node, env, scope); + }, + "single-eval" + ); + }; + +// Interpret AST inside a single mapping closure. +// `scope` maps lower-cased identifiers to unwrapped values/functions. +const interpret = ( + node: ASTNode, + env: Environment, + scope: Record +): unknown => { + switch (node.type) { + case NameConstant: + return node.value; + + case NameVariable: { + const nameLow = node.name.toLowerCase(); + if (Object.hasOwn(scope, nameLow)) return scope[nameLow]; + // Should not happen due to pre-check, unless variable is lambda-bound + const v = env.read(nameLow); + if (v === undefined) throw new Error(`Undefined variable: ${node.name}`); + return v; + } + + case NameApplication: { + const func = interpret(node.function, env, scope); + const args = node.params.map((p) => interpret(p, env, scope)); + + if (typeof func !== "function") + throw new Error(`Not a function: ${node.function.type}`); + + // Local (lambda) functions use raw values and don't expect env + // Library / external functions expect (env, ...AnyCell[]) args + const isLocal = (func as unknown as { local?: boolean })?.local === true; + if (isLocal) { + return (func as (...xs: unknown[]) => unknown)(...args); + } + // Wrap arguments as cells when necessary + const asCell = (v: unknown): AnyCell => { + if (isCell(v)) return v; + // Arrays and objects are wrapped as a single value cell + if (Array.isArray(v)) return env.proxy.new(v, "[]"); + if (v !== null && typeof v === "object") return env.proxy.new(v, "{}"); + return env.proxy.new(v as unknown); + }; + const cellArgs = args.map(asCell); + return (func as (env: Environment, ...xs: AnyCell[]) => unknown)( + env, + ...cellArgs + ); + } + + case NameList: { + const items = node.elements.map((e) => { + const v = interpret(e, env, scope); + return isCell(v) ? (v as AnyCell) : env.proxy.new(v); + }); + return env.proxy.new(items, "[]"); + } + + case NameTuple: { + const items = node.elements.map((e) => { + const v = interpret(e, env, scope); + return isCell(v) ? (v as AnyCell) : env.proxy.new(v); + }); + return env.proxy.new(items, "{}"); + } + + case NameObject: { + const entries = Object.entries(node.values); + const values = entries.map(([, v]) => interpret(v, env, scope)); + const cells = values.map((v) => + isCell(v) ? (v as AnyCell) : env.proxy.new(v) + ); + return env.proxy.mapNoPrevious( + cells, + (...l) => Object.fromEntries(l.map((v, i) => [entries[i][0], v])), + "{}" + ); + } + + case NameField: { + const base = interpret(node.expr, env, scope) as + | AnyCell | unknown[]> + | Record + | unknown[]; + if (isCell(base)) { + if ("field" in node) + return (base as AnyCell>).map( + (obj) => obj[node.field] + ); + const sub = interpret(node.sub, env, scope) as string | number | symbol; + return (base as AnyCell | unknown[]>).map( + (obj: Record | unknown[]) => + Array.isArray(obj) + ? (obj as unknown[])[sub as number] + : (obj as Record)[ + sub as keyof Record + ] + ); + } + // non-cell object + if ("field" in node) return (base as Record)[node.field]; + const sub = interpret(node.sub, env, scope) as string | number | symbol; + return Array.isArray(base) + ? (base as unknown[])[sub as number] + : (base as Record)[ + sub as keyof Record + ]; + } + + case NameLambda: { + const param = node.parameter.toLowerCase(); + const body = node.body; + type LocalFn = ((arg: unknown) => unknown) & { local?: boolean }; + const lambda: LocalFn = (arg: unknown) => { + const childScope = { ...scope, [param]: arg }; + return interpret(body, env, childScope); + }; + lambda.local = true; + return lambda; + } + + case NameError: + throw new Error(`parse error: ${node.value}`); + + default: { + if ((node as unknown as { type: string }).type === "named") + return interpret( + (node as unknown as { value: ASTNode }).value, + env, + scope + ); + const _unreachable: never = node as never; + return _unreachable; + } + } +}; diff --git a/src/stdlib.ts b/src/stdlib.ts index 2579d53..6606940 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -1,11 +1,11 @@ import { type AnyCell, type CellArray, - type MapCell, - type SheetProxy, filterPredicateCell, last, - mapArrayCell + type MapCell, + mapArrayCell, + type SheetProxy } from "@okcontract/cells"; import { NameLambda, NameList } from "./ast"; @@ -17,10 +17,10 @@ import { Rational } from "./rational"; import type { TypeScheme } from "./typeScheme"; import { type MonoType, - type TypeConst, newTypeConst, newTypeObject, newTypeVar, + type TypeConst, typeAny, typeFunction, typeList @@ -35,7 +35,7 @@ export const typeBoolean = newTypeConst("boolean"); // Bytes type export const typeBytes = newTypeConst("bytes"); -const stringToHex = (str) => +const stringToHex = (str: string) => [...str] .map((char) => char.charCodeAt(0).toString(16).padStart(2, "0")) .join(""); @@ -123,7 +123,7 @@ const builtinMAP = (libproxy: SheetProxy): LibraryElement => { env: Environment, fn: AnyCell<(v: AnyCell) => AnyCell>, arr: CellArray - ) => mapArrayCell(env.proxy, arr, fn, "map") as AnyCell, + ) => mapArrayCell(env.proxy, arr, fn, "map"), "$map" ), doc: "Returns a mapped array", @@ -205,7 +205,7 @@ const builtinLENGTH = (libproxy: SheetProxy): LibraryElement => { ? v.length : isClassInstance(v) ? 1 - : typeof v === "object" + : v !== null && typeof v === "object" ? Object.keys(v).length : v !== null && v !== undefined ? 1 @@ -269,7 +269,7 @@ const builtinOperator = ( const builtinComparator = ( libproxy: SheetProxy, - op: string, + op: "<" | ">" | "<=" | ">=" | "==" | "!=", doc?: string ): LibraryElement => { const alpha = newTypeVar("op"); @@ -286,19 +286,21 @@ const builtinComparator = ( (a, b) => { if (a instanceof Rational && b instanceof Rational) return a.compare(op, b); + const left = a as number | string | boolean; + const right = b as number | string | boolean; switch (op) { case "<": - return a < b; + return left < right; case "<=": - return a <= b; + return left <= right; case ">": - return a > b; + return left > right; case ">=": - return a >= b; + return left >= right; case "==": - return a === b; + return left === right; case "!=": - return a !== b; + return left !== right; } throw new Error("unknown comparator"); }, @@ -482,7 +484,7 @@ export const defaultLibrary = ( $concat: { f: libproxy.new((env: Environment, ...vCells: AnyCell[]) => env.proxy.mapNoPrevious(vCells, (...v: unknown[]) => - v.reduce((acc, _v) => `${acc}${_v.toString()}`, "") + v.reduce((acc, _v) => `${acc}${String(_v)}`, "") ) ), doc: "Concatenates string representations of any value", @@ -574,16 +576,18 @@ export const typeFromJSValue = ( case "string": case "boolean": return newTypeConst(typeOfExpression); - case "object": + case "object": { if (Array.isArray(expression)) // Assuming all elements of the array are of the same type return typeList(typeFromJSValue(expression[0])); if (expression === null) return newTypeConst("null"); + const objExpression = expression as Record; return newTypeObject( Object.fromEntries( - Object.entries(expression).map(([k, v]) => [k, typeFromJSValue(v)]) + Object.entries(objExpression).map(([k, v]) => [k, typeFromJSValue(v)]) ) ); + } default: throw new Error("Unsupported expression type"); } diff --git a/src/traverse.test.ts b/src/traverse.test.ts index a50a76f..e2edca4 100644 --- a/src/traverse.test.ts +++ b/src/traverse.test.ts @@ -1,8 +1,8 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { - type ASTNode, type ApplicationNode, + type ASTNode, type ConstantNode, type ListNode, NameApplication, diff --git a/src/traverse.ts b/src/traverse.ts index 81a44be..ecf4b00 100644 --- a/src/traverse.ts +++ b/src/traverse.ts @@ -49,9 +49,13 @@ export const traverseAST = ( aux(vars)(node.sub); } break; - case NameLambda: - aux(vars.add(node.parameter))(node.body); + case NameLambda: { + // Create a new scope for the lambda body (no mutation of parent set) + const scoped = new Set(vars); + scoped.add(node.parameter); + aux(scoped)(node.body); break; + } case "named": aux(vars)(node.value); break; diff --git a/src/types.test.ts b/src/types.test.ts index 98e1122..4f175f1 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -1,5 +1,4 @@ -import { describe, expect, it, test } from "vitest"; - +import { describe, expect, it, test } from "bun:test"; import { Sheet, SheetProxy } from "@okcontract/cells"; import { @@ -11,13 +10,14 @@ import { } from "./ast"; import { Environment } from "./env"; import { parseExpression } from "./parse"; +import { prettyPrint } from "./print"; import { Rational } from "./rational"; import { typeBoolean, typeNumber, typeString } from "./stdlib"; import { Instantiate, InstantiateAlpha, - type TypeScheme, - newTypeScheme + newTypeScheme, + type TypeScheme } from "./typeScheme"; import { type MonoType, typeAny, typeList } from "./types"; @@ -295,7 +295,7 @@ describe("types for lambda functions", async () => { ); }); -test("type errors", async () => { +describe("type errors", () => { const evErrors: { [e: string]: string } = { "foo+a": "Unbound variable: foo", '"foo"+1': "Type mismatch: number vs string", @@ -305,8 +305,11 @@ test("type errors", async () => { const proxy = new SheetProxy(new Sheet()); const env = new Environment(proxy); for (const [k, err] of Object.entries(evErrors)) { - const ast = await parseExpression(k); - await expect(env.infer(ast)).rejects.toThrow(err); + it(`rejects "${k}"`, async () => { + const ast = await parseExpression(k); + console.log({ k, ast: prettyPrint(ast) }); + await expect(env.infer(ast)).rejects.toThrow(err); + }); } }); diff --git a/src/unify.test.ts b/src/unify.test.ts index 4dce047..67186e5 100644 --- a/src/unify.test.ts +++ b/src/unify.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test } from "bun:test"; import { NameVariable } from "./ast"; import { typeNumber, typeString } from "./stdlib"; diff --git a/src/unify.ts b/src/unify.ts index ed93d77..8f0487c 100644 --- a/src/unify.ts +++ b/src/unify.ts @@ -3,20 +3,29 @@ import { NameLambda, NameList, NameObject, - NameTuple + NameTuple, + type SourceRange } from "./ast"; import { prettyPrint, prettyPrintType } from "./print"; import { + isTypeConst, + isTypeVar, type MonoType, + mapType, NameAny, NameConditional, - NameGeneric, - isTypeConst, - isTypeVar, - mapType + NameGeneric } from "./types"; import { plural } from "./utils"; +const throwNodeError = (message: string, node?: ASTNode): never => { + const error = new Error(message) as Error & { range?: SourceRange }; + if (node?.range) { + error.range = node.range; + } + throw error; +}; + export class TypeSubstitution { private _current: { [typeVar: string]: MonoType }; @@ -69,14 +78,15 @@ export class TypeSubstitution { for (let i = t2.argTypes.length; i < t1.argTypes.length; i++) this._unify(t2.argVariadic, t1.argTypes[i], undefined, node); } else - throw new Error( + throwNodeError( `Function expects ${plural( "argument", t1.argTypes.length, true )} (${t2.argTypes.length} provided) ${ node ? `at ${prettyPrint(node)}` : "" - }` + }`, + node ); } for (let i = 0; i < t1.argTypes.length; i++) { @@ -87,8 +97,9 @@ export class TypeSubstitution { this._unify(t1.elementType, t2.elementType, undefined, node); } else if (t1.kind === NameTuple && t2.kind === NameTuple) { if (t1.elementTypes.length !== t2.elementTypes.length) - throw new Error( - `Different arity for tuples: ${t1.elementTypes.length} vs ${t2.elementTypes.length}` + throwNodeError( + `Different arity for tuples: ${t1.elementTypes.length} vs ${t2.elementTypes.length}`, + node ); for (let i = 0; i < t1.elementTypes.length; i++) this._unify(t1.elementTypes[i], t2.elementTypes[i], undefined, node); @@ -109,13 +120,15 @@ export class TypeSubstitution { this._unify(t1.fields[key], t2.fields[key], undefined, node); } else if (key in t1.fields) { if (!t2.open) - throw new Error( - `Object field mismatch: ${key} missing in second type` + throwNodeError( + `Object field mismatch: ${key} missing in second type`, + node ); } else { if (!t1.open) - throw new Error( - `Object field mismatch: ${key} missing in first type` + throwNodeError( + `Object field mismatch: ${key} missing in first type`, + node ); // We update locally t1 @@ -128,8 +141,9 @@ export class TypeSubstitution { } else if (t1.kind === NameGeneric && t2.kind === NameGeneric) { this._unify(t1.baseType, t2.baseType, undefined, node); if (t1.typeArgs.length !== t2.typeArgs.length) { - throw new Error( - `Generic type arguments mismatch: ${t1.typeArgs.length} vs ${t2.typeArgs.length}` + throwNodeError( + `Generic type arguments mismatch: ${t1.typeArgs.length} vs ${t2.typeArgs.length}`, + node ); } for (let i = 0; i < t1.typeArgs.length; i++) { @@ -140,13 +154,21 @@ export class TypeSubstitution { this._unify(t1.check.right, t2.check.right, undefined, node); this._unify(t1.trueType, t2.trueType, undefined, node); this._unify(t1.falseType, t2.falseType, undefined, node); - } else if (isTypeConst(t1) && isTypeConst(t2) && t1.type === t2.type) { + } else if ( + isTypeConst(t1) && + isTypeConst(t2) && + (t1.type === t2.type || + // @todo check 0x-encoded, add prefix to string type + (t1.type === "string" && t2.type === "bytes") || + (t1.type === "bytes" && t2.type === "string")) + ) { return; } else { - throw new Error( + throwNodeError( `Type mismatch: ${prettyPrintType(t1)} vs ${prettyPrintType(t2)} ${ node ? ` at ${prettyPrint(node)}` : "" - }` + }`, + node ); } }; diff --git a/tsconfig.build.json b/tsconfig.build.json index c27d338..5faf306 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,5 @@ { + "extends": "./tsconfig.json", "include": [ "src" ], @@ -14,9 +15,6 @@ ], "declaration": true, "emitDeclarationOnly": true, - "outDir": "dist", - "moduleResolution": "Bundler", - "module": "ES2020", - "target": "ES2020" + "outDir": "dist" } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 1020e9a..2018586 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,11 @@ ], "compilerOptions": { "moduleResolution": "Bundler", - "module": "ES2020", - "target": "ES2020", - "types": [ - "node" - ] + "customConditions": [ + "source" + ], + "module": "ES2022", + "target": "ES2022", + "skipLibCheck": true } -} \ No newline at end of file +} diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index d12b7e0..0000000 --- a/vite.config.js +++ /dev/null @@ -1,32 +0,0 @@ -import { resolve } from "node:path"; -import { defineConfig } from "vite"; - -export default defineConfig({ - esbuild: { - minify: false - }, - build: { - minify: "terser", - terserOptions: { - mangle: { - toplevel: true, - module: true, - properties: { - regex: /^_/ - } - }, - compress: { - drop_console: true, - drop_debugger: true - } - }, - target: "ES2020", - lib: { - entry: resolve(__dirname, "src/index.ts"), - name: "lambdascript" - }, - rollupOptions: { - plugins: [] - } - } -}); diff --git a/vitest.config.mjs b/vitest.config.mjs deleted file mode 100644 index e8d8a09..0000000 --- a/vitest.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vite"; - -export default defineConfig({ - test: { - coverage: { - exclude: ["src/index.ts", "src/types.ts"], - }, - }, -});