diff --git a/.github/workflows/compiler.yml b/.github/workflows/compiler.yml new file mode 100644 index 0000000..d117c21 --- /dev/null +++ b/.github/workflows/compiler.yml @@ -0,0 +1,106 @@ +# The compiler's npm package, @reactivescript/compiler (compiler/): the crate's Node-API build for each platform, +# each tested on its own runner, then published as one package per platform (@reactivescript/compiler-) +# beside the main package, which loads the one that fits and carries the Vite front end (`/vite`). +# +# A release is a version bump merged to main (compiler/README.md, Releasing), once the repository variable RELEASES +# is `on`: a version npm does not have is published by trusted publishing (OIDC, no token) and tagged +# `compiler-v`. +name: compiler + +on: + push: + branches: [main] + paths: ["compiler/**", ".github/workflows/compiler.yml"] + pull_request: + paths: ["compiler/**", ".github/workflows/compiler.yml"] + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, test: true } + - { os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, test: true } + - { os: macos-latest, target: aarch64-apple-darwin, test: true } + - { os: macos-latest, target: x86_64-apple-darwin, test: false } # cross-built on Apple silicon + - { os: windows-latest, target: x86_64-pc-windows-msvc, test: true } + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: compiler + shell: bash + steps: + - uses: actions/checkout@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: compiler + key: ${{ matrix.target }} + - uses: actions/setup-node@v5 + with: + node-version: 22 + - run: npm ci + - name: Node-API build + run: npx napi build --platform --release --strip --features node --target ${{ matrix.target }} --manifest-path ./Cargo.toml -- --lib + - name: Test + if: matrix.test + run: npm test + - uses: actions/upload-artifact@v4 + with: + name: binding-${{ matrix.target }} + path: compiler/*.node + if-no-files-found: error + + # which version is new: on main only, after every platform built + version: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.RELEASES == 'on' + needs: build + runs-on: ubuntu-latest + outputs: + version: ${{ steps.v.outputs.version }} + steps: + - uses: actions/checkout@v5 + - id: v + run: | + v=$(node -p 'require("./compiler/package.json").version') + if [ -z "$(npm view "@reactivescript/compiler@$v" version 2>/dev/null)" ]; then echo "version=$v" >> "$GITHUB_OUTPUT"; fi + + publish: + needs: version + if: needs.version.outputs.version != '' + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write # trusted publishing: npm takes the job's OIDC token, and adds provenance + contents: write # the tag + defaults: + run: + working-directory: compiler + steps: + - uses: actions/checkout@v5 + - uses: actions/download-artifact@v4 + with: + pattern: binding-* + path: compiler/artifacts + merge-multiple: true + - uses: actions/setup-node@v5 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + - run: npm install -g npm@latest # trusted publishing needs npm 11.5.1 or later + - run: npm ci + - name: Platform packages + run: | + node scripts/sync-optional-deps.mjs + npx napi artifacts --output-dir artifacts --npm-dir npm + for d in npm/*/; do npm publish "$d" --access public; done + - run: npm publish --access public + - name: Tag + working-directory: . + run: git tag "compiler-v$V" && git push origin "compiler-v$V" + env: + V: ${{ needs.version.outputs.version }} diff --git a/.gitignore b/.gitignore index c2658d7..31ccf33 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ node_modules/ +compiler/target/ +compiler/*.node +compiler/test/.out/ +compiler/artifacts/ diff --git a/compiler/Cargo.lock b/compiler/Cargo.lock new file mode 100644 index 0000000..9c63c55 --- /dev/null +++ b/compiler/Cargo.lock @@ -0,0 +1,1427 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "compact_str" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "static_assertions", + "zmij", +] + +[[package]] +name = "convert_case" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1af709f1f33454bf52eadfc8c78b3b9ef9cb26fb54d16dc9cd9a7299f899fd1b" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cow-utils" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dragonbox_ecma" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "escape-simd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0440157235aa4badd45ea073af8de2ba44ffead74bf8a7833b215533c36f08e" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "htmlize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e815d50d9e411ba2690d730e6ec139c08260dddb756df315dbd16d01a587226" +dependencies = [ + "memchr", + "pastey", + "phf 0.13.1", + "phf_codegen", + "serde_json", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406e9ae5b95d8aa37db6293405931b766a6172d1848daee22d70eb44aa78a43c" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3632ebbd0c5c3637ba1a40bdf8288f2cb9d2268e73441a69d5fc73d7cfbaa87c" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b195dc605f81aa181bec9b3bd14e26cc71b903e4644f6af578ca9772c7a710" +dependencies = [ + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "smallvec", + "utf8_iter", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913a7d294ad056474749152107309fdee3ec17d2482d1ab70fdc61a81c64b9a3" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "json-escape-simd" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d302fe9761f19c4123edfe82b66be55a08e54012403cd7e27f8a176538737954" +dependencies = [ + "escape-simd", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.39.0+unofficial" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac010f19d6c4af81eeb4018a39d7a115de9d285af45c126a4ac02e6fc5716b7" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "napi" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a746ad50efbfb632ade1de4025519a477decbbd798cea1a6ac41d7c44f80083" +dependencies = [ + "bitflags", + "ctor", + "futures", + "libc", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b899b545d3aa6dca985939059f258c5488d34e4ecf39c274e20009748f4b846d" + +[[package]] +name = "napi-derive" +version = "3.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff48271a0f61abb83f3e2d1989fa71169143db95f6f948c3a8903961f2855dc" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "napi-derive-backend" +version = "6.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2639052c5aff9956a21d7b6202fc61d5e89c5f19c72d289b7a0641ea94e8159" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn 3.0.9", +] + +[[package]] +name = "napi-sys" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d153beb7d5c973f4b9395be8a83054a28968b61188f1dd1a2cc2321af22daea2" +dependencies = [ + "libloading", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "owo-colors" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" + +[[package]] +name = "oxc-miette" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330e07b3807046e0afa8fcc94f1fe976e93831b370d148bfe1168cb53da1b913" +dependencies = [ + "bytecount", + "memchr", + "owo-colors", + "textwrap", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "oxc_allocator" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9262c7bbd58c8c32c2c60d1a7b6c274cd2aeaff1b726a2fb86081b75084698cb" +dependencies = [ + "allocator-api2", + "hashbrown", + "oxc_data_structures", + "rustc-hash", +] + +[[package]] +name = "oxc_ast" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8134809b665d524d9a0ddeee87a5d3602776aa2410a10a0439fd25eeb594a5d" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_estree", + "oxc_regular_expression", + "oxc_span", + "oxc_str", + "oxc_syntax", +] + +[[package]] +name = "oxc_ast_macros" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac120c6863a9d036c78a42a5e4d2f58bb27c5df795d61d904f6e19e48c681a37" +dependencies = [ + "phf 0.14.0", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "oxc_ast_visit" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a57626cd6ef87f5173110a301868e3c438fd8e0576f489d9a61540ef133f686" +dependencies = [ + "oxc_allocator", + "oxc_ast", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_codegen" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8a30cbe510caef092aa75fe29e8d37576ca8eb2ab3e9ab34a453a92863e7f0" +dependencies = [ + "bitflags", + "cow-utils", + "oxc_allocator", + "oxc_ast", + "oxc_data_structures", + "oxc_ecmascript", + "oxc_index", + "oxc_semantic", + "oxc_sourcemap", + "oxc_span", + "oxc_str", + "oxc_syntax", + "rustc-hash", +] + +[[package]] +name = "oxc_data_structures" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a71efea56db93d3ce9e88e1d9f74f6d50fc2b06092444cdd08579f5827fd8db" + +[[package]] +name = "oxc_diagnostics" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff8638533094ecdcd38a63930c003c261d81650e4531c3bc805c797aec2a52" +dependencies = [ + "cow-utils", + "memchr", + "oxc-miette", + "percent-encoding", + "smallvec", +] + +[[package]] +name = "oxc_ecmascript" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c7a326f49f06da37affdcad2fb3ebab46ac4ada8953a428f7cbd04e5cc9e6e" +dependencies = [ + "dragonbox_ecma", + "itoa", + "num-bigint", + "num-traits", + "oxc_ast", + "oxc_data_structures", + "oxc_span", + "oxc_syntax", +] + +[[package]] +name = "oxc_estree" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8275fcb9674a1c320f0bdbc2f534ba9bb09fcb8b1c9fe306c9595d262a06bfcb" + +[[package]] +name = "oxc_index" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e" +dependencies = [ + "nonmax", + "serde", +] + +[[package]] +name = "oxc_parser" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13016de81370106ea4065cfabe4a278dc09e82523cfe22bb6a8ab8e993b4c3a" +dependencies = [ + "bitflags", + "cow-utils", + "memchr", + "num-bigint", + "num-traits", + "oxc_allocator", + "oxc_ast", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_regular_expression", + "oxc_span", + "oxc_str", + "oxc_syntax", + "rustc-hash", + "seq-macro", +] + +[[package]] +name = "oxc_regular_expression" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c375b89d88ec03301b0e62528715699551871861197aa39148e95a1422e1ae9b" +dependencies = [ + "bitflags", + "oxc_allocator", + "oxc_ast_macros", + "oxc_diagnostics", + "oxc_span", + "oxc_str", + "phf 0.14.0", + "rustc-hash", + "unicode-id-start", +] + +[[package]] +name = "oxc_semantic" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8d80604c4220c8f5b1c19682216921397fb45a616db47d3a044474662f0c2e" +dependencies = [ + "itertools", + "memchr", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_data_structures", + "oxc_diagnostics", + "oxc_ecmascript", + "oxc_index", + "oxc_span", + "oxc_str", + "oxc_syntax", + "rustc-hash", + "self_cell", + "smallvec", +] + +[[package]] +name = "oxc_sourcemap" +version = "8.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b415102a94b483bbd76d13e850fe87961197e5795c79a8523ebec5d82025f94f" +dependencies = [ + "base64-simd", + "json-escape-simd", + "rustc-hash", + "serde", + "serde_json", +] + +[[package]] +name = "oxc_span" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd732008dbfb09984106925f7ff7268687c8020b89bd7ee0f0b695c5bb07950" +dependencies = [ + "compact_str", + "oxc-miette", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_str", +] + +[[package]] +name = "oxc_str" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d2ccb0f5aa4da9fc98ef70f59553c2779e28e064a8b271fcb1e88c11a63607" +dependencies = [ + "compact_str", + "hashbrown", + "oxc_allocator", + "oxc_estree", +] + +[[package]] +name = "oxc_syntax" +version = "0.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ff0e987ab15a71dae36fd544b9859513e76639e2cc6c5e25895081162755b1" +dependencies = [ + "bitflags", + "cow-utils", + "dragonbox_ecma", + "nonmax", + "oxc_allocator", + "oxc_ast_macros", + "oxc_estree", + "oxc_index", + "oxc_span", + "oxc_str", + "phf 0.14.0", + "unicode-id-start", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared 0.14.0", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared 0.14.0", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator 0.14.0", + "phf_shared 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reactivescript" +version = "0.0.0" +dependencies = [ + "indexmap", + "napi", + "napi-build", + "napi-derive", + "oxc_allocator", + "oxc_ast", + "oxc_sourcemap", + "oxc_span", + "oxc_syntax", + "regex", + "serde_json", + "solidjs-compiler", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "foldhash", + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "solidjs-compiler" +version = "0.0.0" +source = "git+https://github.com/solidjs/solid?rev=d2323b077c7dda32ebced7843c2ce2b788a91e8d#d2323b077c7dda32ebced7843c2ce2b788a91e8d" +dependencies = [ + "html5ever", + "htmlize", + "markup5ever_rcdom", + "napi-build", + "oxc_allocator", + "oxc_ast", + "oxc_ast_visit", + "oxc_codegen", + "oxc_parser", + "oxc_semantic", + "oxc_span", + "oxc_str", + "oxc_syntax", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "icu_segmenter", + "smawk", + "unicode-width", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "unicode-id-start" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xml5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab627f34ff61b80d756180d556f9c68801d836d271b3b8c094504ceca69d221" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml new file mode 100644 index 0000000..f2aa7b6 --- /dev/null +++ b/compiler/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "reactivescript" +version = "0.0.0" +edition = "2021" +description = "ReactiveScript's compiler (spec/11): parse, check, resolve, and lower through Solid's solidjs-compiler" +license = "MIT" +publish = false + +[lib] +name = "reactivescript" +path = "src/lib.rs" +# rlib for rxc; cdylib for the Node-API build (`--features node`, src/node.rs) that @reactivescript/compiler ships +crate-type = ["rlib", "cdylib"] + +[[bin]] +name = "rxc" +path = "src/main.rs" + +[features] +node = ["dep:napi", "dep:napi-derive"] + +[dependencies] +indexmap = "2" +regex = "1" +serde_json = { version = "1", features = ["preserve_order"] } +# CP-1b: Solid's compiler, at the commit ref/ runs on (ref/SOLID_NEXT), and the Oxc release it is built against (CP-1c: +# the program crosses the seam, so the versions must match) +solidjs-compiler = { git = "https://github.com/solidjs/solid", rev = "d2323b077c7dda32ebced7843c2ce2b788a91e8d", default-features = false } +oxc_allocator = "0.144" +oxc_ast = "0.144" +oxc_span = "0.144" +oxc_sourcemap = "8.1" +oxc_syntax = "0.144" +napi = { version = "3.12", default-features = false, features = ["napi4"], optional = true } +napi-derive = { version = "3.6", optional = true } + +[build-dependencies] +napi-build = "2.4" + +[profile.release] +debug = false diff --git a/compiler/README.md b/compiler/README.md new file mode 100644 index 0000000..3b3f386 --- /dev/null +++ b/compiler/README.md @@ -0,0 +1,102 @@ +# compiler — ReactiveScript's crate + +The production compiler (spec/11 CP-1a): a Rust crate, `reactivescript`, with +the command line `rxc`. It has parse (07), modules (09), check (02–06, 08), +signatures (MO-9) and the resolved model between check and lower (CP-6), +ported from `ref/src/`, and the lowering, ported from `ref/compile/`: each +module is built as an `oxc_ast::Program` and handed to Solid's +`solidjs-compiler` (`compile_program`, CP-1c; styles through `compile_style`, +CP-1d). Both give the same answers as the reference. + +| module | port of | | +| --- | --- | --- | +| `parse.rs`, `ast.rs` | `ref/src/parse.ts`, `ast.ts` | `.rx` and `.d.rx` declarations; the AST serialises as the reference's does | +| `modules.rs` | `modules.ts` | the module graph, MO-5 cycles, MO-6 missing modules, MO-8/MO-9 declaration files | +| `check.rs` | `check.ts` | every diagnostic, with the reference's messages | +| `signature.rs` | `signature.ts` | a module's signature, printed and read back as `.d.rx` | +| `content.rs` | `content.ts` | SV-7d HTML nesting | +| `model.rs` | `model.ts` | CP-6: regions, symbols, computations with their reads, writes and calls | +| `emit.rs`, `awaits.rs` | `ref/compile/emit.ts`, `awaits.ts` | the lowering: `__rt` calls around JSX views, and each target through Solid | +| `js.rs`, `oxc.rs` | | the lowered module as a tree, then as an Oxc program (spans: spec/11 CP-1c) | + +The Solid pin (`solidjs-compiler` in `Cargo.toml`) must be the commit in +`ref/SOLID_NEXT`. + +```sh +cargo build --release +target/release/rxc check ../examples/todo.rx # diagnostics +target/release/rxc model ../examples/todo.rx # the resolved model, as JSON +target/release/rxc compile ../examples/todo.rx --out out # modules (dom; --target ssr|universal), source maps onto .rx, style.css +target/release/rxc app ../examples/todo.rx # the same as JSON, imports by .rx path: what the Vite front end gets +npm --prefix ../ref run crate # CP-5: the crate against ref/ on every program +npm --prefix ../ref run crate -- --lower # … and the lowered JavaScript, for every target, and its source maps +RX_CRATE=1 npm --prefix ../ref run compiled # the scenarios, run on the crate's output +RX_CRATE=1 npm --prefix ../ref run ssr # … served and hydrated from the crate's output +``` + +## The npm package: `@reactivescript/compiler` + +The crate's Node-API build (`--features node`, `src/node.rs`) is the npm +package `@reactivescript/compiler`, with one binary per platform +(`@reactivescript/compiler-`, in `npm/`) as `@solidjs/compiler` +ships. Its `/vite` export (`vite.mjs`) is ReactiveScript's front end for +Solid's Vite plugin, which loads it on its own: an app installs the package +and keeps `solid()` as it is (spec/11 CP-7, decision 7). + +```sh +npm ci && npm run build # compiler..node and its loader, index.js +npm test # the binding compiles a program for every target +SOLID_VITE_PLUGIN=../../solid-vite-plugin node test/vite.mjs # every example app, built by Vite and run in Chromium +``` + +`test/vite.mjs` needs a built checkout of solid-vite-plugin with built-in +`.rx` (solidjs/solid-vite-plugin#471) until a release has it. + +What happens to a `.rx` module in Vite: + +- **Entries find themselves.** JavaScript imports only a program's entry, + whose default export mounts the app; a dependency is a factory over the + app (09 MO-1b) that only `.rx` imports. So an `.rx` module imported + plainly is an entry. The program is checked and compiled whole from it + (CP-2), once per target and again after a change to one of its files. + Dependencies are imported as `./x.rx?rx-dep` and served from the build of + a program that contains them; a module two programs share is one module + (CP-4b `module`). A failed check fails the transform with the checker's + diagnostics. +- **Already through Solid's compiler** for the environment's `generate`, + with its map onto `.rx` (by line, CP-1c) and, on the entry, the program's + stylesheet. Solid's plugin then runs its lazy, refresh and + server-function passes (12 SF-7a) and links the stylesheet as a CSS module + (CSS-4). + +Not yet: hot updates of `.rx` modules are untested; server rendering in Vite +needs the runtime package (CP-4a; the examples take `createApp` from +`ref/compile/rt-web.ts`) and the request-scoped app server functions run in; +no WebAssembly fallback for platforms without a binary. + +### Releasing + +A release is a version bump merged to `main`: `npm version +--no-git-tag-version` here, whose `version` script brings `npm/*` and +`optionalDependencies` along. The `compiler` workflow builds and tests +every platform on every change; on `main`, once the repository variable +`RELEASES` is `on`, it publishes a version npm does not have, platform +packages first, by trusted publishing from the `release` environment, and +tags it `compiler-v`. + +Once, by hand: npm sets up trusted publishing only on a package that +exists, so each of the six packages needs a placeholder first, + +```sh +npm login +for p in compiler compiler-darwin-x64 compiler-darwin-arm64 compiler-linux-x64-gnu compiler-linux-arm64-gnu compiler-win32-x64-msvc; do + npx setup-npm-trusted-publish "@reactivescript/$p" +done +``` + +then, on each package's **Settings › Trusted publishing**, GitHub Actions: +owner `ryansolid`, repository `reactivescript`, workflow `compiler.yml`, +environment `release`. + +`ref/` stays the specification: a change of behaviour lands there first (with +its scenario), then here, and `ref/crate.ts` keeps the two equal. diff --git a/compiler/build.rs b/compiler/build.rs new file mode 100644 index 0000000..280c2fb --- /dev/null +++ b/compiler/build.rs @@ -0,0 +1,6 @@ +fn main() { + // the Node-API build links against the host's symbols (src/node.rs) + if std::env::var_os("CARGO_FEATURE_NODE").is_some() { + napi_build::setup(); + } +} diff --git a/compiler/index.d.ts b/compiler/index.d.ts new file mode 100644 index 0000000..1477570 --- /dev/null +++ b/compiler/index.d.ts @@ -0,0 +1,16 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ + +/** + * Which binding artifact the generated loader actually loaded: `'native'` for + * a native addon, otherwise the `platformArchABI` of the WASI flavor. Every + * flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH` + * can point the loader at a WASI artifact this package does not build itself. + */ +export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1' + +/** + * `rxc app`'s answer, as JSON text: the program `entry` (an absolute path) starts, for `target` (dom, ssr or + * universal; dom by default) over `module_name` (`@solidjs/web` by default). + */ +export declare function app(entry: string, target?: string | undefined | null, moduleName?: string | undefined | null): string diff --git a/compiler/index.js b/compiler/index.js new file mode 100644 index 0000000..7aa2e17 --- /dev/null +++ b/compiler/index.js @@ -0,0 +1,838 @@ +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('fs') +let nativeBinding = null +// Which artifact actually loaded. The WASI fallback chain overwrites it with +// the flavor it resolved; the late native retry below leaves it alone because +// it only runs while no WASI candidate has been loaded. The take-over check +// after the load-failure block can also set it. +let __napiLoadedBindingTarget = 'native' +// What this loader's own `requireNative()` calls returned. The check after +// the load-failure block compares it with `nativeBinding` to tell a binding +// this loader loaded from one that code outside it put in its place. +let __napiRequiredNativeBinding = null +const loadErrors = [] + +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (process.report && typeof process.report.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true + } + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH) + // The override may be a generated WASI loader, which already reports its + // own flavor. Adopt it: `module.exports` aliases this object, so claiming + // 'native' would both misreport the artifact and overwrite the loader's + // marker through the alias. + __napiLoadedBindingTarget = + overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string' + ? overrideBinding.__napiBindingTarget + : 'native' + return overrideBinding + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./compiler.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-android-arm64') + const bindingPackageVersion = require('@reactivescript/compiler-android-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./compiler.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-android-arm-eabi') + const bindingPackageVersion = require('@reactivescript/compiler-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { + try { + return require('./compiler.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-win32-x64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-win32-x64-msvc') + const bindingPackageVersion = require('@reactivescript/compiler-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./compiler.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-win32-ia32-msvc') + const bindingPackageVersion = require('@reactivescript/compiler-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./compiler.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-win32-arm64-msvc') + const bindingPackageVersion = require('@reactivescript/compiler-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./compiler.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-darwin-universal') + const bindingPackageVersion = require('@reactivescript/compiler-darwin-universal/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./compiler.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-darwin-x64') + const bindingPackageVersion = require('@reactivescript/compiler-darwin-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./compiler.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-darwin-arm64') + const bindingPackageVersion = require('@reactivescript/compiler-darwin-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./compiler.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-freebsd-x64') + const bindingPackageVersion = require('@reactivescript/compiler-freebsd-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./compiler.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-freebsd-arm64') + const bindingPackageVersion = require('@reactivescript/compiler-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'ppc64') { + try { + return require('./compiler.freebsd-ppc64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-freebsd-ppc64') + const bindingPackageVersion = require('@reactivescript/compiler-freebsd-ppc64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./compiler.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-x64-musl') + const bindingPackageVersion = require('@reactivescript/compiler-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-x64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./compiler.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-arm64-musl') + const bindingPackageVersion = require('@reactivescript/compiler-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-arm64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./compiler.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-arm-musleabihf') + const bindingPackageVersion = require('@reactivescript/compiler-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-arm-gnueabihf') + const bindingPackageVersion = require('@reactivescript/compiler-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./compiler.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-loong64-musl') + const bindingPackageVersion = require('@reactivescript/compiler-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-loong64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./compiler.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-riscv64-musl') + const bindingPackageVersion = require('@reactivescript/compiler-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./compiler.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-riscv64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./compiler.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-ppc64-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./compiler.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-linux-s390x-gnu') + const bindingPackageVersion = require('@reactivescript/compiler-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./compiler.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-openharmony-arm64') + const bindingPackageVersion = require('@reactivescript/compiler-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./compiler.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-openharmony-x64') + const bindingPackageVersion = require('@reactivescript/compiler-openharmony-x64/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./compiler.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@reactivescript/compiler-openharmony-arm') + const bindingPackageVersion = require('@reactivescript/compiler-openharmony-arm/package.json').version + if (bindingPackageVersion !== '0.0.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +function createLoadErrorChain(errors) { + return errors.reduce((previous, current) => { + let message + try { + message = + current && typeof current.message === 'string' + ? current.message + : String(current) + } catch { + message = 'Unknown error' + } + const error = new Error(message) + error.cause = previous + return error + }, null) +} + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → prefer WASI, but retain native as a lazy fallback +// 'error' → require WASI without initializing a native fallback +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +// +// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict +// WASI loading. It never crosses into another flavor or falls back to native. +const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR +const __napiWasiFlavorRequested = + typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 +if ( + __napiWasiFlavorRequested && + __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 +) { + throw new Error( + 'Unsupported WASI flavor "' + + __napiWasiFlavor + + '". Available flavors: ' + + __napiWasiFlavors.join(', '), + ) +} +const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || + forceWasiError || + __napiWasiFlavorRequested + +if (!forceWasi) { + nativeBinding = requireNative() + __napiRequiredNativeBinding = nativeBinding +} + +if (!nativeBinding || forceWasi) { + let wasiBinding = null + let wasiBindingLoaded = false + const wasiBindingErrors = [] + const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { + try { + require.resolve(specifier) + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + if (isPackage) { + try { + require.resolve(specifier + '/package.json') + } catch (packageError) { + if (packageError && packageError.code === 'MODULE_NOT_FOUND') { + return resolveError + } + // An exports restriction proves the package exists even when its + // package.json is not public. Preserve the root resolution failure. + throw resolveError + } + // The package exists but its main/export target is broken. + throw resolveError + } + return resolveError + } + if (localArtifacts) { + let artifactError = null + for (let i = 0; i < localArtifacts.length; i++) { + try { + require.resolve(localArtifacts[i]) + return null + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + artifactError = resolveError + } + } + return artifactError + } + return null + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./compiler.wasi.cjs', false, ['./compiler.wasm32-wasi.debug.wasm', './compiler.wasm32-wasi.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./compiler.wasi.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasi' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@reactivescript/compiler-wasm32-wasi', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@reactivescript/compiler-wasm32-wasi/package.json').version + if (bindingPackageVersion !== '0.0.0') { + throw new Error(`WASI binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@reactivescript/compiler-wasm32-wasi') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasi' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if ( + !wasiBindingLoaded && + forceWasi && + !forceWasiError && + !__napiWasiFlavorRequested + ) { + nativeBinding = requireNative() + __napiRequiredNativeBinding = nativeBinding + } + if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { + const error = new Error( + __napiWasiFlavorRequested + ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' + : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', + ) + error.cause = createLoadErrorChain(wasiBindingErrors) + throw error + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = createLoadErrorChain(loadErrors) + throw error + } + throw new Error(`Failed to load native binding`) +} + +// A package can patch its own fallback in front of the load-failure check +// above, e.g. a WebContainer download through `napi-postinstall/fallback`. +// That fallback swaps `nativeBinding` for a generated WASI loader, which has +// already stamped its own flavor, while the target above still says 'native'. +// Take that flavor over, or the CommonJS stamp throws a conflict and the ESM +// export reports the wrong artifact. Three limits keep the conflict check: +// - only a binding this loader did not load itself, so a native addon whose +// `#[napi(module_exports)]` hook claims the name is still rejected; +// - only an own data property, read through its descriptor, so no addon +// getter runs here, and it must read back the same, as the stamp reads it; +// - only a flavor in `__napiWasiFlavors`, the ones this loader's own WASI +// chain can report ('wasm32-wasi' for a package without a WASI target). +// This is metadata and never fails a load: a throwing Proxy trap keeps 'native'. +if ( + __napiLoadedBindingTarget === 'native' && + nativeBinding !== __napiRequiredNativeBinding +) { + try { + const __napiBindingTargetDescriptor = Object.getOwnPropertyDescriptor( + nativeBinding, + '__napiBindingTarget', + ) + if ( + __napiBindingTargetDescriptor && + __napiWasiFlavors.indexOf(__napiBindingTargetDescriptor.value) !== -1 && + nativeBinding.__napiBindingTarget === __napiBindingTargetDescriptor.value + ) { + __napiLoadedBindingTarget = __napiBindingTargetDescriptor.value + } + } catch { + // keep 'native' + } +} + +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} +// Stamp before the alias, not after. The guard only reads `nativeBinding` +// (`hasOwnProperty` plus a comparison), which is safe against any addon +// accessor; an assignment is not, because a `#[napi(module_exports)]` hook can +// expose a getter reporting this very value and a setter that throws. So the +// assignment lands on the loader's own `module.exports`, still the original +// object here, and the alias below replaces it. +// +// The assignment is what keeps the marker a statically visible CommonJS export: +// `cjs-module-lexer` is Node's CJS -> ESM named export detection, it cannot see +// a bare call, and the later `module.exports = nativeBinding` does not undo the +// detection. The assignment itself always succeeds — its target is this +// loader's own, still extensible `module.exports` — and the alias below then +// discards the value it wrote. What a consumer reads is whatever the guard put +// on `nativeBinding`, so on a frozen binding, where the guard skips, the +// linked import resolves to `undefined`. +module.exports.__napiBindingTarget = __napiStampBindingTarget(nativeBinding, __napiLoadedBindingTarget) +module.exports = nativeBinding +module.exports.app = nativeBinding.app diff --git a/compiler/npm/darwin-arm64/README.md b/compiler/npm/darwin-arm64/README.md new file mode 100644 index 0000000..90c5582 --- /dev/null +++ b/compiler/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `@reactivescript/compiler-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `@reactivescript/compiler` diff --git a/compiler/npm/darwin-arm64/package.json b/compiler/npm/darwin-arm64/package.json new file mode 100644 index 0000000..15c41bb --- /dev/null +++ b/compiler/npm/darwin-arm64/package.json @@ -0,0 +1,21 @@ +{ + "name": "@reactivescript/compiler-darwin-arm64", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "compiler.darwin-arm64.node", + "files": [ + "compiler.darwin-arm64.node" + ], + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "os": [ + "darwin" + ] +} diff --git a/compiler/npm/darwin-x64/README.md b/compiler/npm/darwin-x64/README.md new file mode 100644 index 0000000..a81328a --- /dev/null +++ b/compiler/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `@reactivescript/compiler-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `@reactivescript/compiler` diff --git a/compiler/npm/darwin-x64/package.json b/compiler/npm/darwin-x64/package.json new file mode 100644 index 0000000..49599e6 --- /dev/null +++ b/compiler/npm/darwin-x64/package.json @@ -0,0 +1,21 @@ +{ + "name": "@reactivescript/compiler-darwin-x64", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "compiler.darwin-x64.node", + "files": [ + "compiler.darwin-x64.node" + ], + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "os": [ + "darwin" + ] +} diff --git a/compiler/npm/linux-arm64-gnu/README.md b/compiler/npm/linux-arm64-gnu/README.md new file mode 100644 index 0000000..210847a --- /dev/null +++ b/compiler/npm/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `@reactivescript/compiler-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `@reactivescript/compiler` diff --git a/compiler/npm/linux-arm64-gnu/package.json b/compiler/npm/linux-arm64-gnu/package.json new file mode 100644 index 0000000..0276e97 --- /dev/null +++ b/compiler/npm/linux-arm64-gnu/package.json @@ -0,0 +1,24 @@ +{ + "name": "@reactivescript/compiler-linux-arm64-gnu", + "version": "0.0.0", + "cpu": [ + "arm64" + ], + "main": "compiler.linux-arm64-gnu.node", + "files": [ + "compiler.linux-arm64-gnu.node" + ], + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] +} diff --git a/compiler/npm/linux-x64-gnu/README.md b/compiler/npm/linux-x64-gnu/README.md new file mode 100644 index 0000000..9d426ff --- /dev/null +++ b/compiler/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@reactivescript/compiler-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@reactivescript/compiler` diff --git a/compiler/npm/linux-x64-gnu/package.json b/compiler/npm/linux-x64-gnu/package.json new file mode 100644 index 0000000..8cf79b0 --- /dev/null +++ b/compiler/npm/linux-x64-gnu/package.json @@ -0,0 +1,24 @@ +{ + "name": "@reactivescript/compiler-linux-x64-gnu", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "compiler.linux-x64-gnu.node", + "files": [ + "compiler.linux-x64-gnu.node" + ], + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "os": [ + "linux" + ], + "libc": [ + "glibc" + ] +} diff --git a/compiler/npm/win32-x64-msvc/README.md b/compiler/npm/win32-x64-msvc/README.md new file mode 100644 index 0000000..289a501 --- /dev/null +++ b/compiler/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@reactivescript/compiler-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@reactivescript/compiler` diff --git a/compiler/npm/win32-x64-msvc/package.json b/compiler/npm/win32-x64-msvc/package.json new file mode 100644 index 0000000..0dae781 --- /dev/null +++ b/compiler/npm/win32-x64-msvc/package.json @@ -0,0 +1,21 @@ +{ + "name": "@reactivescript/compiler-win32-x64-msvc", + "version": "0.0.0", + "cpu": [ + "x64" + ], + "main": "compiler.win32-x64-msvc.node", + "files": [ + "compiler.win32-x64-msvc.node" + ], + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "os": [ + "win32" + ] +} diff --git a/compiler/package-lock.json b/compiler/package-lock.json new file mode 100644 index 0000000..2297bef --- /dev/null +++ b/compiler/package-lock.json @@ -0,0 +1,1946 @@ +{ + "name": "@reactivescript/compiler", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@reactivescript/compiler", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^3.10.6" + }, + "optionalDependencies": { + "@reactivescript/compiler-darwin-arm64": "0.0.0", + "@reactivescript/compiler-darwin-x64": "0.0.0", + "@reactivescript/compiler-linux-arm64-gnu": "0.0.0", + "@reactivescript/compiler-linux-x64-gnu": "0.0.0", + "@reactivescript/compiler-win32-x64-msvc": "0.0.0" + }, + "peerDependencies": { + "@solidjs/vite-plugin": "*" + }, + "peerDependenciesMeta": { + "@solidjs/vite-plugin": { + "optional": true + } + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.8.tgz", + "integrity": "sha512-NY/S1EalQhlonqhwwsarrcnkH7s9ByQuvjWYdk1jB2Gb9EPQJjKkhCTWjM3yQ0UpHaLm1XbIL89b+32G8rhO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.5.tgz", + "integrity": "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.6.tgz", + "integrity": "sha512-Z5SOeRC5VNkINfKN4rY8BSaYKb3GYi8iSEqZZRqogsE2GhBBToQ0Xw25SbI+4bcatLzU009mZ5d9EuJNiJuUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/external-editor": "^3.0.7", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.8.tgz", + "integrity": "sha512-dMEnTkpLqzM/sbILwL7QJgx9iX5hoEM8+bh6shgwd9mYJamdOnth7y2mMaFd08Glj9AXEFKllWcGYyBqnJsHrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.7.tgz", + "integrity": "sha512-enNqTJpojgISCG+a5acjnfJ02hcRTFUBVm8uXiCY40PRPZtSzFnNLskOTU+kDUUEODYma90syxuea1rRtUFh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.9.tgz", + "integrity": "sha512-2Vd5T7lTxfgJMpAd3aOu6bQpbUnCU7tBgvTQIZkP/7juJzmRmTM2t5XfOCuirFduWLTSJI/WZp32Cq1E01TVeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.6.tgz", + "integrity": "sha512-22LnGGCDm1M/pKXxxF8n6ppr/1IztdsxqeqeBRUMTTNnJTkjIBiAkewiDM4u/gjtazj/FvZS+nk9AbO/vEcBFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.5.tgz", + "integrity": "sha512-0v0wLQA4uhWbybmdH6oPl7ooxCiymNylkvi2gCSc+mA5ZQB6QoJLVhoHq12T82lhozpp9fpMJnJkC1nJrU/A+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.5", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.5.tgz", + "integrity": "sha512-l/IWwC+QhOyXgxkZloOh+n5quGzc1m5ZawyzBmoizCZaxbJeLRdS3pStoqg/JVInJRXaG5aHMhjK6H6oYiBC9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.8", + "@inquirer/confirm": "^6.3.5", + "@inquirer/editor": "^5.3.6", + "@inquirer/expand": "^5.1.8", + "@inquirer/input": "^5.1.9", + "@inquirer/number": "^4.2.6", + "@inquirer/password": "^5.2.5", + "@inquirer/rawlist": "^5.3.8", + "@inquirer/search": "^4.3.6", + "@inquirer/select": "^5.2.8" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.8.tgz", + "integrity": "sha512-h+6Y4pPvYvuRbksn1kIBdD9tCYD+E468W5sIcVfLmoLP5IasnY0r33N+r+46ej/P8b2PCREjyKR+MnnfPGdTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.6.tgz", + "integrity": "sha512-JZj9ttkIvuaFQIc2oxsyqC1ad05WL02ku6W/INVKTCzNaVq1KkhP3AfTRC0m7OWlKZyw+EWwPBFZfYiG9jF6Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.8.tgz", + "integrity": "sha512-BklkkAAOpjdCfuQw5fVGcDuhKxA7T1iBpn6b2JUvDDdkIsO3Ftlg+WnWUCVpkUBFagL2o036OFW+xL4lC9wIPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@napi-rs/cli": { + "version": "3.10.9", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.10.9.tgz", + "integrity": "sha512-RpzJrTDlSxUi2Deo+dh4bmDTKklAVe295aSQjZspwbZyUBG7twbXbiKskL2w/bL4kNg3DdkrwbI/iQwI9kgQaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^8.5.2", + "@napi-rs/cross-toolchain": "^1.0.3", + "@napi-rs/wasm-tools": "^1.1.0", + "@octokit/rest": "^22.0.1", + "clipanion": "^4.0.0-rc.4", + "colorette": "^2.0.20", + "es-toolkit": "^1.47.0", + "js-yaml": "^5.0.0", + "obug": "^3.0.0", + "semver": "^7.8.2", + "typanion": "^3.14.0", + "typescript": "^6.0.3" + }, + "bin": { + "napi": "dist/cli.js", + "napi-raw": "cli.mjs" + }, + "engines": { + "node": "^20.17.0 || ^22.13.0 || >= 23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4", + "emnapi": "^1.7.1 || ^2.0.0-alpha.4" + }, + "peerDependenciesMeta": { + "@emnapi/core": { + "optional": true + }, + "@emnapi/runtime": { + "optional": true + }, + "emnapi": { + "optional": true + } + } + }, + "node_modules/@napi-rs/cross-toolchain": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz", + "integrity": "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==", + "dev": true, + "license": "MIT", + "workspaces": [ + ".", + "arm64/*", + "x64/*" + ], + "dependencies": { + "@napi-rs/lzma": "^1.4.5", + "@napi-rs/tar": "^1.1.0", + "debug": "^4.4.1" + }, + "peerDependencies": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" + }, + "peerDependenciesMeta": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-x86_64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-x86_64": { + "optional": true + } + } + }, + "node_modules/@napi-rs/lzma": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.5.1.tgz", + "integrity": "sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.20 || ^24.12 || >=25" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/lzma-android-arm-eabi": "1.5.1", + "@napi-rs/lzma-android-arm64": "1.5.1", + "@napi-rs/lzma-darwin-arm64": "1.5.1", + "@napi-rs/lzma-darwin-x64": "1.5.1", + "@napi-rs/lzma-freebsd-x64": "1.5.1", + "@napi-rs/lzma-linux-arm-gnueabihf": "1.5.1", + "@napi-rs/lzma-linux-arm64-gnu": "1.5.1", + "@napi-rs/lzma-linux-arm64-musl": "1.5.1", + "@napi-rs/lzma-linux-ppc64-gnu": "1.5.1", + "@napi-rs/lzma-linux-riscv64-gnu": "1.5.1", + "@napi-rs/lzma-linux-s390x-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-musl": "1.5.1", + "@napi-rs/lzma-wasm32-wasi": "1.5.1", + "@napi-rs/lzma-win32-arm64-msvc": "1.5.1", + "@napi-rs/lzma-win32-ia32-msvc": "1.5.1", + "@napi-rs/lzma-win32-x64-msvc": "1.5.1" + } + }, + "node_modules/@napi-rs/lzma-android-arm-eabi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.5.1.tgz", + "integrity": "sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-android-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.5.1.tgz", + "integrity": "sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.5.1.tgz", + "integrity": "sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.5.1.tgz", + "integrity": "sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-freebsd-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.5.1.tgz", + "integrity": "sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.5.1.tgz", + "integrity": "sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.5.1.tgz", + "integrity": "sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.5.1.tgz", + "integrity": "sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-ppc64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.5.1.tgz", + "integrity": "sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-riscv64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.5.1.tgz", + "integrity": "sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-s390x-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.5.1.tgz", + "integrity": "sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.5.1.tgz", + "integrity": "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-wasm32-wasi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.5.1.tgz", + "integrity": "sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-arm64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.5.1.tgz", + "integrity": "sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-ia32-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.5.1.tgz", + "integrity": "sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-x64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.5.1.tgz", + "integrity": "sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/tar": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar/-/tar-1.1.1.tgz", + "integrity": "sha512-p6q2HhUc5vwH1CNwfOcrhLoxfgn8ust8Sqlfx+sA4VzAcp1cMbvbkl99tZZlDqOjCHgQNSiTfk/yWPjl/D42qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/tar-android-arm-eabi": "1.1.1", + "@napi-rs/tar-android-arm64": "1.1.1", + "@napi-rs/tar-darwin-arm64": "1.1.1", + "@napi-rs/tar-darwin-x64": "1.1.1", + "@napi-rs/tar-freebsd-x64": "1.1.1", + "@napi-rs/tar-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/tar-linux-arm64-gnu": "1.1.1", + "@napi-rs/tar-linux-arm64-musl": "1.1.1", + "@napi-rs/tar-linux-ppc64-gnu": "1.1.1", + "@napi-rs/tar-linux-s390x-gnu": "1.1.1", + "@napi-rs/tar-linux-x64-gnu": "1.1.1", + "@napi-rs/tar-linux-x64-musl": "1.1.1", + "@napi-rs/tar-wasm32-wasi": "1.1.1", + "@napi-rs/tar-win32-arm64-msvc": "1.1.1", + "@napi-rs/tar-win32-ia32-msvc": "1.1.1", + "@napi-rs/tar-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/tar-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-cAhnA10cSusAUbcE9HtjQY/tZ9BH/0w2sKtRcQc94TzIlnm7QSr1htJSd/PPrbWNPtrv1orXb2CkrHlVlbnlHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.1.tgz", + "integrity": "sha512-EslUWHCDBY/g5abTPBiHLsMaML4GagV0TXLm5WL9hAjx/DDtlxz9fegMb77RJ+f7nFLOIsUxF/3QWFvgOT0sMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-+A42/6ES5G9CQ35BOwzwA+WBjLID28r2jNPgc0dteD2hhClIhng0mva7D2ujUlXBNmgNOsr1LHn3stA4uTf4NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.1.tgz", + "integrity": "sha512-RYtE8w1dkEvj8hSJCDV5Jw0Rz2i13fsM7u893zv5O9n/4Ad5GNsw/f4RQ7/0YGSFaenkVxqPFrjmEvUHlKzsrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-rEepBvCJUwcuvUYkY83e8aot8RsR5Jcnal4PsG3tbWGKW1yAvcXhyMXf0fN6ZGpVRZFnB+FJqDyBxvsCPEXKhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-an1bJdfyhI5FpZYyTQ20mrqwR+a676i8GkaYc4Uy12dH/a7TJIfrK6Qa2Gm46arZvxUvx56qxoRKXbpOjUPvwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-w++Vtx36T2yHTKws7GVnmHHcUT1ybB59xLWSh9A8bwEpJVG4dG7Qub9mFe5cpcbfrJ+XP2mKKxC3oUJSunK3iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-Rh6UFhNtj3i4deJHOBINFIeRL0072mgbeyuK5rl1HokKnNoMKx8qKIZNEzBTTqpogMfDHWGvzyTQdnVxes5dpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-Cp+AxFbv9zcyAXtnzQi0OzmgDnQgy2w9D4Ubr+iwzMtVgJcztzcEoCcCrN1k2ATdEB01LX2Vb49IaocGOZhC9Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-ZyscC3SYKTBWyDRYjLOKAd5TyJ7q0KACRdQ8bWrb3rgrra1CCIJD66CsGTH6Dh0AVSdfLwZ8MfIIXU6+14BMjQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-LlIv+zg4fiOQge9LQX/ieBdRWE2fhVDjCTHxnunZkbugNmdhdelxWf1RpZb/6ZujWpNF4LPu4N/MW7ygg2oYAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-gZBeoKLjanOVj55qk4EMu13P2i9M0SuINmlGQkOxm1niIJofexzddHUYtqO5o/5QqtyL8lADmAcZplLILMLhHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-wasm32-wasi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.1.tgz", + "integrity": "sha512-rwtQ1Mdt/ft6g6I54fJzbUeLspl4yTwj6I3UJ6mitKnrN42soJkcDrdh3Y/FGvlpqZTad2YMQ96fGJl3EtAm2Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/tar-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-30PVp1AehRpfwxmv5wI4cg0yj3WmWBsZ+1QnLGnvEELu7Eu/+dhNU0nrmhI7VfPgLwSRK2eg9DQTB3tP7Wv9bA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-aI3/rmz+izUChiSeaPxcasAOxhf3FpJNuIHMXlxS/vpW+HIxUsSDR5+XV61PEG5DL4L/75iENVUxmSGM5l2yaw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-yJsB2IsrODQVLKbm2Fg1nHiVRbEj49mSPbj4x7JPZWJI0jGVPjohE2Sif0FBbx8OxsVoUODvS0BwksZZ8jl/OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz", + "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || 2.0.0-beta.2", + "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2" + } + }, + "node_modules/@napi-rs/wasm-tools": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools/-/wasm-tools-1.1.0.tgz", + "integrity": "sha512-VjHyKEqXAwYZK+HY7iJctYvRm3TFEbaQxeZwvAG1QRkoo1a39phMY8J6x9tUEqJI03W6MysB8F2jacI6wvcx+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.22.0" + }, + "optionalDependencies": { + "@napi-rs/wasm-tools-android-arm-eabi": "1.1.0", + "@napi-rs/wasm-tools-android-arm64": "1.1.0", + "@napi-rs/wasm-tools-darwin-arm64": "1.1.0", + "@napi-rs/wasm-tools-darwin-x64": "1.1.0", + "@napi-rs/wasm-tools-freebsd-x64": "1.1.0", + "@napi-rs/wasm-tools-linux-arm64-gnu": "1.1.0", + "@napi-rs/wasm-tools-linux-arm64-musl": "1.1.0", + "@napi-rs/wasm-tools-linux-x64-gnu": "1.1.0", + "@napi-rs/wasm-tools-linux-x64-musl": "1.1.0", + "@napi-rs/wasm-tools-wasm32-wasi": "1.1.0", + "@napi-rs/wasm-tools-win32-arm64-msvc": "1.1.0", + "@napi-rs/wasm-tools-win32-ia32-msvc": "1.1.0", + "@napi-rs/wasm-tools-win32-x64-msvc": "1.1.0" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm-eabi": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.1.0.tgz", + "integrity": "sha512-p6J8PB59I8d/XItXB/go5JH6nKW+xIbpzaL43EBTV0hi7mrS/Z4gs+MsB04ZrlqZN29BdZV8fChRyasuXLhRaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.1.0.tgz", + "integrity": "sha512-lWoKN3suypeBSCIRPIw+++sH9V2K6nQkhtdt1opu7XY3v9JwLs6Gw063HWRqkNjphlYpkd/Qy8XcfSPGbJj7nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-jfw5vyNDUf6oe0kP8lMveFN9U7cLk1cUosS7uMIfw/xmqmopYfKQ198DAx2g/6aEF7Tm+CqER2gpMpYKui30LA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.1.0.tgz", + "integrity": "sha512-R+pjeudAB7BYdH1vKkOJM61Tfv5jB6uXkxmFscYd+KKpdUpWBlNG+s4hr0w4i1rMBM91VhIAETZn2pz+MDHK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-freebsd-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.1.0.tgz", + "integrity": "sha512-hQJTe+aazrT++Vgm6I4lUd9099ItUCFYdd+aKg6Ys6nax6d/cZ1barDLTwA2lwOoVDsXMekJI/FOL6ZvVlIYBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.1.0.tgz", + "integrity": "sha512-1TAXJxUHsWGar90k3W/MknavvBMwOWzjh7Q6Spxo8twRcWJbBD5Kow/Q2KhhDq5hxh2sKGDXn3uLc1tdtz4WUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-musl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.1.0.tgz", + "integrity": "sha512-7rw3nlubTjNAVRH2LwphCxHy1b/N2/TerXocQ6XRn4Q+buaY1Z7P/hbdALy1i1ex2yfOU2Xcij7ib7ZLi/lKfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-gnu": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.1.0.tgz", + "integrity": "sha512-1sel0t9MRjI/tdT89M8Dd6gPfANeeFP24Xa46R11WeHNwhjsXXZh+xUk50uWCRTSGcaCy3ugm3AMK/lmHYQJkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-musl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.1.0.tgz", + "integrity": "sha512-o2jH5AMfor4EKF2HII1LBnMQxoWu7+usPifTEY8Zk6e9OiSi4EJkAXf9v3ANlX7TI2V/cUEV34OEW7r10GiVIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.1.0.tgz", + "integrity": "sha512-s6YDtDR1UWrsqJPtaxf+JLYLceWVyn3l8OpQYElHkDhf3Qfz9R6Ba3S0OgznTBv38L5/TIHysQ9Q4yO73Z0csg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-arm64-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.1.0.tgz", + "integrity": "sha512-x+NuxbG84VxU68tU8w7Rf5lSyq0l584M6dVlke5DTweHYFZoMyeqkpbwEq+qsyAX6ivfipK8xRsmFwamb5uDnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-ia32-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.1.0.tgz", + "integrity": "sha512-mdD96QDEp70SX67rXFTY6c725nVYeqEEjyDqzzbNh6u1APj7CI7IMNpMmvE75XbCRl4C2MHZVU4U6AWdAzvyQQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-x64-msvc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.1.0.tgz", + "integrity": "sha512-bVVjuvhlyVX++3eJXfDR63cXdw1ay5QYac6iq0MKQw8wZARInTM+bXCtByDT4fzVFI3+7ZthYb/ERWRdBNIqgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.22.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.8.tgz", + "integrity": "sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.5", + "@octokit/request": "^10.0.16", + "@octokit/request-error": "^7.1.2", + "@octokit/types": "^18.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.5.tgz", + "integrity": "sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^18.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.5.tgz", + "integrity": "sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.16", + "@octokit/types": "^18.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz", + "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.16", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.16.tgz", + "integrity": "sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.5", + "@octokit/request-error": "^7.1.2", + "@octokit/types": "^18.0.0", + "content-type": "^3.0.0", + "json-with-bigint": "^3.5.12", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.2.tgz", + "integrity": "sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^18.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz", + "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^29.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipanion": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz", + "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ], + "dependencies": { + "typanion": "^3.8.0" + }, + "peerDependencies": { + "typanion": "*" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-type": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-3.1.1.tgz", + "integrity": "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-toolkit": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.53.0.tgz", + "integrity": "sha512-3+AYXp9fmJ/UhHBMYj2/U+RaUouMD90SzoItyDOB2Za7W6dB7fQApFGnZPl2MF6K8/Tv1koh1JyirSx/kju6Pw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/js-yaml": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/json-with-bigint": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", + "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", + "dev": true, + "license": "MIT" + }, + "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/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typanion": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz", + "integrity": "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ] + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/compiler/package.json b/compiler/package.json new file mode 100644 index 0000000..3b62e40 --- /dev/null +++ b/compiler/package.json @@ -0,0 +1,66 @@ +{ + "name": "@reactivescript/compiler", + "description": "ReactiveScript's compiler, in process through Node-API, and its front end for @solidjs/vite-plugin", + "version": "0.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/ryansolid/reactivescript.git", + "directory": "compiler" + }, + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + }, + "./vite": { + "types": "./vite.d.mts", + "default": "./vite.mjs" + } + }, + "files": [ + "index.js", + "index.d.ts", + "vite.mjs", + "vite.d.mts", + "README.md" + ], + "scripts": { + "build": "napi build --platform --release --strip --features node --manifest-path ./Cargo.toml -- --lib", + "build:debug": "napi build --platform --features node --manifest-path ./Cargo.toml -- --lib", + "artifacts": "napi artifacts", + "create-npm-dirs": "napi create-npm-dirs", + "version": "napi version && node scripts/sync-optional-deps.mjs", + "test": "node test/node.mjs" + }, + "napi": { + "binaryName": "compiler", + "targets": [ + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc" + ] + }, + "peerDependencies": { + "@solidjs/vite-plugin": "*" + }, + "peerDependenciesMeta": { + "@solidjs/vite-plugin": { + "optional": true + } + }, + "devDependencies": { + "@napi-rs/cli": "^3.10.6" + }, + "optionalDependencies": { + "@reactivescript/compiler-darwin-x64": "0.0.0", + "@reactivescript/compiler-darwin-arm64": "0.0.0", + "@reactivescript/compiler-linux-x64-gnu": "0.0.0", + "@reactivescript/compiler-linux-arm64-gnu": "0.0.0", + "@reactivescript/compiler-win32-x64-msvc": "0.0.0" + } +} diff --git a/compiler/scripts/sync-optional-deps.mjs b/compiler/scripts/sync-optional-deps.mjs new file mode 100644 index 0000000..a74cf57 --- /dev/null +++ b/compiler/scripts/sync-optional-deps.mjs @@ -0,0 +1,14 @@ +// The platform packages (npm/*) at this package's version, and this package's optionalDependencies on them: what +// `npm run version` leaves before a release is published (.github/workflows/compiler.yml). +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; + +const read = f => JSON.parse(readFileSync(f, "utf8")); +const write = (f, j) => writeFileSync(f, JSON.stringify(j, null, 2) + "\n"); +const pkg = read("package.json"); +for (const dir of readdirSync("npm")) { + const p = read(`npm/${dir}/package.json`); + p.version = pkg.version; + write(`npm/${dir}/package.json`, p); + pkg.optionalDependencies[p.name] = pkg.version; +} +write("package.json", pkg); diff --git a/compiler/src/app.rs b/compiler/src/app.rs new file mode 100644 index 0000000..c86f213 --- /dev/null +++ b/compiler/src/app.rs @@ -0,0 +1,50 @@ +//! A program compiled for a bundler (spec/11 CP-4b): every module of the program `entry` starts, through Solid's +//! compiler for `target`, each importing its dependencies by `.rx` path, with the program's stylesheet. What `rxc app` +//! prints and the Node-API build (`src/node.rs`) returns to `@reactivescript/compiler/vite`. +use crate::check::check_modules; +use crate::emit::{compile_linked, lower_view, Target}; +use crate::modules::load_modules; +use serde_json::{json, Map, Value}; +use std::path::Path; + +pub fn read_disk(p: &str) -> Option { + if Path::new(p).is_file() { + std::fs::read_to_string(p).ok() + } else { + None + } +} + +pub fn target(name: &str) -> Target { + match name { + "ssr" => Target::Ssr, + "universal" => Target::Universal, + _ => Target::Dom, + } +} + +/// `{entry, css, modules: {path: {code, map}}}`, or `{diagnostics: [...]}` / `{error}`. `entry` is an absolute path. +pub fn app(entry: &str, target: Target, module_name: &str) -> Value { + // module keys are POSIX paths (`modules::join_path`); Windows takes `D:/a/b.rx` as it takes `D:\a\b.rx` + let entry = &entry.replace('\\', "/"); + (|| { + if read_disk(entry).is_none() { + return Err(json!({ "error": format!("{entry}: no such file") })); + } + let set = load_modules(entry, &read_disk).map_err(|e| json!({ "error": format!("{entry}: {}", e.0) }))?; + let checked = check_modules(&set); + if !checked.diags.is_empty() { + return Err(json!({ "diagnostics": checked.diags.iter().map(|d| { let mut j = d.to_json(); if j["file"].is_null() { j["file"] = json!(set.entry); } j }).collect::>() })); + } + let out = compile_linked(&set, &[], true).map_err(|e| json!({ "error": format!("{entry}: not compiled: {}", e.0) }))?; + let mut modules = Map::new(); + for (name, module) in &out.files { + let path = &out.paths[name]; + let text = read_disk(path).unwrap_or_default(); + let l = lower_view(module, path, target, module_name, Some((path, &text))).map_err(|e| json!({ "error": e }))?; + modules.insert(path.clone(), json!({ "code": l.code, "map": l.map })); + } + Ok(json!({ "entry": set.entry, "css": out.css, "modules": modules })) + })() + .unwrap_or_else(|e| e) +} diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs new file mode 100644 index 0000000..851b43f --- /dev/null +++ b/compiler/src/ast.rs @@ -0,0 +1,576 @@ +//! The abstract syntax (spec/02-core.md §1), as `ref/src/ast.ts` has it. Operators are spelled as in JS; types are TS's, +//! kept as text (07 §2). `to_json` writes a node the way `JSON.stringify` writes the reference's, so the two parsers are +//! diffed node for node (`ref/crate.ts`). + +use serde_json::{json, Map, Value}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Lit { + Undefined, + Null, + Bool(bool), + Num(f64), + Str(String), +} + +#[derive(Debug, Clone)] +pub enum TplPart { + Str(String), + Expr(Expr), +} + +#[derive(Debug, Clone)] +pub enum Field { + Kv { key: String, v: Expr }, + Spread(Expr), +} + +#[derive(Debug, Clone)] +pub enum Expr { + Lit(Lit), + Id(String), + Bin { op: String, l: Box, r: Box }, + Un { op: String, e: Box }, + Pre { name: String, init: Option> }, + Peek(Box), + /// R-1, R-7; `stream`: `await*` (ST-6b) + Await { e: Box, init: Option>, stream: bool }, + Latest(Box), + Pending(Box), + If { c: Box, t: Box, f: Box }, + Tpl(Vec), + Member { o: Box, p: String, opt: bool }, + Index { o: Box, i: Box, opt: bool }, + Call { f: Box, args: Vec }, + Arr(Vec), + Rec(Vec), + /// `stmts`: a statement-bodied arrow (imperative scope, 07 §5) + Fn { params: Vec, body: Box, stmts: Option> }, + /// ST-3 + Draft { seed: Box, body: Vec }, + /// RG-7b + Use(String), + Spread(Box), + /// R-f-3 + Throw(Box), + /// R-f-4: `e catch (x: A | B) => alt`; classes absent: every failure + Catch { e: Box, name: Option, classes: Option>, alt: Box }, + /// 07 CSS-5 + StyleMap { css: String, line: usize }, + /// 12 SF-1d + ServerFn { params: Vec, ptypes: Vec>, ret: Option, body: Vec }, +} + +#[derive(Debug, Clone)] +pub enum PathSeg { + Name(String), + Expr(Expr), +} + +#[derive(Debug, Clone)] +pub struct Target { + pub name: String, + pub path: Vec, + /// `use name := …` (RG-7, O-4). `None`: not written by this form (an `optimistic` target). + pub ctx: Option, +} +impl Target { + pub fn is_ctx(&self) -> bool { + self.ctx == Some(true) + } +} + +#[derive(Debug, Clone)] +pub enum Stmt { + Write { target: Target, e: Expr }, + Let { name: String, e: Expr, ty: Option }, + Expr(Expr), + Optimistic { target: Target, e: Expr }, + Refresh(String), + Affects(String), + Action(Vec), + Sif { c: Expr, t: Vec, f: Vec }, + Return(Option), + Try { body: Vec, catch_name: Option, catch_classes: Option>, handler: Option>, fin: Option> }, + Throw(Expr), +} +impl Stmt { + pub fn kind(&self) -> &'static str { + match self { + Stmt::Write { .. } => "write", + Stmt::Let { .. } => "let", + Stmt::Expr(_) => "expr", + Stmt::Optimistic { .. } => "optimistic", + Stmt::Refresh(_) => "refresh", + Stmt::Affects(_) => "affects", + Stmt::Action(_) => "action", + Stmt::Sif { .. } => "sif", + Stmt::Return(_) => "return", + Stmt::Try { .. } => "try", + Stmt::Throw(_) => "throw", + } + } +} + +#[derive(Debug, Clone)] +pub struct Param { + pub name: String, + pub r#ref: bool, + pub ty: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Side { + Server, + Client, +} +impl Side { + pub fn as_str(self) -> &'static str { + match self { + Side::Server => "server", + Side::Client => "client", + } + } +} + +#[derive(Debug, Clone)] +pub struct ImportName { + pub name: String, + pub alias: String, +} + +#[derive(Debug, Clone)] +pub enum Decl { + Source { name: String, init: Expr, line: usize, question: bool, ty: Option, exported: bool, key: Option }, + Eq { name: String, e: Expr, line: usize, deferred: bool, key: Option, ty: Option, exported: bool }, + Import { + names: Vec, + from: String, + pure: bool, + side: Option, + line: usize, + fails: Option>, + /// an `.rx` import's module key (MO-1, set by the loader) + resolved: Option, + /// a JS import's declaration file (MO-8) + declared: Option, + declared_errors: Option>, + declared_pure: Option>, + declared_side: Option>, + }, + Error { name: String, params: Vec, ptypes: Vec>, msg: Option, line: usize, exported: bool }, + TypeDecl { name: String, line: usize, text: String, exported: bool }, + Effect { inputs: Vec, body: Vec, line: usize }, + Fn { name: String, params: Vec, decls: Vec, tail: Expr, line: usize, ret: Option, exported: bool }, + Component { name: String, props: String, decls: Vec, view: Vec, line: usize, props_type: Option, exported: bool, is_default: bool }, + Action { name: String, params: Vec, ptypes: Vec>, body: Vec, line: usize, exported: bool }, + Server { name: String, params: Vec, ptypes: Vec>, ret: Option, body: Vec, line: usize, exported: bool }, + Provide { name: String, e: Expr, line: usize }, + Context { name: String, ty: Option, source: bool, dflt: Option, line: usize, exported: bool }, +} +impl Decl { + pub fn line(&self) -> usize { + match self { + Decl::Source { line, .. } + | Decl::Eq { line, .. } + | Decl::Import { line, .. } + | Decl::Error { line, .. } + | Decl::TypeDecl { line, .. } + | Decl::Effect { line, .. } + | Decl::Fn { line, .. } + | Decl::Component { line, .. } + | Decl::Action { line, .. } + | Decl::Server { line, .. } + | Decl::Provide { line, .. } + | Decl::Context { line, .. } => *line, + } + } + /// The name a declaration binds (`"name" in d`): every kind but `import` and `effect`. + pub fn name(&self) -> Option<&str> { + match self { + Decl::Source { name, .. } + | Decl::Eq { name, .. } + | Decl::Error { name, .. } + | Decl::TypeDecl { name, .. } + | Decl::Fn { name, .. } + | Decl::Component { name, .. } + | Decl::Action { name, .. } + | Decl::Server { name, .. } + | Decl::Provide { name, .. } + | Decl::Context { name, .. } => Some(name), + Decl::Import { .. } | Decl::Effect { .. } => None, + } + } + pub fn exported(&self) -> bool { + match self { + Decl::Source { exported, .. } + | Decl::Eq { exported, .. } + | Decl::Error { exported, .. } + | Decl::TypeDecl { exported, .. } + | Decl::Fn { exported, .. } + | Decl::Component { exported, .. } + | Decl::Action { exported, .. } + | Decl::Server { exported, .. } + | Decl::Context { exported, .. } => *exported, + _ => false, + } + } + pub fn set_exported(&mut self) { + match self { + Decl::Source { exported, .. } + | Decl::Eq { exported, .. } + | Decl::Error { exported, .. } + | Decl::TypeDecl { exported, .. } + | Decl::Fn { exported, .. } + | Decl::Component { exported, .. } + | Decl::Action { exported, .. } + | Decl::Server { exported, .. } + | Decl::Context { exported, .. } => *exported = true, + _ => {} + } + } +} + +#[derive(Debug, Clone)] +pub struct Attr { + pub name: String, + pub e: Expr, +} + +#[derive(Debug, Clone)] +pub enum MarkupKind { + El { tag: String, attrs: Vec, children: Vec }, + Read(Expr), + Text(String), + /// 07 §6b CSS-1 + Style(String), + Mif { c: Expr, t: Vec, t_decls: Vec, f: Vec, f_decls: Vec }, + Mfor { name: String, xs: Expr, decls: Vec, body: Vec }, + /// H-5/H-6 + Loading { on: Option, fallback: Vec, decls: Vec, body: Vec }, + /// H-12, R-f-4 + Errored { fallback: Vec, decls: Vec, body: Vec, err_name: Option, classes: Option> }, + /// RG-8 + Portal { to: Option, decls: Vec, body: Vec }, +} + +#[derive(Debug, Clone)] +pub struct Markup { + pub kind: MarkupKind, + pub line: Option, +} + +#[derive(Debug, Clone)] +pub struct Program { + pub decls: Vec, + pub view: Vec, + /// markup written at the top level (refused, TOP_LEVEL_MARKUP); `None` when the view is the default component's (MO-1c) + pub legacy_view: Option, +} + +// ---------------------------------------------------------------- JSON, as JSON.stringify writes the reference's AST + +fn obj(pairs: Vec<(&str, Option)>) -> Value { + let mut m = Map::new(); + for (k, v) in pairs { + if let Some(v) = v { + m.insert(k.to_string(), v); + } + } + Value::Object(m) +} +fn s(x: &str) -> Option { + Some(Value::String(x.to_string())) +} +fn os(x: &Option) -> Option { + x.as_ref().map(|x| Value::String(x.clone())) +} +fn strs(xs: &[String]) -> Value { + Value::Array(xs.iter().map(|x| Value::String(x.clone())).collect()) +} +fn ostrs(xs: &[Option]) -> Value { + Value::Array(xs.iter().map(|x| x.as_ref().map(|x| Value::String(x.clone())).unwrap_or(Value::Null)).collect()) +} +fn flag(b: bool) -> Option { + if b { Some(Value::Bool(true)) } else { None } +} +fn num(n: usize) -> Option { + Some(json!(n)) +} + +pub fn lit_json(l: &Lit) -> Option { + match l { + Lit::Undefined => None, + Lit::Null => Some(Value::Null), + Lit::Bool(b) => Some(Value::Bool(*b)), + Lit::Num(n) => Some(num_json(*n)), + Lit::Str(x) => Some(Value::String(x.clone())), + } +} +pub fn num_json(n: f64) -> Value { + if n.fract() == 0.0 && n.abs() < 9.0e15 { json!(n as i64) } else { json!(n) } +} + +impl Expr { + pub fn kind(&self) -> &'static str { + match self { + Expr::Lit(_) => "lit", + Expr::Id(_) => "id", + Expr::Bin { .. } => "bin", + Expr::Un { .. } => "un", + Expr::Pre { .. } => "pre", + Expr::Peek(_) => "peek", + Expr::Await { .. } => "await", + Expr::Latest(_) => "latest", + Expr::Pending(_) => "pending", + Expr::If { .. } => "if", + Expr::Tpl(_) => "tpl", + Expr::Member { .. } => "member", + Expr::Index { .. } => "index", + Expr::Call { .. } => "call", + Expr::Arr(_) => "arr", + Expr::Rec(_) => "rec", + Expr::Fn { .. } => "fn", + Expr::Draft { .. } => "draft", + Expr::Use(_) => "use", + Expr::Spread(_) => "spread", + Expr::Throw(_) => "throw", + Expr::Catch { .. } => "catch", + Expr::StyleMap { .. } => "stylemap", + Expr::ServerFn { .. } => "serverfn", + } + } + pub fn to_json(&self) -> Value { + let k = s(self.kind()); + let b = |e: &Expr| Some(e.to_json()); + let ob = |e: &Option>| e.as_ref().map(|e| e.to_json()); + match self { + Expr::Lit(l) => obj(vec![("k", k), ("v", lit_json(l))]), + Expr::Id(n) => obj(vec![("k", k), ("name", s(n))]), + Expr::Bin { op, l, r } => obj(vec![("k", k), ("op", s(op)), ("l", b(l)), ("r", b(r))]), + Expr::Un { op, e } => obj(vec![("k", k), ("op", s(op)), ("e", b(e))]), + Expr::Pre { name, init } => obj(vec![("k", k), ("name", s(name)), ("init", ob(init))]), + Expr::Peek(e) | Expr::Latest(e) | Expr::Pending(e) | Expr::Spread(e) | Expr::Throw(e) => obj(vec![("k", k), ("e", b(e))]), + Expr::Await { e, init, stream } => obj(vec![("k", k), ("e", b(e)), ("init", ob(init)), ("stream", Some(Value::Bool(*stream)))]), + Expr::If { c, t, f } => obj(vec![("k", k), ("c", b(c)), ("t", b(t)), ("f", b(f))]), + Expr::Tpl(parts) => obj(vec![ + ("k", k), + ("parts", Some(Value::Array(parts.iter().map(|p| match p { TplPart::Str(x) => Value::String(x.clone()), TplPart::Expr(e) => e.to_json() }).collect()))), + ]), + Expr::Member { o, p, opt } => obj(vec![("k", k), ("o", b(o)), ("p", s(p)), ("opt", flag(*opt))]), + Expr::Index { o, i, opt } => obj(vec![("k", k), ("o", b(o)), ("i", b(i)), ("opt", flag(*opt))]), + Expr::Call { f, args } => obj(vec![("k", k), ("f", b(f)), ("args", Some(Value::Array(args.iter().map(|a| a.to_json()).collect())))]), + Expr::Arr(items) => obj(vec![("k", k), ("items", Some(Value::Array(items.iter().map(|a| a.to_json()).collect())))]), + Expr::Rec(fields) => obj(vec![ + ("k", k), + ( + "fields", + Some(Value::Array( + fields + .iter() + .map(|f| match f { + Field::Kv { key, v } => obj(vec![("key", s(key)), ("v", Some(v.to_json()))]), + Field::Spread(e) => obj(vec![("spread", Some(e.to_json()))]), + }) + .collect(), + )), + ), + ]), + Expr::Fn { params, body, stmts } => obj(vec![("k", k), ("params", Some(strs(params))), ("body", b(body)), ("stmts", stmts.as_ref().map(|ss| stmts_json(ss)))]), + Expr::Draft { seed, body } => obj(vec![("k", k), ("seed", b(seed)), ("body", Some(stmts_json(body)))]), + Expr::Use(n) => obj(vec![("k", k), ("name", s(n))]), + Expr::Catch { e, name, classes, alt } => obj(vec![("k", k), ("e", b(e)), ("name", os(name)), ("classes", classes.as_ref().map(|c| strs(c))), ("alt", b(alt))]), + Expr::StyleMap { css, line } => obj(vec![("k", k), ("css", s(css)), ("line", num(*line))]), + Expr::ServerFn { params, ptypes, ret, body } => obj(vec![("k", k), ("params", Some(strs(params))), ("ptypes", Some(ostrs(ptypes))), ("ret", os(ret)), ("body", Some(stmts_json(body)))]), + } + } +} + +pub fn stmts_json(ss: &[Stmt]) -> Value { + Value::Array(ss.iter().map(|s| s.to_json()).collect()) +} +fn target_json(t: &Target) -> Value { + obj(vec![ + ("name", s(&t.name)), + ("path", Some(Value::Array(t.path.iter().map(|p| match p { PathSeg::Name(n) => Value::String(n.clone()), PathSeg::Expr(e) => e.to_json() }).collect()))), + ("ctx", t.ctx.map(Value::Bool)), + ]) +} +impl Stmt { + pub fn to_json(&self) -> Value { + let k = s(self.kind()); + match self { + Stmt::Write { target, e } | Stmt::Optimistic { target, e } => obj(vec![("k", k), ("target", Some(target_json(target))), ("e", Some(e.to_json()))]), + Stmt::Let { name, e, ty } => obj(vec![("k", k), ("name", s(name)), ("e", Some(e.to_json())), ("type", os(ty))]), + Stmt::Expr(e) | Stmt::Throw(e) => obj(vec![("k", k), ("e", Some(e.to_json()))]), + Stmt::Refresh(n) | Stmt::Affects(n) => obj(vec![("k", k), ("name", s(n))]), + Stmt::Action(body) => obj(vec![("k", k), ("body", Some(stmts_json(body)))]), + Stmt::Sif { c, t, f } => obj(vec![("k", k), ("c", Some(c.to_json())), ("t", Some(stmts_json(t))), ("f", Some(stmts_json(f)))]), + Stmt::Return(e) => obj(vec![("k", k), ("e", e.as_ref().map(|e| e.to_json()))]), + Stmt::Try { body, catch_name, catch_classes, handler, fin } => obj(vec![ + ("k", k), + ("body", Some(stmts_json(body))), + ("catchName", os(catch_name)), + ("catchClasses", catch_classes.as_ref().map(|c| strs(c))), + ("handler", handler.as_ref().map(|h| stmts_json(h))), + ("fin", fin.as_ref().map(|h| stmts_json(h))), + ]), + } + } +} + +pub fn decls_json(ds: &[Decl]) -> Value { + Value::Array(ds.iter().map(|d| d.to_json()).collect()) +} +pub fn markups_json(ms: &[Markup]) -> Value { + Value::Array(ms.iter().map(|m| m.to_json()).collect()) +} +impl Decl { + pub fn to_json(&self) -> Value { + match self { + Decl::Source { name, init, line, question, ty, exported, key } => obj(vec![ + ("k", s("source")), + ("name", s(name)), + ("init", Some(init.to_json())), + ("line", num(*line)), + ("question", flag(*question)), + ("type", os(ty)), + ("key", os(key)), + ("exported", flag(*exported)), + ]), + Decl::Eq { name, e, line, deferred, key, ty, exported } => obj(vec![ + ("k", s("eq")), + ("name", s(name)), + ("e", Some(e.to_json())), + ("line", num(*line)), + ("deferred", flag(*deferred)), + ("key", os(key)), + ("type", os(ty)), + ("exported", flag(*exported)), + ]), + Decl::Import { names, from, pure, side, line, fails, resolved, declared, declared_errors, declared_pure, declared_side } => obj(vec![ + ("k", s("import")), + ("names", Some(Value::Array(names.iter().map(|n| obj(vec![("name", s(&n.name)), ("alias", s(&n.alias))])).collect()))), + ("from", s(from)), + ("pure", Some(Value::Bool(*pure))), + ("line", num(*line)), + ("fails", fails.as_ref().map(|f| strs(f))), + ("side", side.map(|x| Value::String(x.as_str().into()))), + ("resolved", os(resolved)), + ("declared", os(declared)), + ("declaredErrors", declared_errors.as_ref().map(|f| strs(f))), + ("declaredPure", declared_pure.as_ref().map(|f| strs(f))), + ( + "declaredSide", + declared_side.as_ref().map(|xs| { + let mut m = Map::new(); + for (n, sd) in xs { + m.insert(n.clone(), Value::String(sd.as_str().into())); + } + Value::Object(m) + }), + ), + ]), + Decl::Error { name, params, ptypes, msg, line, exported } => obj(vec![ + ("k", s("error")), + ("name", s(name)), + ("params", Some(strs(params))), + ("ptypes", Some(ostrs(ptypes))), + ("msg", msg.as_ref().map(|m| m.to_json())), + ("line", num(*line)), + ("exported", flag(*exported)), + ]), + Decl::TypeDecl { name, line, text, exported } => obj(vec![("k", s("typedecl")), ("name", s(name)), ("line", num(*line)), ("text", s(text)), ("exported", flag(*exported))]), + Decl::Effect { inputs, body, line } => obj(vec![("k", s("effect")), ("inputs", Some(Value::Array(inputs.iter().map(|e| e.to_json()).collect()))), ("body", Some(stmts_json(body))), ("line", num(*line))]), + Decl::Fn { name, params, decls, tail, line, ret, exported } => obj(vec![ + ("k", s("fn")), + ("name", s(name)), + ("params", Some(Value::Array(params.iter().map(|p| obj(vec![("name", s(&p.name)), ("ref", Some(Value::Bool(p.r#ref))), ("type", os(&p.ty))])).collect()))), + ("decls", Some(decls_json(decls))), + ("tail", Some(tail.to_json())), + ("line", num(*line)), + ("ret", os(ret)), + ("exported", flag(*exported)), + ]), + Decl::Component { name, props, decls, view, line, props_type, exported, is_default } => obj(vec![ + ("k", s("component")), + ("name", s(name)), + ("props", s(props)), + ("decls", Some(decls_json(decls))), + ("view", Some(markups_json(view))), + ("line", num(*line)), + ("propsType", os(props_type)), + ("exported", flag(*exported)), + ("isDefault", flag(*is_default)), + ]), + Decl::Action { name, params, ptypes, body, line, exported } => obj(vec![ + ("k", s("action")), + ("name", s(name)), + ("params", Some(strs(params))), + ("ptypes", Some(ostrs(ptypes))), + ("body", Some(stmts_json(body))), + ("line", num(*line)), + ("exported", flag(*exported)), + ]), + Decl::Server { name, params, ptypes, ret, body, line, exported } => obj(vec![ + ("k", s("server")), + ("name", s(name)), + ("params", Some(strs(params))), + ("ptypes", Some(ostrs(ptypes))), + ("ret", os(ret)), + ("body", Some(stmts_json(body))), + ("line", num(*line)), + ("exported", flag(*exported)), + ]), + Decl::Provide { name, e, line } => obj(vec![("k", s("provide")), ("name", s(name)), ("e", Some(e.to_json())), ("line", num(*line))]), + Decl::Context { name, ty, source, dflt, line, exported } => obj(vec![ + ("k", s("context")), + ("name", s(name)), + ("type", os(ty)), + ("source", Some(Value::Bool(*source))), + ("dflt", dflt.as_ref().map(|e| e.to_json())), + ("line", num(*line)), + ("exported", flag(*exported)), + ]), + } + } +} + +impl Markup { + pub fn to_json(&self) -> Value { + let line = self.line.map(|l| json!(l)); + let ms = |x: &Vec| Some(markups_json(x)); + let ds = |x: &Vec| Some(decls_json(x)); + match &self.kind { + MarkupKind::El { tag, attrs, children } => obj(vec![ + ("k", s("el")), + ("tag", s(tag)), + ("attrs", Some(Value::Array(attrs.iter().map(|a| obj(vec![("name", s(&a.name)), ("e", Some(a.e.to_json()))])).collect()))), + ("children", ms(children)), + ("line", line), + ]), + MarkupKind::Read(e) => obj(vec![("k", s("read")), ("e", Some(e.to_json())), ("line", line)]), + MarkupKind::Text(x) => obj(vec![("k", s("text")), ("s", s(x)), ("line", line)]), + MarkupKind::Style(css) => obj(vec![("k", s("style")), ("css", s(css)), ("line", line)]), + MarkupKind::Mif { c, t, t_decls, f, f_decls } => obj(vec![("k", s("mif")), ("c", Some(c.to_json())), ("t", ms(t)), ("tDecls", ds(t_decls)), ("f", ms(f)), ("fDecls", ds(f_decls)), ("line", line)]), + MarkupKind::Mfor { name, xs, decls, body } => obj(vec![("k", s("mfor")), ("name", s(name)), ("xs", Some(xs.to_json())), ("decls", ds(decls)), ("body", ms(body)), ("line", line)]), + MarkupKind::Loading { on, fallback, decls, body } => obj(vec![("k", s("loading")), ("on", on.as_ref().map(|e| e.to_json())), ("fallback", ms(fallback)), ("decls", ds(decls)), ("body", ms(body)), ("line", line)]), + MarkupKind::Errored { fallback, decls, body, err_name, classes } => obj(vec![ + ("k", s("errored")), + ("fallback", ms(fallback)), + ("decls", ds(decls)), + ("body", ms(body)), + ("errName", os(err_name)), + ("classes", classes.as_ref().map(|c| strs(c))), + ("line", line), + ]), + MarkupKind::Portal { to, decls, body } => obj(vec![("k", s("portal")), ("to", to.as_ref().map(|e| e.to_json())), ("decls", ds(decls)), ("body", ms(body)), ("line", line)]), + } + } +} + +impl Program { + pub fn to_json(&self) -> Value { + obj(vec![("decls", Some(decls_json(&self.decls))), ("view", Some(markups_json(&self.view))), ("legacyView", self.legacy_view.map(Value::Bool))]) + } +} diff --git a/compiler/src/awaits.rs b/compiler/src/awaits.rs new file mode 100644 index 0000000..a04ea47 --- /dev/null +++ b/compiler/src/awaits.rs @@ -0,0 +1,108 @@ +//! R-1 / 07 §3 (`ref/src/awaits.ts`): the awaits of an equation that are not its result, each with the conditions under +//! which the equation reaches it (E-11: an arm not selected is not demanded, so its flight is not asked). The compiler +//! hoists each into its own async node, asked only while its guards hold; the equation reads it where the `await` was. + +use crate::ast::{Expr, Markup, MarkupKind}; +use crate::check::{children, Node}; + +#[derive(Clone, Copy)] +pub struct Guard<'a> { + pub e: &'a Expr, + pub neg: bool, + pub nullish: bool, +} +pub struct Hoisted<'a> { + /// an `Expr::Await` + pub node: &'a Expr, + pub guards: Vec>, +} + +fn with<'a>(g: &[Guard<'a>], x: Guard<'a>) -> Vec> { + let mut v = g.to_vec(); + v.push(x); + v +} + +/// `tail = false`: every await of `root` — a reader of a view, an effect's input or a provision, whose expression is not an +/// equation's own memo, so even its result-position `await` is a flight of its own (07 §3). +pub fn non_tail_awaits(root: &Expr, tail: bool) -> Vec> { + let mut out = vec![]; + walk(Node::E(root), tail, &[], &mut out); + out +} + +fn walk<'a>(n: Node<'a>, tail: bool, g: &[Guard<'a>], out: &mut Vec>) { + let g0 = |e| Guard { e, neg: false, nullish: false }; + if let Node::E(x) = n { + match x { + Expr::Await { e, init, .. } => { + if !tail { + out.push(Hoisted { node: x, guards: g.to_vec() }); + } + walk(Node::E(e), false, g, out); + if let Some(i) = init { + walk(Node::E(i), false, g, out); + } + return; + } + Expr::If { c, t, f } => { + walk(Node::E(c), false, g, out); + walk(Node::E(t), tail, &with(g, g0(c)), out); + walk(Node::E(f), tail, &with(g, Guard { e: c, neg: true, nullish: false }), out); + return; + } + Expr::Bin { op, l, r } => { + walk(Node::E(l), false, g, out); + match op.as_str() { + "&&" => walk(Node::E(r), false, &with(g, g0(l)), out), + "||" => walk(Node::E(r), false, &with(g, Guard { e: l, neg: true, nullish: false }), out), + "??" => walk(Node::E(r), false, &with(g, Guard { e: l, neg: false, nullish: true }), out), + _ => walk(Node::E(r), false, g, out), + } + return; + } + Expr::Catch { e, alt, .. } => { + walk(Node::E(e), tail, g, out); + walk(Node::E(alt), false, g, out); + return; + } + // a closure's body runs later; a draft's statements are its own + Expr::Fn { .. } | Expr::Draft { .. } => return, + _ => {} + } + } + for c in children(n, false) { + walk(c, false, g, out); + } +} + +/// 07 §3: the expressions a region's own view reads — readers, attributes (not handlers), `if` conditions, `for` collections, +/// `loading on(…)` and fallbacks, `portal to(…)` — but not its sub-regions', which are hoisted where they are set up. An +/// `errored` fallback binds its error, so it stays out. +pub fn view_readers(ms: &[Markup]) -> Vec<&Expr> { + let mut out = vec![]; + for m in ms { + match &m.kind { + MarkupKind::Read(e) => out.push(e), + MarkupKind::El { attrs, children, .. } => { + out.extend(attrs.iter().filter(|a| !is_handler(&a.name)).map(|a| &a.e)); + out.extend(view_readers(children)); + } + MarkupKind::Mif { c, .. } => out.push(c), + MarkupKind::Mfor { xs, .. } => out.push(xs), + MarkupKind::Loading { on, fallback, .. } => { + out.extend(on.iter()); + out.extend(view_readers(fallback)); + } + MarkupKind::Portal { to, .. } => out.extend(to.iter()), + _ => {} + } + } + out +} + +/// `/^on[A-Z:]/` +pub fn is_handler(name: &str) -> bool { + let b = name.as_bytes(); + b.len() > 2 && b[0] == b'o' && b[1] == b'n' && (b[2].is_ascii_uppercase() || b[2] == b':') +} diff --git a/compiler/src/check.rs b/compiler/src/check.rs new file mode 100644 index 0000000..caa6546 --- /dev/null +++ b/compiler/src/check.rs @@ -0,0 +1,3410 @@ +//! The checker: spec/08-constraints.md, ported from `ref/src/check.ts`. Every diagnostic is an error (CN-1a); codes keep +//! Solid's names where one exists (CN-1b); `rule` is the spec rule enforced. Names resolve lexically (RG-7a); provisions +//! resolve through the static application tree (RG-7b). The walk also records the resolved model (CP-6, `model.rs`). +//! +//! The port keeps the reference's structure — a walk that records readers, applications, actions and provisions, lazy row +//! and failure thunks over symbols, and a resolution over the application tree after the walk — so that its verdicts, +//! messages and their order are the reference's (`ref/crate.ts` diffs them on every scenario program). + +use crate::ast::*; +use crate::content::{misnested, misnested_inside, Shape, P_CLOSERS, P_SCOPE}; +use crate::model::{ModelBuilder, ModelSymbol}; +use crate::modules::ModuleSet; +use crate::parse::{js_trim, JsDecl}; +use crate::signature::{Catch, CompSig, ExportSig, ModSig, Own, PropSig, SigParam, HOLD}; +use indexmap::{IndexMap, IndexSet}; +use regex::Regex; +use std::collections::{HashMap, HashSet}; +use std::sync::LazyLock; + +#[derive(Debug, Clone, PartialEq)] +pub struct Diagnostic { + pub code: String, + pub rule: String, + pub line: i64, + pub msg: String, + pub file: Option, +} +impl Diagnostic { + pub fn to_json(&self) -> serde_json::Value { + let mut m = serde_json::Map::new(); + m.insert("code".into(), self.code.clone().into()); + m.insert("rule".into(), self.rule.clone().into()); + m.insert("line".into(), self.line.into()); + m.insert("msg".into(), self.msg.clone().into()); + if let Some(f) = &self.file { + m.insert("file".into(), f.clone().into()); + } + serde_json::Value::Object(m) + } +} + +const BUILTINS: &[&str] = &["len", "str", "map", "filter", "sum", "fail", "fetch", "every"]; +fn is_handler(attr: &str) -> bool { + let b = attr.as_bytes(); + b.len() > 2 && b[0] == b'o' && b[1] == b'n' && (b[2].is_ascii_uppercase() || b[2] == b':') +} +fn is_upper_tag(tag: &str) -> bool { + tag.as_bytes().first().is_some_and(|c| c.is_ascii_uppercase()) +} +static ROW_WORDS: LazyLock = LazyLock::new(|| Regex::new(r"^((?:io|pending|lane|fails\([^)]*\))\s+)+").unwrap()); +static DOLLAR_PARAM: LazyLock = LazyLock::new(|| Regex::new(r"^\$\d+$").unwrap()); +static USE_JSON: LazyLock = LazyLock::new(|| Regex::new(r#""k":"use","name":"([^"]+)""#).unwrap()); +static AT_SUFFIX: LazyLock = LazyLock::new(|| Regex::new(r"@\d+$").unwrap()); + +/// `s.split(/\s+(?![^(]*\))/)`: whitespace runs, except those inside a parenthesis. +fn split_rows(s: &str) -> Vec { + let mut out = vec![]; + let mut cur = String::new(); + let chars: Vec<(usize, char)> = s.char_indices().collect(); + let mut k = 0; + while k < chars.len() { + let (_, c) = chars[k]; + if c.is_whitespace() { + let mut j = k; + while j < chars.len() && chars[j].1.is_whitespace() { + j += 1; + } + let after = if j < chars.len() { &s[chars[j].0..] } else { "" }; + let inside = match after.find(['(', ')']) { + Some(i) => after.as_bytes()[i] == b')', + None => false, + }; + if inside { + for (_, c) in &chars[k..j] { + cur.push(*c); + } + } else { + out.push(std::mem::take(&mut cur)); + } + k = j; + } else { + cur.push(c); + k += 1; + } + } + out.push(cur); + out +} + +/// A context key (`file#name`, `@n` for one declared in a block) → its name. +pub fn key_name(k: &str) -> String { + let last = k.split('#').last().unwrap_or(""); + AT_SUFFIX.replace(last, "").to_string() +} + +type RId = usize; +type SId = usize; +type EId = usize; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SymKind { + Source, + Eq, + Props, + Value, + Fn, + Action, + Comp, + Import, + ErrClass, + Context, + Server, +} +impl SymKind { + pub fn as_str(self) -> &'static str { + match self { + SymKind::Source => "source", + SymKind::Eq => "eq", + SymKind::Props => "props", + SymKind::Value => "value", + SymKind::Fn => "fn", + SymKind::Action => "action", + SymKind::Comp => "comp", + SymKind::Import => "import", + SymKind::ErrClass => "errclass", + SymKind::Context => "context", + SymKind::Server => "server", + } + } + fn reactive(self) -> bool { + matches!(self, SymKind::Source | SymKind::Eq | SymKind::Props) + } +} + +struct Region { + id: usize, + depth: i64, + parent: Option, + component: Option, + is_component_root: bool, + provides: IndexSet, + catches: Option, + /// children passed to a component: (the component's root, the site) + kids: Option<(RId, RId)>, + sig: Option, +} + +#[derive(Clone)] +struct PropRef { + comp: RId, + field: String, + pre: Vec, +} +#[derive(Clone, Default)] +struct Fails { + cls: IndexSet, + props: Vec, +} +impl Fails { + fn add(&mut self, b: Fails) { + for x in b.cls { + self.cls.insert(x); + } + self.props.extend(b.props); + } +} + +enum RowsT<'a> { + Expr { e: &'a Expr, env: EId, cache: Option>, busy: bool }, + Fixed(IndexSet), +} +enum FailsT<'a> { + Expr { e: &'a Expr, env: EId, cache: Option, busy: bool }, + Fixed(IndexSet), +} +enum ServerRejects<'a> { + Body { body: &'a [Stmt], env: EId, cache: Option> }, + Fixed(IndexSet), +} + +#[derive(Clone)] +struct CtxInfo { + key: String, + source: bool, + dflt: bool, + contract: Option<(IndexSet, IndexSet)>, +} + +struct Sym<'a> { + kind: SymKind, + name: String, + region: RId, + line: i64, + question: bool, + io: bool, + instantiating: bool, + params: Option>, + captures: Vec, + maps_records: bool, + rows: Option>, + comp: Option<&'a Decl>, + expr: Option<(&'a Expr, EId)>, + comp_region: Option, + comp_env: Option, + fails_t: Option>, + fn_tail: Option<(&'a Expr, EId)>, + rejects: Option>, + imported: Option, + ctx: Option, + side: Option, + io_via: Option, + server_rejects: Option>, + fn_decl: Option<&'a Decl>, + decl_env: Option, +} +impl<'a> Sym<'a> { + fn new(kind: SymKind, name: &str, region: RId, line: i64) -> Self { + Sym { + kind, + name: name.to_string(), + region, + line, + question: false, + io: false, + instantiating: false, + params: None, + captures: vec![], + maps_records: false, + rows: None, + comp: None, + expr: None, + comp_region: None, + comp_env: None, + fails_t: None, + fn_tail: None, + rejects: None, + imported: None, + ctx: None, + side: None, + io_via: None, + server_rejects: None, + fn_decl: None, + decl_env: None, + } + } +} + +struct Env { + vars: IndexMap, + parent: Option, + region: RId, +} + +#[derive(Clone, Copy, Default)] +struct Ctx { + imperative: bool, + in_fn: bool, + in_action: bool, + server: bool, +} +impl Ctx { + fn reactive() -> Ctx { + Ctx::default() + } + fn imperative() -> Ctx { + Ctx { imperative: true, ..Ctx::default() } + } +} + +/// A node the reference walks generically (`Object.values`): an expression, or a statement inside one. +#[derive(Clone, Copy)] +pub(crate) enum Node<'a> { + E(&'a Expr), + S(&'a Stmt), +} +/// The children of a node in the order `Object.values` gives them; `paths`: also a write's target path (an object without +/// `k`, which only a walk over every value reaches). +pub(crate) fn children<'a>(n: Node<'a>, paths: bool) -> Vec> { + let mut out = vec![]; + let ss = |out: &mut Vec>, ss: &'a [Stmt]| out.extend(ss.iter().map(Node::S)); + match n { + Node::E(e) => match e { + Expr::Lit(_) | Expr::Id(_) | Expr::Use(_) | Expr::StyleMap { .. } => {} + Expr::Bin { l, r, .. } => out.extend([Node::E(l), Node::E(r)]), + Expr::Un { e, .. } | Expr::Peek(e) | Expr::Latest(e) | Expr::Pending(e) | Expr::Spread(e) | Expr::Throw(e) => out.push(Node::E(e)), + Expr::Pre { init, .. } => out.extend(init.iter().map(|x| Node::E(x))), + Expr::Await { e, init, .. } => { + out.push(Node::E(e)); + out.extend(init.iter().map(|x| Node::E(x))); + } + Expr::If { c, t, f } => out.extend([Node::E(c), Node::E(t), Node::E(f)]), + Expr::Tpl(parts) => out.extend(parts.iter().filter_map(|p| if let TplPart::Expr(e) = p { Some(Node::E(e)) } else { None })), + Expr::Member { o, .. } => out.push(Node::E(o)), + Expr::Index { o, i, .. } => out.extend([Node::E(o), Node::E(i)]), + Expr::Call { f, args } => { + out.push(Node::E(f)); + out.extend(args.iter().map(Node::E)); + } + Expr::Arr(items) => out.extend(items.iter().map(Node::E)), + Expr::Rec(fields) => out.extend(fields.iter().map(|f| match f { + Field::Kv { v, .. } => Node::E(v), + Field::Spread(e) => Node::E(e), + })), + Expr::Fn { body, stmts, .. } => { + out.push(Node::E(body)); + if let Some(s) = stmts { + ss(&mut out, s); + } + } + Expr::Draft { seed, body } => { + out.push(Node::E(seed)); + ss(&mut out, body); + } + Expr::Catch { e, alt, .. } => out.extend([Node::E(e), Node::E(alt)]), + Expr::ServerFn { body, .. } => ss(&mut out, body), + }, + Node::S(s) => match s { + Stmt::Write { target, e } | Stmt::Optimistic { target, e } => { + if paths { + out.extend(target.path.iter().filter_map(|p| if let PathSeg::Expr(x) = p { Some(Node::E(x)) } else { None })); + } + out.push(Node::E(e)); + } + Stmt::Let { e, .. } | Stmt::Expr(e) | Stmt::Throw(e) => out.push(Node::E(e)), + Stmt::Refresh(_) | Stmt::Affects(_) => {} + Stmt::Action(body) => ss(&mut out, body), + Stmt::Sif { c, t, f } => { + out.push(Node::E(c)); + ss(&mut out, t); + ss(&mut out, f); + } + Stmt::Return(e) => out.extend(e.iter().map(Node::E)), + Stmt::Try { body, handler, fin, .. } => { + ss(&mut out, body); + if let Some(h) = handler { + ss(&mut out, h); + } + if let Some(f) = fin { + ss(&mut out, f); + } + } + }, + } + out +} + +// ---------------------------------------------------------------- free names +/// Free identifiers of an expression (`pre:x` for `pre x`), minus names bound inside it. +fn free_names(e: &Expr, bound: &HashSet, out: &mut IndexSet) { + let go = |x: &Expr, out: &mut IndexSet| free_names(x, bound, out); + match e { + Expr::Lit(_) | Expr::Use(_) => {} + Expr::Id(n) => { + if !bound.contains(n) { + out.insert(n.clone()); + } + } + Expr::Pre { name, .. } => { + if !bound.contains(name) { + out.insert(format!("pre:{}", name)); + } + } + Expr::Peek(x) => go(x, out), + Expr::Await { e, init, .. } => { + go(e, out); + if let Some(i) = init { + go(i, out); + } + } + Expr::Latest(x) | Expr::Pending(x) | Expr::Un { e: x, .. } | Expr::Spread(x) => go(x, out), + Expr::Bin { l, r, .. } => { + go(l, out); + go(r, out); + } + Expr::If { c, t, f } => { + go(c, out); + go(t, out); + go(f, out); + } + Expr::Tpl(parts) => { + for p in parts { + if let TplPart::Expr(x) = p { + go(x, out); + } + } + } + Expr::Member { o, .. } => go(o, out), + Expr::Index { o, i, .. } => { + go(o, out); + go(i, out); + } + Expr::Call { f, args } => { + go(f, out); + for a in args { + go(a, out); + } + } + Expr::Arr(items) => { + for a in items { + go(a, out); + } + } + Expr::Rec(fields) => { + for f in fields { + match f { + Field::Kv { v, .. } => go(v, out), + Field::Spread(x) => go(x, out), + } + } + } + Expr::Fn { params, body, stmts } => { + let mut b = bound.clone(); + b.extend(params.iter().cloned()); + free_names(body, &b, out); + if let Some(ss) = stmts { + free_names_stmts(ss, &b, out); + } + } + Expr::Draft { seed, body } => { + go(seed, out); + let mut b = bound.clone(); + b.insert("d".into()); + free_names_stmts(body, &b, out); + } + Expr::Throw(x) => go(x, out), + Expr::Catch { e, name, alt, .. } => { + go(e, out); + match name { + Some(n) => { + let mut b = bound.clone(); + b.insert(n.clone()); + free_names(alt, &b, out); + } + None => free_names(alt, bound, out), + } + } + Expr::StyleMap { .. } | Expr::ServerFn { .. } => {} + } +} +fn free_names_stmts(ss: &[Stmt], bound0: &HashSet, out: &mut IndexSet) { + let mut bound = bound0.clone(); + for s in ss { + match s { + Stmt::Let { name, e, .. } => { + free_names(e, &bound, out); + bound.insert(name.clone()); + } + Stmt::Write { target, e } | Stmt::Optimistic { target, e } => { + if !target.is_ctx() && !bound.contains(&target.name) { + out.insert(target.name.clone()); + } + for p in &target.path { + if let PathSeg::Expr(x) = p { + free_names(x, &bound, out); + } + } + free_names(e, &bound, out); + } + Stmt::Expr(e) | Stmt::Throw(e) => free_names(e, &bound, out), + Stmt::Return(e) => { + if let Some(e) = e { + free_names(e, &bound, out); + } + } + Stmt::Refresh(n) | Stmt::Affects(n) => { + if !bound.contains(n) { + out.insert(n.clone()); + } + } + Stmt::Action(body) => free_names_stmts(body, &bound, out), + Stmt::Sif { c, t, f } => { + free_names(c, &bound, out); + free_names_stmts(t, &bound, out); + free_names_stmts(f, &bound, out); + } + Stmt::Try { body, catch_name, handler, fin, .. } => { + free_names_stmts(body, &bound, out); + if let Some(h) = handler { + let mut b = bound.clone(); + if let Some(n) = catch_name { + b.insert(n.clone()); + } + free_names_stmts(h, &b, out); + } + if let Some(f) = fin { + free_names_stmts(f, &bound, out); + } + } + } + } +} +fn names_of(e: &Expr) -> IndexSet { + let mut out = IndexSet::new(); + free_names(e, &HashSet::new(), &mut out); + out +} +fn strip_pre(n: &str) -> &str { + n.strip_prefix("pre:").unwrap_or(n) +} +/// Free names outside `peek` — the reads that make an initializer a snapshot (E-4). +fn reads_outside_peek(n: Node, out: &mut IndexSet) { + match n { + Node::E(Expr::Peek(_)) | Node::E(Expr::Fn { .. }) => {} + Node::E(Expr::Id(name)) => { + out.insert(name.clone()); + } + Node::E(Expr::Pre { name, .. }) => { + out.insert(name.clone()); + } + _ => { + for c in children(n, false) { + reads_outside_peek(c, out); + } + } + } +} +/// `map(xs, x => ({…}))` / `xs.map(x => ({…}))` +fn maps_into_records(e: &Expr) -> bool { + let Expr::Call { f, args } = e else { return false }; + let fn_arg = match &**f { + Expr::Id(n) if n == "map" => args.get(1), + Expr::Member { p, .. } if p == "map" => args.first(), + _ => None, + }; + matches!(fn_arg, Some(Expr::Fn { stmts: None, body, .. }) if matches!(**body, Expr::Rec(_))) +} +/// 12 SF-1d: the server fn a declaration's value is — itself, or wrapped by calls. +pub fn unwrap_server(e: &Expr) -> Option<&Expr> { + match e { + Expr::ServerFn { .. } => Some(e), + Expr::Call { args, .. } => { + let inner: Vec<&Expr> = args.iter().filter_map(unwrap_server).collect(); + if inner.len() == 1 { Some(inner[0]) } else { None } + } + _ => None, + } +} +fn js_string(s: &str) -> String { + serde_json::Value::String(s.to_string()).to_string() +} +/// A short rendering of an expression, for messages. +pub fn show(e: &Expr) -> String { + match e { + Expr::Lit(l) => match l { + Lit::Undefined => "undefined".into(), + Lit::Null => "null".into(), + Lit::Bool(b) => b.to_string(), + Lit::Num(n) => js_number(*n), + Lit::Str(s) => js_string(s), + }, + Expr::Id(n) => n.clone(), + Expr::Member { o, p, .. } => format!("{}.{}", show(o), p), + Expr::Call { f, .. } => format!("{}(…)", show(f)), + Expr::Await { e, .. } => format!("await {}", show(e)), + Expr::Latest(e) => format!("latest {}", show(e)), + Expr::Peek(e) => format!("peek {}", show(e)), + Expr::Throw(e) => format!("throw {}", show(e)), + Expr::Index { o, .. } => format!("{}[…]", show(o)), + Expr::Use(n) => format!("use {}", n), + Expr::Catch { e, .. } => format!("{} catch …", show(e)), + Expr::Tpl(_) => "`…`".into(), + Expr::Bin { op, l, r } => format!("{} {} {}", show(l), op, show(r)), + Expr::If { c, .. } => format!("{} ? … : …", show(c)), + _ => "…".into(), + } +} +pub fn js_number(n: f64) -> String { + if n.is_nan() { + "null".into() + } else if n.fract() == 0.0 && n.abs() < 1e21 { + format!("{}", n as i128) + } else { + format!("{}", n) + } +} + +// ---------------------------------------------------------------- the checker + +#[derive(Clone)] +struct Step { + f: Option, + p: Option, +} +#[derive(Clone)] +struct PFEntry { + chain: Vec, + term: Option<(i64, String)>, +} +#[derive(Clone)] +struct Item { + cls: String, + line: i64, + what: String, +} +#[derive(Clone)] +struct Req { + name: String, + line: i64, + what: String, +} +#[derive(Clone, Default)] +struct Esc { + fails: Vec, + reqs: Vec, +} +fn ck(c: &Option) -> String { + match c { + None => "-".into(), + Some(Catch::All) => "*".into(), + Some(Catch::Set(s)) => { + let mut v: Vec<&String> = s.iter().collect(); + v.sort(); + v.into_iter().cloned().collect::>().join("|") + } + } +} +fn chain_key(c: &[Step]) -> String { + c.iter().map(|x| format!("{}~{}", ck(&x.f), ck(&x.p))).collect::>().join("/") +} +fn dedupe_chains(xs: Vec>) -> Vec> { + let mut seen = HashSet::new(); + xs.into_iter().filter(|x| seen.insert(chain_key(x))).collect() +} +fn dedupe_entries(xs: Vec) -> Vec { + let mut seen = HashSet::new(); + xs.into_iter() + .filter(|x| { + let term = match &x.term { + None => "null".to_string(), + Some((l, n)) => format!("{{\"line\":{},\"name\":{}}}", l, js_string(n)), + }; + seen.insert(chain_key(&x.chain) + &term) + }) + .collect() +} +fn esc_f(chain: &[Step], cls: &str) -> bool { + !chain.iter().any(|s| s.f.as_ref().is_some_and(|f| f.caught(cls))) +} +fn esc_p(chain: &[Step], name: &str) -> bool { + !chain.iter().any(|s| match &s.p { + Some(Catch::All) => true, + Some(Catch::Set(p)) => p.contains(name), + None => false, + }) +} +fn meet(a: Catch, b: &Catch) -> Catch { + match (a, b) { + (Catch::All, b) => b.clone(), + (a, Catch::All) => a, + (Catch::Set(a), Catch::Set(b)) => Catch::Set(a.into_iter().filter(|x| b.contains(x)).collect()), + } +} +fn along(chain: &[Step], f: bool) -> Catch { + let mut out = IndexSet::new(); + for st in chain { + let c = if f { &st.f } else { &st.p }; + match c { + Some(Catch::All) => return Catch::All, + Some(Catch::Set(s)) => out.extend(s.iter().cloned()), + None => {} + } + } + Catch::Set(out) +} + +#[derive(Clone)] +enum Via { + Reader(RId), + Arg { comp: RId, field: String, site: RId }, + Action { line: i64, name: String }, +} +#[derive(Clone)] +struct Occ { + comp: RId, + field: String, + pre: Vec, + via: Via, +} + +struct UseRec { + region: RId, + name: String, + line: i64, + display: String, +} +struct ReaderRec<'a> { + region: RId, + e: &'a Expr, + env: EId, + line: i64, +} +struct AppRec<'a> { + comp: RId, + site: RId, + line: i64, + tag: String, + args: Vec<(String, &'a Expr, EId)>, +} +struct ActionRec<'a> { + name: String, + body: &'a [Stmt], + env: EId, + line: i64, +} +struct Provision<'a> { + key: String, + e: &'a Expr, + env: EId, +} +enum Later<'a> { + Content { ms: &'a [Markup], env: EId, anc: Vec }, + Provide { name: String, e: &'a Expr, env: EId, line: i64, contract: (IndexSet, IndexSet) }, + RowFails { name: String, env: EId, fw: Option, written: String, line: i64 }, +} + +pub struct Opts<'a> { + pub sigs: &'a IndexMap, + pub file: Option, + pub declarations: &'a IndexMap>, +} + +pub struct Checker<'a> { + pub diags: Vec, + pub model: ModelBuilder, + opts: Opts<'a>, + root_env: Option, + line: i64, + seq: usize, + uses: Vec, + readers: Vec>, + apps: Vec>, + actions: Vec>, + provisions: Vec>, + later: Vec>, + comp_syms: HashMap, + rows_busy: HashSet, + regions: Vec, + syms: Vec>, + envs: Vec, + awaited_calls: HashSet<*const Expr>, + server_exprs: HashMap<*const Expr, SId>, + stmt_awaits: HashSet<*const Expr>, + shapes: HashMap>, + // the application tree (resolve_tree), kept for the signature's component summaries + occ: Vec, + r_memo: HashMap>>, + r_busy: HashSet, + pf_memo: HashMap<(RId, String), Vec>, + pf_busy: HashSet<(RId, String)>, + esc_memo: HashMap, Esc>, + esc_busy: HashSet>, + reader_f: Vec<(RId, &'a Expr, i64, Fails)>, + arg_r: Vec<(usize, String)>, + arg_f: Vec<(usize, String, Fails)>, +} + +pub struct CheckResult { + pub diags: Vec, + pub sig: ModSig, + pub model: serde_json::Value, +} + +pub fn check_with_signature(p: &Program, opts: Opts) -> CheckResult { + let file = opts.file.clone().unwrap_or_else(|| "main.rx".into()); + let mut c = Checker::new(opts); + c.run(p); + let sig = c.signature(p); + let model = c.build_model(&file); + CheckResult { diags: c.diags, sig, model } +} + +pub struct Checked { + pub diags: Vec, + pub sigs: IndexMap, + pub models: IndexMap, +} +/// CN-4b / MO-3: check every module of a program, dependencies first, each against its imports' signatures only. +pub fn check_modules(set: &ModuleSet) -> Checked { + let mut sigs: IndexMap = set.signatures.clone(); + let mut diags = set.diags.clone(); + let mut models = IndexMap::new(); + for path in &set.order { + let r = check_with_signature(&set.programs[path], Opts { sigs: &sigs, file: Some(path.clone()), declarations: &set.declarations }); + diags.extend(r.diags); + sigs.insert(path.clone(), r.sig); + models.insert(path.clone(), r.model); + } + Checked { diags, sigs, models } +} + +impl<'a> Checker<'a> { + pub fn new(opts: Opts<'a>) -> Self { + Checker { + diags: vec![], + model: ModelBuilder::default(), + opts, + root_env: None, + line: 1, + seq: 0, + uses: vec![], + readers: vec![], + apps: vec![], + actions: vec![], + provisions: vec![], + later: vec![], + comp_syms: HashMap::new(), + rows_busy: HashSet::new(), + regions: vec![], + syms: vec![], + envs: vec![], + awaited_calls: HashSet::new(), + server_exprs: HashMap::new(), + stmt_awaits: HashSet::new(), + shapes: HashMap::new(), + occ: vec![], + r_memo: HashMap::new(), + r_busy: HashSet::new(), + pf_memo: HashMap::new(), + pf_busy: HashSet::new(), + esc_memo: HashMap::new(), + esc_busy: HashSet::new(), + reader_f: vec![], + arg_r: vec![], + arg_f: vec![], + } + } + + fn err(&mut self, code: &str, rule: &str, msg: String) { + let line = self.line; + if !self.diags.iter().any(|d| d.code == code && d.line == line && d.msg == msg) { + self.diags.push(Diagnostic { code: code.into(), rule: rule.into(), line, msg, file: self.opts.file.clone() }); + } + } + fn region(&mut self, parent: Option, kind: &'static str, component: bool, name: Option) -> RId { + let id = self.seq; + self.seq += 1; + let r = self.regions.len(); + let depth = parent.map(|p| self.regions[p].depth + 1).unwrap_or(0); + let comp = if component { Some(r) } else { parent.and_then(|p| self.regions[p].component) }; + self.regions.push(Region { id, depth, parent, component: comp, is_component_root: component, provides: IndexSet::new(), catches: None, kids: None, sig: None }); + self.model.region(id, kind, parent.map(|p| self.regions[p].id), name); + r + } + fn env(&mut self, parent: Option, region: RId) -> EId { + self.envs.push(Env { vars: IndexMap::new(), parent, region }); + self.envs.len() - 1 + } + fn lookup(&self, env: EId, n: &str) -> Option { + let mut e = Some(env); + while let Some(i) = e { + if let Some(&s) = self.envs[i].vars.get(n) { + return Some(s); + } + e = self.envs[i].parent; + } + None + } + fn new_sym(&mut self, s: Sym<'a>) -> SId { + self.syms.push(s); + self.syms.len() - 1 + } + /// A constant (`let`, a parameter, a row binding): bound, not part of the model. + fn set_value(&mut self, env: EId, name: &str, line: i64) { + let region = self.envs[env].region; + let s = self.new_sym(Sym::new(SymKind::Value, name, region, line)); + self.envs[env].vars.insert(name.to_string(), s); + } + /// CP-6: a symbol bound in `env`, recorded for the model. + fn bind(&mut self, env: EId, name: &str, sym: Sym<'a>) -> SId { + let kind = sym.kind; + let s = self.new_sym(sym); + self.envs[env].vars.insert(name.to_string(), s); + self.model.sym(s, kind.as_str()); + s + } + fn reg(&self, env: EId) -> RId { + self.envs[env].region + } + /// CP-6: a computation the walk enters. + fn comp(&mut self, kind: &'static str, env: EId, name: Option, at: Option, f: F) { + let region = self.regions[self.reg(env)].id; + let saved = self.model.within(kind, region, self.line, name, at); + f(self); + self.model.leave(saved); + } + fn read(&mut self, s: SId, ctx: Ctx, pre: bool, field: Option<&str>) { + let kind = self.syms[s].kind.as_str(); + self.model.read(s, kind, ctx.imperative, pre, field); + } + pub fn build_model(&self, file: &str) -> serde_json::Value { + self.model.build(file, |i| { + let s = &self.syms[i]; + ModelSymbol { + name: s.name.clone(), + kind: s.kind.as_str(), + region: self.regions[s.region].id, + line: s.line, + question: if s.kind == SymKind::Source { Some(s.question) } else { None }, + io: s.io, + from: s.imported.clone(), + } + }) + } + + pub fn run(&mut self, p: &'a Program) { + let r = self.region(None, "program", false, None); + let root = self.env(None, r); + self.root_env = Some(root); + if p.legacy_view == Some(true) { + self.line = p.view.first().and_then(|m| m.line).unwrap_or(1) as i64; + self.err("TOP_LEVEL_MARKUP", "MO-1c", "markup at a module's top level: a module is declarations; the app's view is a component — `export default component App() { … }`".into()); + } + self.block(&p.decls, &p.view, root, Ctx::reactive()); + self.later.push(Later::Content { ms: &p.view, env: root, anc: vec![] }); + let mut i = 0; + while i < self.later.len() { + let job = std::mem::replace(&mut self.later[i], Later::Content { ms: &[], env: root, anc: vec![] }); + self.run_later(job); + i += 1; + } + self.resolve_tree(); + } + fn run_later(&mut self, job: Later<'a>) { + match job { + Later::Content { ms, env, anc } => self.content_check(ms, env, &anc), + Later::Provide { name, e, env, line, contract } => { + let rows = self.rows_of(e, env); + let f = self.fails_of(e, env, false, &HashMap::new()); + self.line = line; + for r in rows { + if !contract.0.contains(&r) { + self.err("ROW_NARROWER_THAN_INFERRED", "RG-7e", format!("context {} is declared without `{}`, but this provision is {}", name, r, r)); + } + } + for x in f.cls { + if !contract.1.contains("Error") && !contract.1.contains(&x) { + self.err("ROW_NARROWER_THAN_INFERRED", "RG-7e", format!("context {} is declared without fails({}), but this provision can fail with {}", name, x, x)); + } + } + } + Later::RowFails { name, env, fw, written, line } => { + let Some(s) = self.envs[env].vars.get(&name).copied() else { return }; + if self.syms[s].fails_t.is_none() { + return; + } + let f = self.sym_fails(s); + let allowed: Option> = fw.as_ref().map(|fw| fw[6..fw.len() - 1].split('|').map(|x| js_trim(x).to_string()).filter(|x| !x.is_empty()).collect()); + let all = allowed.as_ref().is_none_or(|a| a.iter().any(|x| x == "Error")); + if fw.is_none() && f.cls.is_empty() { + return; + } + for c in f.cls { + if fw.is_none() || (!all && !allowed.as_ref().unwrap().contains(&c)) { + self.line = line; + self.err("ROW_NARROWER_THAN_INFERRED", "CN-4a", format!("{} is declared `{}` but its value also fails with {}", name, written, c)); + } + } + } + } + } + + // ---- blocks: declarations, then a view (02 §1, 03) ---- + fn block(&mut self, decls: &'a [Decl], view: &'a [Markup], env: EId, ctx: Ctx) { + let region = self.reg(env); + // 1. names (E-7: one declaration per name in a block) + for d in decls { + self.line = d.line() as i64; + if let Decl::Import { resolved: Some(_), .. } = d { + self.import_rx(d, env); + continue; + } + if let Decl::Import { declared: Some(_), .. } = d { + self.import_declared(d, env); + continue; + } + match d { + Decl::Import { names, pure, fails, side, line, .. } => { + for n in names { + let mut s = Sym::new(SymKind::Import, &n.alias, region, *line as i64); + s.io = !pure; + s.rejects = Some(fails.clone().unwrap_or_else(|| vec!["Error".into()]).into_iter().collect()); + s.side = *side; + self.bind(env, &n.alias, s); + } + continue; + } + Decl::Effect { .. } | Decl::Provide { .. } | Decl::TypeDecl { .. } => continue, + Decl::Context { name, ty, source, dflt, line, exported } => { + if self.envs[env].vars.contains_key(name) { + self.err("DUPLICATE_DECLARATION", "E-7", format!("{} is declared twice in one block", name)); + } + let contract = if ty.as_ref().is_some_and(|t| ROW_WORDS.is_match(t)) || *exported { Some(contract_of(ty.as_deref())) } else { None }; + if *source && dflt.is_some() { + self.err("INVALID_CONTEXT_DEFAULT", "RG-7c", format!("context {}: source … has no default — a provided source is its provider's; a default would be one source every user writes (declare that source instead)", name)); + } + let suffix = if self.regions[region].parent.is_some() { + let n = self.seq; + self.seq += 1; + format!("@{}", n) + } else { + String::new() + }; + let key = format!("{}#{}{}", self.opts.file.as_deref().unwrap_or("main.rx"), name, suffix); + let mut s = Sym::new(SymKind::Context, name, region, *line as i64); + s.ctx = Some(CtxInfo { key: key.clone(), source: *source, dflt: dflt.is_some(), contract }); + self.bind(env, name, s); + if let Some(e) = dflt { + self.provisions.push(Provision { key, e, env }); + } + continue; + } + _ => {} + } + let name = d.name().unwrap(); + if let Some(&prev) = self.envs[env].vars.get(name) { + if self.syms[prev].kind != SymKind::Import { + self.err("DUPLICATE_DECLARATION", "E-7", format!("{} is declared twice in one block", name)); + } + } + let kind = match d { + Decl::Fn { .. } => SymKind::Fn, + Decl::Component { .. } => SymKind::Comp, + Decl::Action { .. } => SymKind::Action, + Decl::Server { .. } => SymKind::Server, + Decl::Source { .. } => SymKind::Source, + Decl::Error { .. } => SymKind::ErrClass, + _ => SymKind::Eq, + }; + let mut sym = Sym::new(kind, name, region, d.line() as i64); + let mut server_expr = None; + match d { + Decl::Server { params, .. } => sym.params = Some(params.iter().map(|n| SigParam { name: n.clone(), r#ref: false, ty: None }).collect()), + Decl::Source { question, .. } => sym.question = *question, + Decl::Action { .. } => sym.io = true, + Decl::Fn { params, decls: fdecls, .. } => { + sym.params = Some(params.iter().map(|p| SigParam { name: p.name.clone(), r#ref: p.r#ref, ty: p.ty.clone() }).collect()); + sym.instantiating = params.iter().any(|p| p.r#ref) || !fdecls.is_empty(); + sym.fn_decl = Some(d); + sym.decl_env = Some(env); + } + Decl::Component { name, .. } => { + sym.comp = Some(d); + let r = self.region(Some(region), "component", true, Some(name.clone())); + sym.comp_region = Some(r); + } + Decl::Eq { e, key, .. } => { + sym.maps_records = key.is_none() && maps_into_records(e); + if let Expr::Id(n) = e { + if let Some(t) = self.lookup(env, n) { + if self.syms[t].io { + sym.io = true; + } + } + } + if let Some(sf) = unwrap_server(e) { + sym.kind = SymKind::Server; + if let Expr::ServerFn { params, .. } = sf { + sym.params = Some(params.iter().map(|n| SigParam { name: n.clone(), r#ref: false, ty: None }).collect()); + } + server_expr = Some(sf as *const Expr); + } + } + _ => {} + } + let comp_region = sym.comp_region; + let s = self.bind(env, name, sym); + if let Some(r) = comp_region { + self.comp_syms.insert(r, s); + } + if let Some(p) = server_expr { + self.server_exprs.insert(p, s); + } + } + self.server_io(decls, env); + for d in decls { + if let Decl::Source { name, init, question: false, .. } = d { + let s = self.envs[env].vars[name.as_str()]; + self.syms[s].fails_t = Some(FailsT::Expr { e: init, env, cache: None, busy: false }); + } + } + for d in decls { + let (name, e) = match d { + Decl::Eq { name, e, .. } => (name, e), + Decl::Source { name, init, question: true, .. } => (name, init), + _ => continue, + }; + let s = self.envs[env].vars[name.as_str()]; + let sym = &mut self.syms[s]; + sym.rows = Some(RowsT::Expr { e, env, cache: None, busy: false }); + sym.expr = Some((e, env)); + sym.fails_t = Some(FailsT::Expr { e, env, cache: None, busy: false }); + } + // 2. C-1 over this block's equations (the static graph, CN-3a) + self.cycles(decls, env); + // 3. each declaration + for d in decls { + self.line = d.line() as i64; + self.decl(d, env, ctx); + } + // 4. the view + let view_root = self.regions[region].is_component_root || self.regions[region].parent.is_none(); + self.markup(view, env, view_root); + } + + /// 12 SF-1: a server fn is io when its body calls io — JS declared io, `log`, or another server fn that is. + fn server_io(&mut self, decls: &'a [Decl], env: EId) { + let mut servers: Vec<(&str, &[String], &[Stmt])> = vec![]; + for d in decls { + match d { + Decl::Server { name, params, body, .. } => servers.push((name, params, body)), + Decl::Eq { name, e, .. } => { + if let Some(Expr::ServerFn { params, body, .. }) = unwrap_server(e) { + servers.push((name, params, body)); + } + } + _ => {} + } + } + let mut changed = true; + while changed { + changed = false; + for (name, params, body) in &servers { + let s = self.envs[env].vars[*name]; + if self.syms[s].io { + continue; + } + let mut calls = vec![]; + for st in body.iter() { + calls_in(Node::S(st), &mut calls); + } + let hit = calls.into_iter().find(|n| { + !params.contains(n) + && if n == "log" { + self.lookup(env, n).is_none() + } else { + self.lookup(env, n).is_some_and(|t| self.syms[t].io) + } + }); + if let Some(h) = hit { + self.syms[s].io = true; + self.syms[s].io_via = Some(format!("{}(…)", h)); + changed = true; + } + } + } + } + + /// MO-3/MO-4: bind an `.rx` import's names from its module's signature. + fn import_rx(&mut self, d: &'a Decl, env: EId) { + let Decl::Import { names, from, pure, line, fails, resolved, .. } = d else { return }; + let region = self.reg(env); + let line = *line as i64; + let sigs = self.opts.sigs; + let sig = sigs.get(resolved.as_ref().unwrap()); + if fails.is_some() { + self.err("IMPORT_ROW_MISMATCH", "MO-4", format!("`fails` asserts rows at the JS boundary; {} is .rx and its rows are its signature's", from)); + } + for ImportName { name, alias } in names { + let base = || Sym::new(SymKind::Import, alias, region, line); + let Some(sig) = sig else { + self.bind(env, alias, base()); + continue; + }; + let Some(x) = sig.exports.get(name) else { + self.err("NOT_EXPORTED", "MO-6", format!("{} does not export {}", from, if name == "default" { "a default".to_string() } else { name.clone() })); + self.bind(env, alias, base()); + continue; + }; + let set = |v: &Vec| -> IndexSet { v.iter().cloned().collect() }; + let sym = match x { + ExportSig::Type => continue, + ExportSig::Source { question, rows, fails } => { + let mut s = Sym::new(SymKind::Source, alias, region, line); + s.question = *question; + s.imported = Some(from.clone()); + s.rows = Some(RowsT::Fixed(set(rows))); + s.fails_t = Some(FailsT::Fixed(set(fails))); + s + } + ExportSig::Eq { rows, fails, io } => { + let mut s = Sym::new(SymKind::Eq, alias, region, line); + s.io = *io; + s.rows = Some(RowsT::Fixed(set(rows))); + s.fails_t = Some(FailsT::Fixed(set(fails))); + s + } + ExportSig::Fn { params, instantiating, rows, fails, rejects } => { + let mut s = Sym::new(SymKind::Fn, alias, region, line); + s.params = Some(params.clone()); + s.instantiating = *instantiating; + s.rows = Some(RowsT::Fixed(set(rows))); + s.fails_t = Some(FailsT::Fixed(set(fails))); + s.rejects = Some(set(rejects)); + s + } + ExportSig::Action => { + let mut s = Sym::new(SymKind::Action, alias, region, line); + s.io = true; + s + } + ExportSig::Server { io, params, rejects } => { + let mut s = Sym::new(SymKind::Server, alias, region, line); + s.io = *io; + s.params = Some(params.iter().map(|n| SigParam { name: n.clone(), r#ref: false, ty: None }).collect()); + s.server_rejects = Some(ServerRejects::Fixed(set(rejects))); + s + } + ExportSig::ErrClass { .. } => Sym::new(SymKind::ErrClass, alias, region, line), + ExportSig::Comp { sig } => { + let r = self.region(None, "imported", true, Some(alias.clone())); + self.regions[r].sig = Some(sig.clone()); + let mut s = Sym::new(SymKind::Comp, alias, region, line); + s.comp_region = Some(r); + s + } + ExportSig::Context { key, source, dflt, rows, fails, .. } => { + let mut s = Sym::new(SymKind::Context, alias, region, line); + s.ctx = Some(CtxInfo { key: key.clone(), source: *source, dflt: *dflt, contract: Some((set(rows), set(fails))) }); + s + } + }; + if *pure && sym.io { + self.err("IMPORT_ROW_MISMATCH", "MO-4", format!("{} is io in {}'s signature; `pure` cannot lower an .rx row", name, from)); + } + self.bind(env, alias, sym); + } + } + /// RG-7: the context a `provide`/`use` names — lexically, like any name. + fn ctx_of(&mut self, name: &str, env: EId) -> Option { + let Some(s) = self.lookup(env, name) else { + self.err("UNBOUND", "RG-7", format!("{} is not declared here: declare the context (`context {}: T`) or import it", name, name)); + return None; + }; + let k = self.syms[s].kind; + if k != SymKind::Context { + let what = match k { + SymKind::Source => "a source".to_string(), + SymKind::Eq => "an equation".to_string(), + k => format!("a {}", k.as_str()), + }; + self.err("NOT_A_CONTEXT", "RG-7", format!("{} is {}, not a context: `provide` and `use` name a `context` declaration", name, what)); + return None; + } + Some(s) + } + /// MO-8: bind a JS import's names from its declaration file. + fn import_declared(&mut self, d: &'a Decl, env: EId) { + let Decl::Import { names, from, pure, line, fails, side, declared, .. } = d else { return }; + let region = self.reg(env); + let line = *line as i64; + let declared = declared.as_ref().unwrap(); + let decls: &[JsDecl] = self.opts.declarations.get(declared).map(|v| v.as_slice()).unwrap_or(&[]); + let file = declared.split('/').last().unwrap_or("").to_string(); + if fails.is_some() { + self.err("IMPORT_ROW_MISMATCH", "MO-8", format!("{}'s rows are declared in {}: `fails` on the import cannot restate them", from, file)); + } + for ImportName { name, alias } in names { + let Some(x) = decls.iter().find(|y| y.name() == name) else { + self.err("NOT_EXPORTED", "MO-8", format!("{} declares no export {} (declare it, or it is not {}'s)", file, name, from)); + let mut s = Sym::new(SymKind::Import, alias, region, line); + s.io = true; + self.bind(env, alias, s); + continue; + }; + match x { + JsDecl::Type { .. } => continue, + JsDecl::Error { .. } => { + self.bind(env, alias, Sym::new(SymKind::ErrClass, alias, region, line)); + continue; + } + JsDecl::Fn { pure: xpure, side: xside, rejects, .. } => { + if *pure && !xpure { + self.err("IMPORT_ROW_MISMATCH", "MO-8", format!("{} is declared io in {} (`export fn`, not `export pure fn`); `pure` on the import cannot lower it", name, file)); + } + if let Some(sd) = side { + if *xside != Some(*sd) { + self.err( + "IMPORT_ROW_MISMATCH", + "SF-5", + format!("{} is declared {} in {}; `{}` on the import cannot restate it", name, xside.map(|x| format!("`{}`", x.as_str())).unwrap_or_else(|| "for both sides".into()), file, sd.as_str()), + ); + } + } + let mut s = Sym::new(SymKind::Import, alias, region, line); + s.io = !xpure; + s.rejects = Some(rejects.clone().unwrap_or_else(|| vec!["Error".into()]).into_iter().collect()); + s.side = *xside; + self.bind(env, alias, s); + } + } + } + } + + /// MO-3: this module's signature — every export as an importer sees it. + pub fn signature(&mut self, p: &'a Program) -> ModSig { + let mut exports = IndexMap::new(); + let env = self.root_env.unwrap(); + let arr = |x: IndexSet| -> Vec { x.into_iter().collect() }; + for d in &p.decls { + if !d.exported() { + continue; + } + let Some(name) = d.name() else { continue }; + if let Decl::TypeDecl { .. } = d { + exports.insert(name.to_string(), ExportSig::Type); + continue; + } + let Some(&s) = self.envs[env].vars.get(name) else { continue }; + let x = match self.syms[s].kind { + SymKind::Source => { + let rows = if self.syms[s].rows.is_some() { self.sym_rows(s) } else { IndexSet::new() }; + let fails = if self.syms[s].fails_t.is_some() { self.sym_fails(s).cls } else { IndexSet::new() }; + ExportSig::Source { question: self.syms[s].question, rows: arr(rows), fails: arr(fails) } + } + SymKind::Eq => { + let rows = if self.syms[s].rows.is_some() { self.sym_rows(s) } else { IndexSet::new() }; + let fails = if self.syms[s].fails_t.is_some() { self.sym_fails(s).cls } else { IndexSet::new() }; + ExportSig::Eq { rows: arr(rows), fails: arr(fails), io: self.syms[s].io } + } + SymKind::Fn => { + let rows = if self.syms[s].rows.is_some() { self.sym_rows(s) } else { IndexSet::new() }; + let fails = if self.syms[s].fails_t.is_some() { self.sym_fails(s).cls } else { IndexSet::new() }; + let rejects = match self.syms[s].fn_tail { + Some((t, te)) => self.rejects(t, te, &mut HashSet::new()), + None => self.syms[s].rejects.clone().unwrap_or_default(), + }; + ExportSig::Fn { params: self.syms[s].params.clone().unwrap_or_default(), instantiating: self.syms[s].instantiating, rows: arr(rows), fails: arr(fails), rejects: arr(rejects) } + } + SymKind::Action => ExportSig::Action, + SymKind::Server => { + let rejects = if self.syms[s].server_rejects.is_some() { self.sym_server_rejects(s) } else { IndexSet::new() }; + ExportSig::Server { io: self.syms[s].io, params: self.syms[s].params.clone().unwrap_or_default().into_iter().map(|p| p.name).collect(), rejects: arr(rejects) } + } + SymKind::ErrClass => ExportSig::ErrClass { params: if let Decl::Error { params, .. } = d { params.clone() } else { vec![] } }, + SymKind::Comp => { + let r = self.syms[s].comp_region.unwrap(); + ExportSig::Comp { sig: self.comp_sig(r) } + } + SymKind::Context => { + let c = self.syms[s].ctx.clone().unwrap(); + let (rows, fails) = c.contract.clone().unwrap_or_default(); + let ty = if let Decl::Context { ty, .. } = d { ty.as_ref().map(|t| ROW_WORDS.replace(t, "").to_string()) } else { None }; + ExportSig::Context { key: c.key, source: c.source, dflt: c.dflt, rows: arr(rows), fails: arr(fails), ty } + } + _ => continue, + }; + exports.insert(name.to_string(), x); + } + ModSig { file: self.opts.file.clone().unwrap_or_default(), exports } + } + + fn cycles(&mut self, decls: &'a [Decl], env: EId) { + let mut eqs: IndexMap<&str, (&'a Expr, i64)> = IndexMap::new(); + for d in decls { + match d { + Decl::Eq { name, e, line, .. } => { + eqs.insert(name, (e, *line as i64)); + } + Decl::Source { name, init, line, question: true, .. } => { + eqs.insert(name, (init, *line as i64)); + } + _ => {} + } + } + let mut color: HashMap = HashMap::new(); + let mut reported: HashSet = HashSet::new(); + let names: Vec<&str> = eqs.keys().copied().collect(); + for n in names { + self.visit_cycle(n, &[], &eqs, env, &mut color, &mut reported); + } + } + fn visit_cycle(&mut self, n: &str, stack: &[String], eqs: &IndexMap<&str, (&'a Expr, i64)>, env: EId, color: &mut HashMap, reported: &mut HashSet) { + if color.get(n) == Some(&2) { + return; + } + if color.get(n) == Some(&1) { + let at = stack.iter().position(|s| s == n).map(|i| i as i64).unwrap_or(-1); + let from = if at < 0 { stack.len().saturating_sub(1) } else { at as usize }; + let mut cyc: Vec = stack[from..].to_vec(); + cyc.push(n.to_string()); + let mut sorted = cyc.clone(); + sorted.sort(); + let key = sorted.join(","); + if reported.insert(key) { + self.line = eqs[n].1; + self.err("CYCLE", "C-1", format!("instantaneous cycle: {} (break it with `pre`, C-2)", cyc.join(" → "))); + } + return; + } + color.insert(n.to_string(), 1); + let names = names_of(eqs[n].0); + let region = self.reg(env); + for m0 in names { + if m0 == format!("pre:{}", n) { + continue; + } + let m = strip_pre(&m0); + if eqs.contains_key(m) && self.envs[env].vars.get(m).is_some_and(|&s| self.syms[s].region == region) { + let mut st = stack.to_vec(); + st.push(n.to_string()); + self.visit_cycle(m, &st, eqs, env, color, reported); + } + } + color.insert(n.to_string(), 2); + } + + // ---- rows (R-13) ---- + fn sym_rows(&mut self, s: SId) -> IndexSet { + let (e, env) = match &mut self.syms[s].rows { + None => return IndexSet::new(), + Some(RowsT::Fixed(x)) => return x.clone(), + Some(RowsT::Expr { cache: Some(c), .. }) => return c.clone(), + Some(RowsT::Expr { busy: true, .. }) => return IndexSet::new(), + Some(RowsT::Expr { e, env, busy, .. }) => { + *busy = true; + (*e, *env) + } + }; + let rows = self.rows_of(e, env); + if let Some(RowsT::Expr { e: e2, cache, busy, .. }) = &mut self.syms[s].rows { + if std::ptr::eq(*e2, e) { + *busy = false; + *cache = Some(rows.clone()); + } + } + rows + } + /// A fresh rows thunk over `e`, evaluated once (`this.rowsThunk(e, env)()`). + fn rows_of(&mut self, e: &'a Expr, env: EId) -> IndexSet { + let mut rows = IndexSet::new(); + self.rows_walk(Node::E(e), env, &mut rows); + rows + } + fn rows_walk(&mut self, x: Node<'a>, env: EId, rows: &mut IndexSet) { + if let Node::E(e) = x { + match e { + Expr::Await { e: inner, init, .. } => { + rows.insert("pending".into()); + if init.is_none() && self.client_call(inner, env) { + rows.insert("client".into()); + } + } + Expr::Latest(_) | Expr::Pending(_) => { + rows.insert("lane".into()); + } + Expr::Fn { .. } | Expr::ServerFn { .. } => return, + Expr::Use(name) => { + let Some(c) = self.lookup(env, name).and_then(|s| self.syms[s].ctx.clone()) else { return }; + if let Some((r, _)) = &c.contract { + rows.extend(r.iter().cloned()); + return; + } + let mut i = 0; + while i < self.provisions.len() { + let (pk, pe, penv) = (self.provisions[i].key.clone(), self.provisions[i].e, self.provisions[i].env); + if pk == c.key && !self.rows_busy.contains(&c.key) { + self.rows_busy.insert(c.key.clone()); + let r = self.rows_of(pe, penv); + rows.extend(r); + self.rows_busy.remove(&c.key); + } + i += 1; + } + return; + } + Expr::Id(n) => { + if let Some(s) = self.lookup(env, n) { + if self.syms[s].rows.is_some() { + let r = self.sym_rows(s); + rows.extend(r); + } + } + return; + } + _ => {} + } + } + for c in children(x, false) { + self.rows_walk(c, env, rows); + } + } + + /// SF-5b: a call into `client` JS. + fn client_call(&self, e: &Expr, env: EId) -> bool { + let f = if let Expr::Catch { e, .. } = e { &**e } else { e }; + match f { + Expr::Call { f, .. } => match &**f { + Expr::Id(n) => self.lookup(env, n).is_some_and(|s| self.syms[s].side == Some(Side::Client)), + _ => false, + }, + _ => false, + } + } + + fn decl(&mut self, d: &'a Decl, env: EId, ctx: Ctx) { + let reactive = Ctx { in_fn: ctx.in_fn, ..Ctx::reactive() }; + match d { + Decl::Import { fails, .. } => { + if let Some(f) = fails { + for n in f { + self.err_class(n, env); + } + } + } + Decl::TypeDecl { .. } => {} + Decl::Error { params, msg, line, .. } => { + if let Some(m) = msg { + let region = self.reg(env); + let inner = self.env(Some(env), region); + for p in params { + self.set_value(inner, p, *line as i64); + } + self.expr(m, inner, Ctx { in_fn: true, ..Ctx::reactive() }, None); + } + } + Decl::Source { name, init, question, ty, .. } => { + if *question { + self.comp("question", env, Some(name.clone()), None, |c| c.expr(init, env, reactive, None)); + self.row_check(name, ty.as_deref(), env); + return; + } + let mut reads = IndexSet::new(); + reads_outside_peek(Node::E(init), &mut reads); + let hit = reads.iter().find(|n| self.lookup(env, n).is_some_and(|s| self.syms[s].kind.reactive())).cloned(); + if let Some(h) = hit { + self.err("SNAPSHOT_INITIALIZER", "E-4", format!("source {} := … reads {} outside peek(…): write the snapshot as one (DV-6)", name, h)); + } + let c2 = Ctx { in_fn: ctx.in_fn, ..Ctx::reactive() }; + self.comp("seed", env, Some(name.clone()), None, |c| { + c.model.snap += 1; + c.expr(init, env, c2, None); + }); + } + Decl::Eq { name, e, deferred, key, ty, .. } => { + let kind = if *deferred { + "deferred" + } else if key.is_some() { + "keyed" + } else { + "memo" + }; + if let Expr::Call { f, args } = e { + if let Expr::Id(fname) = &**f { + if let Some(fs) = self.lookup(env, fname) { + if self.syms[fs].kind == SymKind::Fn && self.syms[fs].instantiating { + self.snapshot_args(e, fs, env); + let params = self.syms[fs].params.clone().unwrap_or_default(); + self.comp(kind, env, Some(name.clone()), None, |c| { + c.model.call(Some(fs), "fn"); + for (i, a) in args.iter().enumerate() { + let value = params.get(i).is_some_and(|p| !p.r#ref); + if value { + c.model.snap += 1; + } + c.expr(a, env, reactive, None); + if value { + c.model.snap -= 1; + } + } + }); + self.row_check(name, ty.as_deref(), env); + return; + } + } + } + } + self.comp(kind, env, Some(name.clone()), None, |c| c.expr(e, env, reactive, None)); + self.row_check(name, ty.as_deref(), env); + } + Decl::Provide { name, e, .. } => { + self.comp("provide", env, Some(name.clone()), None, |c| c.expr(e, env, reactive, None)); + let Some(c) = self.ctx_of(name, env) else { return }; + let info = self.syms[c].ctx.clone().unwrap(); + let region = self.reg(env); + self.regions[region].provides.insert(info.key.clone()); + self.provisions.push(Provision { key: info.key.clone(), e, env }); + if info.source { + let t = if let Expr::Id(n) = e { self.lookup(env, n) } else { None }; + if t.is_none_or(|t| self.syms[t].kind != SymKind::Source || self.syms[t].imported.is_some()) { + self.err("PROVIDE_NOT_A_SOURCE", "RG-7c", format!("context {} carries a source: provide one of this module's sources by name (`provide {} = x`)", name, name)); + } + } + if let Some(contract) = info.contract { + let line = self.line; + self.later.push(Later::Provide { name: name.clone(), e, env, line, contract }); + } + } + Decl::Context { name, dflt, .. } => { + if let Some(dflt) = dflt { + self.comp("default", env, Some(name.clone()), None, |c| c.expr(dflt, env, reactive, None)); + } + } + Decl::Effect { inputs, body, .. } => { + self.comp("effect", env, None, None, |c| { + for i in inputs { + c.expr(i, env, reactive, None); + } + c.comp("apply", env, None, None, |c| { + let region = c.reg(env); + let inner = c.env(Some(env), region); + c.stmts(body, inner, Ctx::imperative()); + }); + }); + self.own_source_writes(inputs, body, env); + } + Decl::Action { name, params, body, line, .. } => { + let region = self.reg(env); + let inner = self.env(Some(env), region); + for p in params { + self.set_value(inner, p, *line as i64); + } + self.comp("action", env, Some(name.clone()), None, |c| c.stmts(body, inner, Ctx { in_action: true, ..Ctx::imperative() })); + self.actions.push(ActionRec { name: name.clone(), body, env: inner, line: *line as i64 }); + } + Decl::Server { name, params, body, line, .. } => { + let region = self.reg(env); + let inner = self.env(Some(env), region); + for p in params { + self.set_value(inner, p, *line as i64); + } + self.comp("server", env, Some(name.clone()), None, |c| c.stmts(body, inner, Ctx { server: true, ..Ctx::imperative() })); + let s = self.lookup(env, name).unwrap(); + self.syms[s].server_rejects = Some(ServerRejects::Body { body, env: inner, cache: None }); + } + Decl::Fn { name, params, decls, tail, line, .. } => { + let region = self.reg(env); + let r = if !decls.is_empty() { self.region(Some(region), "fn", false, Some(name.clone())) } else { region }; + let inner = self.env(Some(env), r); + for p in params { + if p.r#ref { + self.bind(inner, &p.name, Sym::new(SymKind::Eq, &p.name, r, *line as i64)); + } else { + let s = self.new_sym(Sym::new(SymKind::Value, &p.name, r, *line as i64)); + self.envs[inner].vars.insert(p.name.clone(), s); + } + } + self.comp("fn", env, Some(name.clone()), None, |c| { + c.block(decls, &[], inner, Ctx { in_fn: true, ..Ctx::reactive() }); + c.expr(tail, inner, Ctx { in_fn: true, ..Ctx::reactive() }, None); + }); + let s = self.lookup(env, name).unwrap(); + let sym = &mut self.syms[s]; + sym.fn_tail = Some((tail, inner)); + sym.fails_t = Some(FailsT::Expr { e: tail, env: inner, cache: None, busy: false }); + sym.rows = Some(RowsT::Expr { e: tail, env: inner, cache: None, busy: false }); + } + Decl::Component { name, props, decls, view, line, .. } => { + let s = self.lookup(env, name).unwrap(); + let r = self.syms[s].comp_region.unwrap(); + let inner = self.env(Some(env), r); + self.bind(inner, props, Sym::new(SymKind::Props, props, r, *line as i64)); + self.syms[s].comp_env = Some(inner); + self.block(decls, view, inner, Ctx::reactive()); + self.later.push(Later::Content { ms: view, env: inner, anc: vec![] }); + } + } + } + + /// C-3 (ruled 2026-10-23): an apply block's own statements may not write a source its compute half reads. + fn own_source_writes(&mut self, inputs: &'a [Expr], body: &'a [Stmt], env: EId) { + let mut read: IndexMap = IndexMap::new(); + let mut seen: HashSet = HashSet::new(); + for i in inputs { + self.osw_visit(i, env, "", &mut read, &mut seen); + } + self.osw_direct(body, env, &read); + } + fn osw_visit(&self, e: &Expr, scope: EId, via: &str, read: &mut IndexMap, seen: &mut HashSet) { + for n0 in names_of(e) { + let Some(s) = self.lookup(scope, strip_pre(&n0)) else { continue }; + if !seen.insert(s) { + continue; + } + let path = if via.is_empty() { self.syms[s].name.clone() } else { format!("{} → {}", via, self.syms[s].name) }; + if self.syms[s].kind == SymKind::Source && !read.contains_key(&s) { + read.insert(s, path.clone()); + } + if let Some((x, xe)) = self.syms[s].expr { + self.osw_visit(x, xe, &path, read, seen); + } + } + } + fn osw_direct(&mut self, ss: &'a [Stmt], env: EId, read: &IndexMap) { + for st in ss { + match st { + Stmt::Write { target, .. } | Stmt::Optimistic { target, .. } if !target.is_ctx() => { + if let Some(t) = self.lookup(env, &target.name) { + if let Some(path) = read.get(&t) { + let n = self.syms[t].name.clone(); + self.err( + "EFFECT_WRITES_OWN_SOURCE", + "C-3", + format!( + "the apply block writes {}, which its compute half reads ({}): a loop across instants — derive it as an equation, or as `source {} = …` (ST-5), or write it from the event that changes the inputs", + n, path, n + ), + ); + } + } + } + Stmt::Sif { t, f, .. } => { + self.osw_direct(t, env, read); + self.osw_direct(f, env, read); + } + Stmt::Try { body, handler, fin, .. } => { + self.osw_direct(body, env, read); + if let Some(h) = handler { + self.osw_direct(h, env, read); + } + if let Some(f) = fin { + self.osw_direct(f, env, read); + } + } + Stmt::Action(body) => self.osw_direct(body, env, read), + _ => {} + } + } + } + + /// E-10: a reactive argument for a value parameter of an instantiating fn is snapshotted. + fn snapshot_args(&mut self, call: &'a Expr, f: SId, env: EId) { + let Expr::Call { args, .. } = call else { return }; + let params = self.syms[f].params.clone().unwrap_or_default(); + let fname = self.syms[f].name.clone(); + for (i, p) in params.iter().enumerate() { + let Some(a) = args.get(i) else { continue }; + if p.r#ref { + continue; + } + let mut reads = IndexSet::new(); + reads_outside_peek(Node::E(a), &mut reads); + let hit = reads.iter().find(|n| self.lookup(env, n).is_some_and(|s| self.syms[s].kind.reactive())).cloned(); + if let Some(h) = hit { + self.err( + "SNAPSHOT_ARGUMENT", + "E-10", + format!("{}(…): the argument for value parameter {} reads {} and would be snapshotted — declare the parameter `ref`, or pass peek(…)", fname, p.name, h), + ); + } + } + } + + /// CN-4a: a written row narrower than the inferred one. + fn row_check(&mut self, name: &str, ty: Option<&str>, env: EId) { + let Some(ty) = ty else { return }; + let Some(m) = ROW_WORDS.find(ty) else { return }; + let m0 = js_trim(m.as_str()).to_string(); + let words = split_rows(&m0); + let written: HashSet = words.iter().map(|w| match w.find('(') { Some(i) => w[..i].to_string(), None => w.clone() }).collect(); + let inferred = match self.envs[env].vars.get(name).copied() { + Some(s) if self.syms[s].rows.is_some() => self.sym_rows(s), + _ => IndexSet::new(), + }; + for r in inferred { + if !written.contains(&r) { + self.err("ROW_NARROWER_THAN_INFERRED", "CN-4a", format!("{} is declared `{}` but its value is also `{}`", name, m0, r)); + } + } + let fw = words.iter().find(|w| w.starts_with("fails(")).cloned(); + let line = self.line; + self.later.push(Later::RowFails { name: name.to_string(), env, fw, written: m0, line }); + } + /// R-f-1: a name in a catch, a boundary or an import's `fails` must be an error class. + fn err_class(&mut self, n: &str, env: EId) { + if n == "Error" || n == "Panic" { + return; + } + let ok = self.lookup(env, n).is_some_and(|s| matches!(self.syms[s].kind, SymKind::ErrClass | SymKind::Import)); + if !ok { + self.err("NOT_AN_ERROR_CLASS", "R-f-1", format!("{} is not an error class (declare it: `error {}(…)`)", n, n)); + } + } + + // ---- expressions ---- + fn resolve(&mut self, name: &str, env: EId, ctx: Ctx) -> Option { + if let Some(s) = self.lookup(env, name) { + self.sided(s, ctx); + return Some(s); + } + if BUILTINS.contains(&name) { + return None; + } + if ctx.imperative && (name == "log" || DOLLAR_PARAM.is_match(name)) { + return None; + } + self.err("UNBOUND", "SCOPE", format!("{} is not declared here (a component sees its declaration site, not its caller — RG-7a)", name)); + None + } + /// 12 SF-3/SF-5: what a name may be on this side. + fn sided(&mut self, s: SId, ctx: Ctx) { + let (name, kind, side) = (self.syms[s].name.clone(), self.syms[s].kind, self.syms[s].side); + if !ctx.server { + if side == Some(Side::Server) { + self.err("SERVER_ONLY", "SF-5a", format!("{} is `server` JS: it is not in the client bundle — call it in a `server fn` body", name)); + } + return; + } + if side == Some(Side::Client) { + self.err("CLIENT_IN_SERVER", "SF-5c", format!("{} is `client` JS and this is a server function's body", name)); + return; + } + let what = if kind.reactive() { + Some(match kind { + SymKind::Source => "a source", + SymKind::Eq => "an equation", + _ => "the component's props", + }) + } else { + match kind { + SymKind::Context => Some("a context"), + SymKind::Comp => Some("a component"), + SymKind::Action => Some("a client action"), + _ => None, + } + }; + let via = if what.is_some() { None } else if kind == SymKind::Fn { self.reach(s, &mut HashSet::new()) } else { None }; + if what.is_some() || via.is_some() { + let w = match what { + Some(w) => w.to_string(), + None => format!("a fn that reads {}", via.unwrap()), + }; + self.err( + "SERVER_CAPTURE", + "SF-3", + format!("a server function's body names {}, {}: it runs in its own request on the server, where that is not the client's — pass what it needs as an argument", name, w), + ); + } + } + /// SF-3: the reactive state a value fn reads, through the fns it calls; `None` if none. + fn reach(&self, f: SId, seen: &mut HashSet) -> Option { + if seen.contains(&f) { + return None; + } + let (Some(d), Some(denv)) = (self.syms[f].fn_decl, self.syms[f].decl_env) else { return None }; + seen.insert(f); + let Decl::Fn { params, decls, tail, .. } = d else { return None }; + let mut bound: HashSet = params.iter().map(|p| p.name.clone()).collect(); + for x in decls { + if let Some(n) = x.name() { + bound.insert(n.to_string()); + } + } + let mut names = IndexSet::new(); + for x in decls { + match x { + Decl::Eq { e, .. } => free_names(e, &bound, &mut names), + Decl::Source { init, .. } => free_names(init, &bound, &mut names), + _ => {} + } + } + free_names(tail, &bound, &mut names); + let text = serde_json::Value::Array(vec![decls_json(decls), tail.to_json()]).to_string(); + if let Some(u) = USE_JSON.captures(&text) { + return Some(format!("use {}", &u[1])); + } + for n0 in names { + let n = strip_pre(&n0); + let Some(s) = self.lookup(denv, n) else { continue }; + let k = self.syms[s].kind; + if k.reactive() || k == SymKind::Context { + return Some(n.to_string()); + } + if k == SymKind::Fn { + if let Some(r) = self.reach(s, seen) { + return Some(format!("{} → {}", n, r)); + } + } + } + None + } + + /// `field`: the member read of an identifier (`props.title`), for the model (CP-6). + fn expr(&mut self, e: &'a Expr, env: EId, ctx: Ctx, field: Option<&str>) { + match e { + Expr::Lit(_) => {} + Expr::Id(name) => { + let s = self.resolve(name, env, ctx); + if let Some(s) = s { + self.read(s, ctx, false, field); + match self.syms[s].kind { + SymKind::Comp => self.err("COMPONENT_AS_VALUE", "RG-4a", format!("component {} used as a value: apply it in a view", name)), + SymKind::Context => self.err("CONTEXT_AS_VALUE", "RG-7", format!("context {} used as a value: read the nearest provision with `use {}`", name, name)), + _ => {} + } + } + } + Expr::Pre { name, init } => { + let s = self.resolve(name, env, ctx); + if let Some(s) = s { + self.read(s, ctx, true, None); + if !self.syms[s].kind.reactive() { + self.err("PRE_OF_NON_REACTIVE", "C-2", format!("pre of {}, which is not reactive", name)); + } + } + if let Some(i) = init { + self.expr(i, env, ctx, None); + } + } + Expr::Use(name) => { + if ctx.server { + self.err("SERVER_CAPTURE", "SF-3", format!("a server function's body reads `use {}`: a provision is the client's — pass it as an argument", name)); + } + let c = self.ctx_of(name, env); + if let Some(c) = c { + self.read(c, ctx, false, None); + let info = self.syms[c].ctx.clone().unwrap(); + if !info.dflt { + let region = self.reg(env); + self.uses.push(UseRec { region, name: info.key, line: self.line, display: name.clone() }); + } + } + } + Expr::Peek(x) => { + self.model.snap += 1; + self.expr(x, env, ctx, None); + self.model.snap -= 1; + } + Expr::Latest(x) | Expr::Pending(x) => { + let save = self.model.probe; + self.model.probe = Some(if matches!(e, Expr::Latest(_)) { "latest" } else { "pending" }); + self.expr(x, env, ctx, None); + self.model.probe = save; + } + Expr::Un { e: x, .. } | Expr::Spread(x) => self.expr(x, env, ctx, None), + Expr::Await { e: inner, init, .. } => { + if ctx.imperative && !self.stmt_awaits.contains(&(e as *const Expr)) { + let sh = show(e); + self.err( + "AWAIT_IN_EXPRESSION", + "AC-1", + format!("`{}` inside an expression of imperative scope: a run suspends only at a statement — bind it first, `let v = {}` (a `catch (e) => alt` may follow it)", sh, sh), + ); + } + self.awaited_calls.insert(&**inner as *const Expr); + self.expr(inner, env, ctx, None); + if let Some(i) = init { + self.expr(i, env, ctx, None); + } + } + Expr::ServerFn { params, body, .. } => { + let region = self.reg(env); + let inner = self.env(Some(env), region); + for p in params { + self.set_value(inner, p, self.line); + } + self.comp("server", env, None, None, |c| c.stmts(body, inner, Ctx { server: true, ..Ctx::imperative() })); + if let Some(&sym) = self.server_exprs.get(&(e as *const Expr)) { + self.syms[sym].server_rejects = Some(ServerRejects::Body { body, env: inner, cache: None }); + } + } + Expr::Throw(x) => self.expr(x, env, ctx, None), + Expr::Catch { e: inner, name, classes, alt } => { + self.expr(inner, env, ctx, None); + if let Some(cs) = classes { + for n in cs { + self.err_class(n, env); + } + } + let region = self.reg(env); + let ienv = self.env(Some(env), region); + if let Some(n) = name { + self.set_value(ienv, n, self.line); + } + self.expr(alt, ienv, ctx, None); + } + Expr::Bin { l, r, .. } => { + self.expr(l, env, ctx, None); + self.expr(r, env, ctx, None); + } + Expr::If { c, t, f } => { + self.expr(c, env, ctx, None); + self.expr(t, env, ctx, None); + self.expr(f, env, ctx, None); + } + Expr::Tpl(parts) => { + for p in parts { + if let TplPart::Expr(x) = p { + self.expr(x, env, ctx, None); + } + } + } + Expr::Member { o, p, .. } => { + if let Expr::Id(_) = &**o { + self.expr(o, env, ctx, Some(p)); + } else { + self.expr(o, env, ctx, None); + } + } + Expr::Index { o, i, .. } => { + self.expr(o, env, ctx, None); + self.expr(i, env, ctx, None); + } + Expr::Arr(items) => { + for x in items { + self.expr(x, env, ctx, None); + } + } + Expr::Rec(fields) => { + for f in fields { + match f { + Field::Kv { v, .. } => self.expr(v, env, ctx, None), + Field::Spread(x) => self.expr(x, env, ctx, None), + } + } + } + Expr::Call { f, args } => { + if let Expr::Id(fname) = &**f { + if fname == "until" && self.lookup(env, "until").is_none() { + if !ctx.imperative { + self.err("IO_IN_REACTIVE", "AC-6", "until(…) waits for the world to confirm a condition: it is called from imperative scope (an action or a handler), and awaited".into()); + } + let ok = args.len() == 1 && matches!(&args[0], Expr::Fn { params, .. } if params.is_empty()); + if !ok { + self.err("UNTIL_PREDICATE", "AC-6", "until takes a predicate, `until(() => e)`: a value would be read once and never change".into()); + for a in args { + self.expr(a, env, ctx, None); + } + return; + } + self.expr(&args[0], env, Ctx { server: ctx.server, ..Ctx::reactive() }, None); + return; + } + let s = self.lookup(env, fname); + let (io, kind, side, io_via, instantiating) = match s { + Some(s) => (self.syms[s].io, Some(self.syms[s].kind), self.syms[s].side, self.syms[s].io_via.clone(), self.syms[s].instantiating), + None => (false, None, None, None, false), + }; + if !ctx.imperative && (io || (s.is_none() && fname == "log")) { + if ctx.in_fn { + self.err("IO_IN_FN", "R-io-2", format!("{}(…) is io; a fn is pure by definition", fname)); + } else { + let code = if kind == Some(SymKind::Action) { "ACTION_CALLED_IN_OWNED_SCOPE" } else { "IO_IN_REACTIVE" }; + self.err(code, "IO-2", format!("{}(…) is io and is called in reactive scope (R-io-3){}", fname, io_via.map(|v| format!(" — its server body calls {}", v)).unwrap_or_default())); + } + } + if side == Some(Side::Client) && !ctx.imperative && !ctx.server && !self.awaited_calls.contains(&(e as *const Expr)) { + self.err( + "CLIENT_NOT_AWAITED", + "SF-5b", + format!("{}(…) is `client` JS: in reactive scope it is awaited — a client question, asked once the browser has the page (SV-7b) — never called for its value now", fname), + ); + } + if kind == Some(SymKind::Fn) && instantiating { + self.err("INSTANTIATION_NOT_BOUND", "E-9", format!("{}(…) declares state: bind it, `x = {}(…)` (RG-6)", fname, fname)); + } + if kind == Some(SymKind::Comp) { + self.err("COMPONENT_AS_VALUE", "RG-4a", format!("component {} called as a function: apply it in a view", fname)); + } + self.model.call(s, kind.map(|k| k.as_str()).unwrap_or("")); + } + self.expr(f, env, ctx, None); + for a in args { + self.expr(a, env, ctx, None); + } + } + Expr::Fn { params, body, stmts } => { + let region = self.reg(env); + let inner = self.env(Some(env), region); + for p in params { + self.set_value(inner, p, self.line); + } + if let Some(ss) = stmts { + if !ctx.imperative { + self.err("REACTIVE_WRITE_IN_OWNED_SCOPE", "W-2", "a statement-bodied closure in reactive scope (SC-3: a closure created here is a pure function)".into()); + return; + } + self.stmts(ss, inner, Ctx { server: ctx.server, ..Ctx::imperative() }); + return; + } + self.expr(body, inner, Ctx { imperative: ctx.imperative, in_fn: ctx.in_fn, server: ctx.server, in_action: false }, None); + } + Expr::Draft { seed, body } => { + self.expr(seed, env, ctx, None); + let region = self.reg(env); + let inner = self.env(Some(env), region); + self.set_value(inner, "d", self.line); + for s in body { + match s { + Stmt::Let { name, e, .. } => { + self.expr(e, inner, ctx, None); + self.set_value(inner, name, self.line); + } + Stmt::Expr(x) => self.expr(x, inner, ctx, None), + Stmt::Write { target, e } => { + if target.name != "d" || target.is_ctx() { + self.err("DRAFT_WRITE_OUTSIDE_DRAFT", "ST-3a", format!("a draft block writes only its draft, not {}{}", if target.is_ctx() { "use " } else { "" }, target.name)); + } + for p in &target.path { + if let PathSeg::Expr(x) = p { + self.expr(x, inner, ctx, None); + } + } + self.expr(e, inner, ctx, None); + } + other => self.err("ASYNC_STORE_SETTER", "ST-3b", format!("{} in a draft block: it is reactive scope (only let, expressions and writes to d)", other.kind())), + } + } + } + Expr::StyleMap { .. } => {} + } + } + + // ---- imperative scope (SC-2) ---- + fn stmts(&mut self, ss: &'a [Stmt], env0: EId, ctx: Ctx) { + let region = self.reg(env0); + let env = self.env(Some(env0), region); + for s in ss { + if let Stmt::Let { e, .. } | Stmt::Expr(e) = s { + let t = if let Expr::Catch { e: inner, .. } = e { &**inner } else { e }; + if let Expr::Await { .. } = t { + self.stmt_awaits.insert(t as *const Expr); + } + } + match s { + Stmt::Let { name, e, .. } => { + self.expr(e, env, ctx, None); + let captures = self.stored(e, env); + let mut sym = Sym::new(SymKind::Value, name, region, self.line); + sym.captures = captures; + let id = self.new_sym(sym); + self.envs[env].vars.insert(name.clone(), id); + } + Stmt::Expr(e) | Stmt::Throw(e) => self.expr(e, env, ctx, None), + Stmt::Return(e) => { + if let Some(e) = e { + self.expr(e, env, ctx, None); + } + } + Stmt::Write { target, e } | Stmt::Optimistic { target, e } => { + let op = if matches!(s, Stmt::Optimistic { .. }) { "optimistic" } else { "write" }; + if ctx.server { + self.err( + "SERVER_WRITE", + "SF-4", + format!( + "a server function writes {}{}: return the value, and write it in the action that awaited the call (`let r = await f(…); {} := r`)", + if target.is_ctx() { "use " } else { "" }, + target.name, + target.name + ), + ); + for p in &target.path { + if let PathSeg::Expr(x) = p { + self.expr(x, env, ctx, None); + } + } + self.expr(e, env, ctx, None); + continue; + } + let mut tsym = None; + if target.is_ctx() { + if let Some(c) = self.ctx_of(&target.name, env) { + self.model.write(Some(c), op, !target.path.is_empty()); + let info = self.syms[c].ctx.clone().unwrap(); + self.uses.push(UseRec { region, name: info.key.clone(), line: self.line, display: target.name.clone() }); + if !info.source { + self.err( + "WRITE_TO_NON_SOURCE", + "RG-7c", + format!("context {} is not declared `source`: what is provided is read-only (`context {}: source T` to write through it)", target.name, target.name), + ); + } + } + } else { + tsym = self.resolve(&target.name, env, ctx); + self.model.write(tsym, op, !target.path.is_empty()); + if let Some(t) = tsym { + let k = self.syms[t].kind; + if k != SymKind::Source { + let what = match k { + SymKind::Eq => "an equation".to_string(), + SymKind::Props => "props".to_string(), + k => format!("a {}", k.as_str()), + }; + self.err("WRITE_TO_NON_SOURCE", "W-1", format!("{} is {}: only sources are written (W-1)", target.name, what)); + } else if let Some(imp) = self.syms[t].imported.clone() { + self.err( + "WRITE_TO_NON_SOURCE", + "MO-2", + format!("{} is a source of {}: a module boundary is a value boundary — write it through an action {} exports", target.name, imp, imp), + ); + } + } + } + if op == "optimistic" { + if !ctx.in_action { + self.err("OPTIMISTIC_OUTSIDE_HOLD", "AC-3", "optimistic outside an action body: a guess needs a hold to live in".into()); + } + if let Some(t) = tsym { + if self.syms[t].kind == SymKind::Source && !self.syms[t].question { + self.err( + "OPTIMISTIC_NO_QUESTION", + "AC-3", + format!("{} is declared with := — a guess needs a question to confirm it (source {} = …, ST-5c)", target.name, target.name), + ); + } + } + } + for p in &target.path { + if let PathSeg::Expr(x) = p { + self.expr(x, env, ctx, None); + } + } + self.expr(e, env, ctx, None); + self.escape(target, e, tsym, env); + } + Stmt::Refresh(name) | Stmt::Affects(name) => { + let is_refresh = matches!(s, Stmt::Refresh(_)); + if ctx.server { + self.err( + "SERVER_CAPTURE", + "SF-3", + format!("`{} {}` in a server function's body: it is the client's (do it in the action that awaited the call)", if is_refresh { "refresh" } else { "affects" }, name), + ); + continue; + } + let t = self.resolve(name, env, ctx); + if is_refresh { + self.model.write(t, "refresh", false); + if let Some(t) = t { + let k = self.syms[t].kind; + if !(k == SymKind::Eq || (k == SymKind::Source && self.syms[t].question)) { + self.err("INVALID_REFRESH_TARGET", "AC-4", format!("{} has no question to re-ask", name)); + } + } + } else { + self.model.write(t, "affects", false); + if let Some(t) = t { + if !self.syms[t].kind.reactive() { + self.err("INVALID_AFFECTS_TARGET", "AC-5", format!("{} is not reactive", name)); + } + } + } + } + Stmt::Action(body) => { + if ctx.server { + self.err("SERVER_CAPTURE", "SF-3", "`action { … }` in a server function's body: it is the client's (do it in the action that awaited the call)".into()); + continue; + } + self.comp("action", env, Some("(inline)".into()), None, |c| c.stmts(body, env, Ctx { in_action: true, ..Ctx::imperative() })); + self.actions.push(ActionRec { name: "(inline)".into(), body, env, line: self.line }); + } + Stmt::Sif { c, t, f } => { + self.expr(c, env, ctx, None); + self.stmts(t, env, ctx); + self.stmts(f, env, ctx); + } + Stmt::Try { body, catch_name, catch_classes, handler, fin } => { + self.stmts(body, env, ctx); + if let Some(cs) = catch_classes { + for n in cs { + self.err_class(n, env); + } + } + if let Some(h) = handler { + let henv = self.env(Some(env), region); + if let Some(n) = catch_name { + self.set_value(henv, n, self.line); + } + self.stmts(h, henv, ctx); + } + if let Some(f) = fin { + self.stmts(f, env, ctx); + } + } + } + } + } + + /// The reactive names captured by closures that a value stores (O-3). + fn stored(&self, e: &'a Expr, env: EId) -> Vec { + let mut out = vec![]; + self.stored_walk(Node::E(e), env, &mut out); + out + } + fn stored_walk(&self, x: Node<'a>, env: EId, out: &mut Vec) { + match x { + Node::E(f @ Expr::Fn { .. }) => { + for n in names_of(f) { + if let Some(s) = self.lookup(env, strip_pre(&n)) { + if self.syms[s].kind.reactive() { + out.push(s); + } + out.extend(self.syms[s].captures.iter().copied()); + } + } + } + Node::E(Expr::Id(n)) => { + if let Some(s) = self.lookup(env, n) { + out.extend(self.syms[s].captures.iter().copied()); + } + } + _ => { + for c in children(x, false) { + self.stored_walk(c, env, out); + } + } + } + } + fn escape(&mut self, target: &Target, e: &'a Expr, tsym: Option, env: EId) { + let value = match e { + Expr::Fn { body, stmts: None, .. } => &**body, + _ => e, + }; + let captured = self.stored(value, env); + if captured.is_empty() { + return; + } + let t_depth = match tsym { + Some(t) => self.regions[self.syms[t].region].depth, + None => { + let key = self.lookup(env, &target.name).and_then(|s| self.syms[s].ctx.as_ref().map(|c| c.key.clone())).unwrap_or_else(|| target.name.clone()); + let mut r = Some(self.reg(env)); + let mut found = None; + while let Some(ri) = r { + if self.regions[ri].provides.contains(&key) { + found = Some(ri); + break; + } + if self.regions[ri].is_component_root { + break; + } + r = self.regions[ri].parent; + } + match found { + Some(f) => self.regions[f].depth, + None => match self.regions[self.reg(env)].component { + Some(c) => self.regions[c].depth - 1, + None => 0, + }, + } + } + }; + if let Some(&leak) = captured.iter().find(|&&c| self.regions[self.syms[c].region].depth > t_depth) { + let ln = self.syms[leak].name.clone(); + self.err( + "REGION_ESCAPE", + "O-3", + format!("a closure capturing {} (declared in a region that ends sooner) is stored into {}{}, which outlives it", ln, if target.is_ctx() { "use " } else { "" }, target.name), + ); + } + } + + // ---- views ---- + fn markup(&mut self, ms: &'a [Markup], env: EId, view_root: bool) { + let mut styles = 0; + for m in ms { + if let Some(l) = m.line { + self.line = l as i64; + } + let region = self.reg(env); + match &m.kind { + MarkupKind::Text(_) => {} + MarkupKind::Style(_) => { + if !view_root { + self.err("STYLE_PLACEMENT", "CSS-1", "a `, with no attributes"); + } + let Some(end) = c.rest().find("") else { return c.fail("expected ") }; + let css = c.rest()[..end].to_string(); + c.i += end + "".len(); + return Ok(Expr::StyleMap { css, line }); + } + if SERVER_FN_EXPR.is_match(c.rest()) { + c.keyword("server"); + c.ws(true); + c.keyword("fn"); + c.ws(true); + c.expect("(")?; + c.ws(true); + let (params, ptypes) = params_typed(c)?; + c.expect(")")?; + let ret = opt_type(c, true)?; + c.ws(true); + let body = parse_block(c)?; + return Ok(Expr::ServerFn { params, ptypes, ret, body }); + } + let ch = c.peek(); + if ch == '`' { + c.bump(); + let mut parts = vec![]; + let mut s = String::new(); + while !c.eof() && c.peek() != '`' { + if c.starts_with("${") { + c.i += 2; + if !s.is_empty() { + parts.push(TplPart::Str(std::mem::take(&mut s))); + } + c.ws(true); + parts.push(TplPart::Expr(parse_expr(c)?)); + c.ws(true); + c.expect("}")?; + continue; + } + if c.peek() == '\\' { + c.bump(); + } + if !c.eof() { + s.push(c.peek()); + } + c.bump(); + } + c.expect("`")?; + if !s.is_empty() || parts.is_empty() { + parts.push(TplPart::Str(s)); + } + return Ok(Expr::Tpl(parts)); + } + if ch == '"' || ch == '\'' { + c.bump(); + let mut s = String::new(); + while !c.eof() && c.peek() != ch { + if c.peek() == '\\' { + c.bump(); + } + if !c.eof() { + s.push(c.peek()); + } + c.bump(); + } + c.expect(&ch.to_string())?; + return Ok(Expr::Lit(Lit::Str(s))); + } + if let Some(m) = NUMBER.find(c.rest()) { + let text = m.as_str().to_string(); + c.i += text.len(); + return Ok(Expr::Lit(Lit::Num(text.replace('_', "").parse::().unwrap_or(f64::NAN)))); + } + if c.eat("(") { + c.ws(true); + let e = parse_expr(c)?; + c.ws(true); + c.expect(")")?; + return Ok(e); + } + if c.eat("[") { + c.ws(true); + let mut items = vec![]; + while !c.starts_with("]") { + if c.eat("...") { + c.ws(true); + items.push(Expr::Spread(Box::new(parse_expr(c)?))); + } else { + items.push(parse_expr(c)?); + } + c.ws(true); + if !c.eat(",") { + break; + } + c.ws(true); + } + c.expect("]")?; + return Ok(Expr::Arr(items)); + } + if c.eat("{") { + c.ws(true); + let mut fields = vec![]; + while !c.starts_with("}") { + if c.eat("...") { + c.ws(true); + fields.push(Field::Spread(parse_expr(c)?)); + c.ws(true); + if !c.eat(",") { + break; + } + c.ws(true); + continue; + } + let key = match c.ident() { + Some(k) => k, + None => { + if c.peek() == '"' || c.peek() == '\'' { + match parse_primary(c)? { + Expr::Lit(Lit::Str(s)) => s, + _ => unreachable!(), + } + } else { + return c.fail("field"); + } + } + }; + c.ws(true); + if c.eat(":") { + c.ws(true); + let v = parse_expr(c)?; + fields.push(Field::Kv { key, v }); + } else { + fields.push(Field::Kv { v: Expr::Id(key.clone()), key }); + } + c.ws(true); + if !c.eat(",") { + break; + } + c.ws(true); + } + c.expect("}")?; + return Ok(Expr::Rec(fields)); + } + if c.keyword("true") { + return Ok(Expr::Lit(Lit::Bool(true))); + } + if c.keyword("false") { + return Ok(Expr::Lit(Lit::Bool(false))); + } + if c.keyword("undefined") { + return Ok(Expr::Lit(Lit::Undefined)); + } + if c.keyword("null") { + return Ok(Expr::Lit(Lit::Null)); + } + if let Some(id) = c.ident() { + return Ok(Expr::Id(id)); + } + c.fail("expression") +} + +// ---- markup ---- +#[derive(Clone)] +enum Close { + /// the top level: markup runs to the end of input + Top, + /// a control form's body, up to `}` + Brace, + /// an element, up to `` (`""`: a fragment's ``) + Tag(String), +} + +fn parse_markup(c: &mut Cursor, close: Close) -> R> { + let mut out: Vec = vec![]; + let in_element = matches!(close, Close::Tag(_)); + let close_text = match &close { + Close::Top => None, + Close::Brace => Some("}".to_string()), + Close::Tag(t) => Some(t.clone()), + }; + let mut broke = false; + loop { + let start = c.i; + if in_element { + let mut s = String::new(); + while !c.eof() && " \t\r\n".contains(c.peek()) { + s.push(c.peek()); + c.bump(); + } + broke = s.contains('\n'); + let tag = close_text.as_deref().unwrap(); + if !s.is_empty() && !broke && !c.starts_with(&format!("", tag)) && !out.is_empty() { + out.push(Markup { kind: MarkupKind::Text(" ".into()), line: None }); + } + } else { + c.ws(true); + } + if c.eof() { + if let Some(t) = &close_text { + return c.fail(&format!("expected ", t)); + } + return Ok(out); + } + if let Close::Tag(t) = &close { + if c.starts_with(&format!("", t)) { + c.i += t.len() + 3; + return Ok(out); + } + } + if matches!(close, Close::Brace) && c.starts_with("}") { + c.bump(); + return Ok(out); + } + let line = c.line(); + if c.starts_with("<") { + let mut m = parse_element(c)?; + m.line = Some(line); + out.push(m); + continue; + } + if c.starts_with("{") { + c.bump(); + c.ws(true); + if let Some(mut ctl) = parse_control(c)? { + c.ws(true); + c.expect("}")?; + ctl.line = Some(line); + out.push(ctl); + continue; + } + let e = parse_expr(c)?; + c.ws(true); + c.expect("}")?; + out.push(Markup { kind: MarkupKind::Read(e), line: Some(line) }); + continue; + } + if !in_element { + if let Some(mut ctl) = parse_control(c)? { + ctl.line = Some(line); + out.push(ctl); + continue; + } + } + if matches!(close, Close::Top) && looks_like_decl(c) { + return c.fail("declaration after markup: a block is declarations, then its view"); + } + let mut s = String::new(); + while !c.eof() && !"<{}\n".contains(c.peek()) { + s.push(c.peek()); + c.bump(); + } + if c.peek() == '\n' || c.eof() { + s = s.trim_end_matches([' ', '\t', '\r']).to_string(); + } + if !in_element { + if js_trim(&s).is_empty() { + if c.peek() == '\n' { + c.bump(); + } + if c.i == start { + // the reference loops here forever (a stray `}`); this ends it + return c.fail("declaration or markup"); + } + continue; + } + if s.contains(":=") { + return c.fail("W-2: write in reactive scope (no statement position)"); + } + return c.fail("declaration or markup"); + } + if broke && matches!(out.last(), Some(Markup { kind: MarkupKind::Text(_), .. })) { + out.push(Markup { kind: MarkupKind::Text(" ".into()), line: None }); + } + if !s.is_empty() { + out.push(Markup { kind: MarkupKind::Text(s), line: None }); + } + if c.i == start { + // the reference loops here forever (a stray `}` in an element); this ends it + return c.fail("declaration or markup"); + } + } +} + +/// Control forms of a view: `if c { … } else { … }`, `for x in e { … }`, `loading … { … }`, `errored …`, `portal …`. +fn parse_control(c: &mut Cursor) -> R> { + let save = c.i; + let mk = |kind| Some(Markup { kind, line: None }); + if c.keyword("if") { + c.ws(true); + let cond = parse_expr(c)?; + c.ws(true); + if !c.starts_with("{") { + c.i = save; + return Ok(None); + } + c.expect("{")?; + let (t_decls, t) = parse_block_body(c)?; + c.ws(true); + let (mut f_decls, mut f) = (vec![], vec![]); + if c.keyword("else") { + c.ws(true); + c.expect("{")?; + (f_decls, f) = parse_block_body(c)?; + } + return Ok(mk(MarkupKind::Mif { c: cond, t, t_decls, f, f_decls })); + } + if c.keyword("loading") { + c.ws(true); + let mut on = None; + if c.keyword("on") { + c.ws(true); + c.expect("(")?; + c.ws(true); + on = Some(parse_expr(c)?); + c.ws(true); + c.expect(")")?; + c.ws(true); + } + if !c.keyword("fallback") { + return c.fail("fallback"); + } + c.ws(true); + c.expect("{")?; + let fallback = parse_markup(c, Close::Brace)?; + c.ws(true); + c.expect("{")?; + let (decls, body) = parse_block_body(c)?; + return Ok(mk(MarkupKind::Loading { on, fallback, decls, body })); + } + if c.keyword("portal") { + c.ws(true); + let mut to = None; + if c.keyword("to") { + c.ws(true); + c.expect("(")?; + c.ws(true); + to = Some(parse_expr(c)?); + c.ws(true); + c.expect(")")?; + c.ws(true); + } + c.expect("{")?; + let (decls, body) = parse_block_body(c)?; + return Ok(mk(MarkupKind::Portal { to, decls, body })); + } + if c.keyword("errored") { + c.ws(true); + if !c.keyword("fallback") { + return c.fail("fallback"); + } + c.ws(false); + let mut err_name = None; + let mut classes = None; + if c.eat("(") { + c.ws(true); + err_name = Some(c.ident_or("error name")?); + c.ws(false); + if c.eat(":") { + classes = parse_classes(c)?; + } + c.ws(true); + c.expect(")")?; + } + c.ws(true); + c.expect("{")?; + let fallback = parse_markup(c, Close::Brace)?; + c.ws(true); + c.expect("{")?; + let (decls, body) = parse_block_body(c)?; + return Ok(mk(MarkupKind::Errored { fallback, decls, body, err_name, classes })); + } + if c.keyword("for") { + c.ws(true); + let name = c.ident_or("for name")?; + c.ws(true); + if !c.keyword("in") { + return c.fail("in"); + } + c.ws(true); + let xs = parse_expr(c)?; + c.ws(true); + c.expect("{")?; + let (decls, body) = parse_block_body(c)?; + return Ok(mk(MarkupKind::Mfor { name, xs, decls, body })); + } + Ok(None) +} +/// A reactive block body: declarations, then markup, up to the closing `}` (03). +fn parse_block_body(c: &mut Cursor) -> R<(Vec, Vec)> { + let mut decls = vec![]; + loop { + c.ws(true); + if !looks_like_decl(c) { + break; + } + decls.push(parse_decl(c)?); + c.ws(true); + c.eat(";"); + } + Ok((decls, parse_markup(c, Close::Brace)?)) +} +fn looks_like_decl(c: &mut Cursor) -> bool { + let save = c.i; + c.ws(true); + let r = c.starts_with("source ") + || c.starts_with("effect") + || c.starts_with("fn ") + || c.starts_with("component ") + || c.starts_with("deferred ") + || c.starts_with("action ") + || c.starts_with("provide ") + || c.starts_with("context ") + || c.starts_with("import ") + || c.starts_with("export ") + || c.starts_with("type ") + || c.starts_with("interface ") + || ERROR_DECL.is_match(c.rest()) + || SERVER_DECL.is_match(c.rest()) + || { + let id = c.ident(); + c.ws(false); + match id { + Some(id) if !is_kw(&id) => { + if c.peek() == ':' && c.peek_at(1) != '=' { + true + } else { + (c.peek() == '=' && c.peek_at(1) != '=') || EQ_KEY.is_match(c.rest()) + } + } + _ => false, + } + }; + c.i = save; + r +} +fn parse_element(c: &mut Cursor) -> R { + c.expect("<")?; + let mk = |kind| Markup { kind, line: None }; + if c.eat(">") { + let children = parse_markup(c, Close::Tag(String::new()))?; + return Ok(mk(MarkupKind::El { tag: String::new(), attrs: vec![], children })); + } + let Some(tag) = c.tag_name() else { return c.fail("tag") }; + if tag == "style" { + c.ws(true); + if !c.eat(">") { + return c.fail(if c.rest().starts_with("ref") { + "CSS-5: a class map is a declaration, `styles = ` — TSRX's ``" + }); + } + let Some(end) = c.rest().find("") else { return c.fail("expected ") }; + let css = c.rest()[..end].to_string(); + c.i += end + "".len(); + return Ok(mk(MarkupKind::Style(css))); + } + let mut attrs = vec![]; + loop { + c.ws(true); + if c.eat("/>") { + return Ok(mk(MarkupKind::El { tag, attrs, children: vec![] })); + } + if c.eat(">") { + break; + } + let Some(name) = c.tag_name() else { return c.fail("attr") }; + c.ws(false); + if !c.eat("=") { + attrs.push(Attr { name, e: Expr::Lit(Lit::Bool(true)) }); + continue; + } + c.ws(true); + if c.eat("{") { + c.ws(true); + let e = parse_expr(c)?; + attrs.push(Attr { name, e }); + c.ws(true); + c.expect("}")?; + } else { + let e = parse_primary(c)?; + attrs.push(Attr { name, e }); + } + } + let children = parse_markup(c, Close::Tag(tag.clone()))?; + Ok(mk(MarkupKind::El { tag, attrs, children })) +} diff --git a/compiler/src/signature.rs b/compiler/src/signature.rs new file mode 100644 index 0000000..b00f24e --- /dev/null +++ b/compiler/src/signature.rs @@ -0,0 +1,506 @@ +//! MO-3 / MO-9: a module's signature — what an importer is checked against — and its `.d.rx` text, printed and read back +//! (`ref/src/signature.ts`). One export per line: +//! +//! export source products: pending fails(Error) = … a question (ST-5); `:= …` a plain source +//! export discount: pending fails(Error) an equation; `io` when bound to an io value +//! export pure fn qtyOf(id) rejects(Offline) instantiates a fn (`ref` params; RG-6); `rejects(…)` absent: none +//! export server fn add(id) rejects(Error) 12 SF-1 (`server pure fn` when no io) +//! export action setCoupon +//! export error NotFound(id) +//! export type Product +//! export context theme: source pending Theme = … rows, then the written type; `= …` a default +//! export component Panel(props: { title; children: catches(Error) provides(./theme.rx#theme) }) fails(Error) uses(…) shape(…) + +use crate::content::Shape; +use indexmap::{IndexMap, IndexSet}; +use regex::Regex; +use std::sync::LazyLock; + +pub const HOLD: &str = "$hold"; + +/// What a catch or a boundary discharges: named classes, or every failure. +#[derive(Debug, Clone, PartialEq)] +pub enum Catch { + All, + Set(IndexSet), +} +impl Catch { + pub fn of(classes: &Option>) -> Catch { + match classes { + Some(c) => Catch::Set(c.iter().cloned().collect()), + None => Catch::All, + } + } + pub fn caught(&self, cls: &str) -> bool { + match self { + Catch::All => true, + Catch::Set(s) => cls != "Error" && s.contains(cls), + } + } + pub fn is_empty_set(&self) -> bool { + matches!(self, Catch::Set(s) if s.is_empty()) + } +} + +#[derive(Debug, Clone)] +pub struct PropSig { + pub f: Catch, + pub p: Catch, + pub rendered: bool, + pub action: Option<(Catch, String)>, +} +#[derive(Debug, Clone)] +pub struct Own { + pub cls: String, + pub what: String, + pub at: Option, +} +/// MO-3: an exported component as an importer sees it. +#[derive(Debug, Clone)] +pub struct CompSig { + pub own: Vec, + pub requires: Vec, + pub props: IndexMap, + pub shape: Option, +} +#[derive(Debug, Clone)] +pub struct SigParam { + pub name: String, + pub r#ref: bool, + pub ty: Option, +} +#[derive(Debug, Clone)] +pub enum ExportSig { + Source { question: bool, rows: Vec, fails: Vec }, + Eq { rows: Vec, fails: Vec, io: bool }, + Fn { params: Vec, instantiating: bool, rows: Vec, fails: Vec, rejects: Vec }, + Action, + Server { io: bool, params: Vec, rejects: Vec }, + ErrClass { params: Vec }, + Comp { sig: CompSig }, + Context { key: String, source: bool, dflt: bool, rows: Vec, fails: Vec, ty: Option }, + Type, +} +#[derive(Debug, Clone)] +pub struct ModSig { + pub file: String, + pub exports: IndexMap, +} + +fn sorted(xs: impl IntoIterator) -> Vec { + let mut v: Vec = xs.into_iter().collect(); + v.sort(); + v +} +fn cls(c: &Catch) -> String { + match c { + Catch::All => "*".into(), + Catch::Set(s) if s.is_empty() => "nothing".into(), + Catch::Set(s) => sorted(s.iter().cloned()).join(" | "), + } +} +fn rows(r: &[String], fails: &[String]) -> String { + let mut out: Vec = ["io", "pending", "lane"].iter().filter(|x| r.iter().any(|y| y == *x)).map(|x| x.to_string()).collect(); + if !fails.is_empty() { + out.push(format!("fails({})", sorted(fails.iter().cloned()).join(" | "))); + } + out.join(" ") +} +fn dir(p: &str) -> String { + let parts: Vec<&str> = p.split('/').collect(); + parts[..parts.len() - 1].join("/") +} +/// `[path, name]` at the last `#` (`slice(0, -1)` and the whole key when there is none, as the reference has it). +fn split_hash(key: &str) -> (String, String) { + match key.rfind('#') { + Some(i) => (key[..i].to_string(), key[i + 1..].to_string()), + None => { + let mut p: Vec = key.chars().collect(); + p.pop(); + (p.into_iter().collect(), key.to_string()) + } + } +} +/// A context key relative to the module whose signature names it (`./theme.rx#theme`). +fn rel_key(from: &str, key: &str) -> String { + if key == HOLD { + return "loading".into(); + } + let (path, name) = split_hash(key); + let d = dir(from); + let a: Vec<&str> = d.split('/').filter(|x| !x.is_empty()).collect(); + let b: Vec<&str> = path.split('/').filter(|x| !x.is_empty()).collect(); + let mut i = 0; + while i < a.len() && (i as i64) < b.len() as i64 - 1 && a[i] == b[i] { + i += 1; + } + let up = a.len() - i; + let rel = format!("{}{}", if up > 0 { "../".repeat(up) } else { "./".into() }, b[i.min(b.len())..].join("/")); + format!("{}#{}", if path.starts_with('/') == from.starts_with('/') { rel } else { path }, name) +} +fn abs_key(from: &str, rel: &str) -> String { + if rel == "loading" { + return HOLD.into(); + } + let (path, name) = split_hash(rel); + if !path.starts_with('.') { + return rel.into(); + } + let lead = if from.starts_with('/') { "/" } else { "" }; + let d = dir(from); + let mut out: Vec = d.split('/').filter(|x| !x.is_empty()).map(String::from).collect(); + for seg in path.split('/') { + if seg == "." || seg.is_empty() { + continue; + } + if seg == ".." { + out.pop(); + } else { + out.push(seg.into()); + } + } + format!("{}{}#{}", lead, out.join("/"), name) +} +fn shape_text(s: &Option) -> String { + let Some(s) = s else { return String::new() }; + let mut parts = vec![]; + if !s.roots.is_empty() { + parts.push(format!("roots {}", s.roots.join(" "))); + } + if s.closes_p { + parts.push("closes-p".into()); + } + if s.a { + parts.push("a".into()); + } + if s.form { + parts.push("form".into()); + } + for c in &s.children_at { + parts.push(format!("children-at {}", c.join(" "))); + } + if parts.is_empty() { String::new() } else { format!(" shape({})", parts.join("; ")) } +} +fn with_rows(head: String, r: String) -> String { + if r.is_empty() { head } else { format!("{}: {}", head, r) } +} + +fn line(file: &str, name: &str, x: &ExportSig) -> String { + match x { + ExportSig::Source { question, rows: r, fails } => format!("{} {} …", with_rows(format!("export source {}", name), rows(r, fails)), if *question { "=" } else { ":=" }), + ExportSig::Eq { rows: r, fails, io } => { + let mut rr = r.clone(); + if *io { + rr.insert(0, "io".into()); + } + with_rows(format!("export {}", name), rows(&rr, fails)) + } + ExportSig::Fn { params, instantiating, rows: r, fails, rejects } => { + let ps: Vec = params.iter().map(|p| format!("{}{}{}", if p.r#ref { "ref " } else { "" }, p.name, p.ty.as_ref().map(|t| format!(": {}", t)).unwrap_or_default())).collect(); + format!( + "{}{}{}", + with_rows(format!("export pure fn {}({})", name, ps.join(", ")), rows(r, fails)), + if rejects.is_empty() { String::new() } else { format!(" rejects({})", rejects.join(" | ")) }, + if *instantiating { " instantiates" } else { "" } + ) + } + ExportSig::Server { io, params, rejects } => format!( + "export server {}fn {}({}){}", + if *io { "" } else { "pure " }, + name, + params.join(", "), + if rejects.is_empty() { String::new() } else { format!(" rejects({})", sorted(rejects.iter().cloned()).join(" | ")) } + ), + ExportSig::Action => format!("export action {}", name), + ExportSig::ErrClass { params } => format!("export error {}({})", name, params.join(", ")), + ExportSig::Type => format!("export type {}", name), + ExportSig::Context { source, dflt, rows: r, fails, ty, .. } => { + let t: Vec = [if *source { "source".to_string() } else { String::new() }, rows(r, fails), ty.clone().unwrap_or_default()].into_iter().filter(|x| !x.is_empty()).collect(); + let t = t.join(" "); + format!("export context {}{}{}", name, if t.is_empty() { String::new() } else { format!(": {}", t) }, if *dflt { " = …" } else { "" }) + } + ExportSig::Comp { sig: s } => { + let props: Vec = s + .props + .iter() + .map(|(f, v)| { + let mut parts = vec![]; + if !v.rendered { + parts.push("unrendered".to_string()); + } else { + if !v.f.is_empty_set() { + parts.push(format!("catches({})", cls(&v.f))); + } + if !v.p.is_empty_set() { + parts.push(format!( + "provides({})", + match &v.p { + Catch::All => "*".to_string(), + Catch::Set(ks) => ks.iter().map(|k| rel_key(file, k)).collect::>().join(", "), + } + )); + } + } + if let Some((af, an)) = &v.action { + parts.push(format!("action {} catches({})", an, cls(af))); + } + format!("{}{}", if f == "*" { "...props" } else { f }, if parts.is_empty() { String::new() } else { format!(": {}", parts.join(" ")) }) + }) + .collect(); + let own: IndexSet = s.own.iter().map(|o| o.cls.clone()).collect(); + format!( + "export component {}(props: {}){}{}{}", + name, + if props.is_empty() { "{}".to_string() } else { format!("{{ {} }}", props.join("; ")) }, + if own.is_empty() { String::new() } else { format!(" fails({})", sorted(own).join(" | ")) }, + if s.requires.is_empty() { String::new() } else { format!(" uses({})", s.requires.iter().map(|k| rel_key(file, k)).collect::>().join(", ")) }, + shape_text(&s.shape) + ) + } + } +} + +pub fn print_signature(sig: &ModSig) -> String { + let mut out = vec![format!("// {} — signature (09-modules MO-3). Sources are read-only to importers (MO-2).", sig.file.split('/').last().unwrap_or(""))]; + for (n, x) in &sig.exports { + out.push(line(&sig.file, n, x)); + } + out.join("\n") + "\n" +} + +// ---------------------------------------------------------------- reading it back (MO-9) +#[derive(Debug)] +pub struct SignatureError(pub String); + +macro_rules! re { + ($name:ident, $pat:expr) => { + static $name: LazyLock = LazyLock::new(|| Regex::new($pat).unwrap()); + }; +} +re!(ROW, r"^(io|pending|lane|fails\(([^)]*)\))$"); +re!(TRAIL_COMMENT, r"\s//.*$"); +re!(LEAD_COMMENT, r"^//.*$"); +re!(T_TYPE, r"^type\s+([A-Za-z_$][A-Za-z0-9_$]*)"); +re!(T_ACTION, r"^action\s+([A-Za-z_$][A-Za-z0-9_$]*)$"); +re!(T_ERROR, r"^error\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:\(([^)]*)\))?$"); +re!(T_SOURCE, r"^source\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::([^=].*?)?)??\s*(:?=)\s*…$"); +re!(T_SERVER, r"^server\s+(pure\s+)?fn\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(([^)]*)\)\s*(?:rejects\(([^)]*)\))?$"); +re!(T_FN, r"^pure\s+fn\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\((.*?)\)\s*(?::\s*((?:(?:io|pending|lane|fails\([^)]*\))\s*)*))?\s*(?:rejects\(([^)]*)\))?\s*(instantiates)?$"); +re!(T_CONTEXT, r"^context\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::\s*(.*?))?\s*(=\s*…)?$"); +re!(T_COMP, r"^component\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(props:\s*(\{.*?\}|\{\})\)(.*)$"); +re!(T_EQ, r"^([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::\s*(.*))?$"); +re!(P_ACTION, r"(^|[^A-Za-z0-9_])action\s+([A-Za-z_$][A-Za-z0-9_$]*)\s+catches\(([^)]*)\)"); +re!(P_UNRENDERED, r"(^|[^A-Za-z0-9_])unrendered([^A-Za-z0-9_]|$)"); +re!(P_CATCHES, r"(^|[^A-Za-z0-9_])catches\(([^)]*)\)"); +re!(P_PROVIDES, r"(^|[^A-Za-z0-9_])provides\(([^)]*)\)"); +re!(C_FAILS, r"(^|[^A-Za-z0-9_])fails\(([^)]*)\)"); +re!(C_USES, r"(^|[^A-Za-z0-9_])uses\(([^)]*)\)"); +re!(C_SHAPE, r"(^|[^A-Za-z0-9_])shape\(([^)]*)\)"); +re!(SOURCE_WORD, r"^source([^A-Za-z0-9_]|$)"); +re!(REF_PARAM, r"^ref\s"); + +fn classes_of(s: &str) -> Vec { + s.split('|').map(|x| crate::parse::js_trim(x).to_string()).filter(|x| !x.is_empty() && x != "nothing").collect() +} +fn catch_of(s: &str) -> Catch { + if crate::parse::js_trim(s) == "*" { Catch::All } else { Catch::Set(classes_of(s).into_iter().collect()) } +} +/// Split on `sep` outside parentheses and braces. +fn split(s: &str, sep: char) -> Vec { + let mut out = vec![]; + let mut depth = 0i64; + let mut cur = String::new(); + for ch in s.chars() { + if "({[<".contains(ch) { + depth += 1; + } + if ")}]>".contains(ch) { + depth -= 1; + } + if ch == sep && depth == 0 { + out.push(std::mem::take(&mut cur)); + } else { + cur.push(ch); + } + } + if !crate::parse::js_trim(&cur).is_empty() { + out.push(cur); + } + out.iter().map(|x| crate::parse::js_trim(x).to_string()).filter(|x| !x.is_empty()).collect() +} +struct RowWords { + rows: Vec, + fails: Vec, + rest: String, +} +fn row_words(s: &str) -> RowWords { + let words = split(s, ' '); + let mut rows = vec![]; + let mut fails = vec![]; + let mut i = 0; + while i < words.len() { + let Some(m) = ROW.captures(&words[i]) else { break }; + if let Some(f) = m.get(2) { + fails = classes_of(f.as_str()); + } else { + rows.push(m[1].to_string()); + } + i += 1; + } + RowWords { rows, fails, rest: words[i..].join(" ") } +} +fn shape_of(s: Option<&str>) -> Result { + let mut sh = Shape::default(); + let Some(s) = s else { return Ok(sh) }; + for p in s.split(';').map(crate::parse::js_trim).filter(|x| !x.is_empty()) { + let ws: Vec = p.split(char::is_whitespace).filter(|x| !x.is_empty()).map(String::from).collect(); + let (w, rest) = (ws[0].as_str(), ws[1..].to_vec()); + match w { + "roots" => sh.roots = rest, + "closes-p" => sh.closes_p = true, + "a" => sh.a = true, + "form" => sh.form = true, + "children-at" => sh.children_at.push(rest), + _ => return Err(format!("shape: {}", p)), + } + } + Ok(sh) +} + +/// A `.d.rx` printed for (or written as) an `.rx` module's signature, read in place of the module. `file` is the `.rx` +/// module's path: context keys are its, and relative keys resolve against it. +pub fn parse_signature(src: &str, file: &str) -> Result { + let mut exports = IndexMap::new(); + for (i, raw) in src.split('\n').enumerate() { + let t = TRAIL_COMMENT.replace(raw, ""); + let t = LEAD_COMMENT.replace(&t, ""); + let t = crate::parse::js_trim(&t).to_string(); + if t.is_empty() { + continue; + } + let fail = |msg: &str| SignatureError(format!("{}:{}: {}: {}", file.strip_suffix(".rx").map(|f| format!("{}.d.rx", f)).unwrap_or_else(|| file.to_string()), i + 1, msg, t)); + if !t.starts_with("export ") { + return Err(fail("a signature declares exports")); + } + let s = crate::parse::js_trim(&t[7..]).to_string(); + if let Some(m) = T_TYPE.captures(&s) { + exports.insert(m[1].to_string(), ExportSig::Type); + continue; + } + if let Some(m) = T_ACTION.captures(&s) { + exports.insert(m[1].to_string(), ExportSig::Action); + continue; + } + if let Some(m) = T_ERROR.captures(&s) { + let params = m.get(2).map(|x| x.as_str()).unwrap_or("").split(',').map(|x| crate::parse::js_trim(x).to_string()).filter(|x| !x.is_empty()).collect(); + exports.insert(m[1].to_string(), ExportSig::ErrClass { params }); + continue; + } + if let Some(m) = T_SOURCE.captures(&s) { + let r = row_words(m.get(2).map(|x| x.as_str()).unwrap_or("")); + if !r.rest.is_empty() { + return Err(fail("a source's rows")); + } + exports.insert(m[1].to_string(), ExportSig::Source { question: &m[3] == "=", rows: r.rows, fails: r.fails }); + continue; + } + if let Some(m) = T_SERVER.captures(&s) { + let params = m[3].split(',').map(|x| crate::parse::js_trim(x).to_string()).filter(|x| !x.is_empty()).collect(); + exports.insert(m[2].to_string(), ExportSig::Server { io: m.get(1).is_none(), params, rejects: classes_of(m.get(4).map(|x| x.as_str()).unwrap_or("")) }); + continue; + } + if let Some(m) = T_FN.captures(&s) { + let params = split(&m[2], ',') + .into_iter() + .map(|p0| { + let r = REF_PARAM.is_match(&p0); + let p = if r { crate::parse::js_trim(&p0[4..]).to_string() } else { p0.clone() }; + match p.find(':') { + None => SigParam { name: crate::parse::js_trim(&p).to_string(), r#ref: r, ty: None }, + Some(c) => SigParam { name: crate::parse::js_trim(&p[..c]).to_string(), r#ref: r, ty: Some(crate::parse::js_trim(&p[c + 1..]).to_string()) }, + } + }) + .collect(); + let r = row_words(m.get(3).map(|x| x.as_str()).unwrap_or("")); + if !r.rest.is_empty() { + return Err(fail("a fn's rows")); + } + exports.insert(m[1].to_string(), ExportSig::Fn { params, instantiating: m.get(5).is_some(), rows: r.rows, fails: r.fails, rejects: classes_of(m.get(4).map(|x| x.as_str()).unwrap_or("")) }); + continue; + } + if let Some(m) = T_CONTEXT.captures(&s) { + let mut body = crate::parse::js_trim(m.get(2).map(|x| x.as_str()).unwrap_or("")).to_string(); + let source = SOURCE_WORD.is_match(&body); + if source { + body = crate::parse::js_trim(&body[6..]).to_string(); + } + let r = row_words(&body); + exports.insert( + m[1].to_string(), + ExportSig::Context { key: format!("{}#{}", file, &m[1]), source, dflt: m.get(3).is_some(), rows: r.rows, fails: r.fails, ty: if r.rest.is_empty() { None } else { Some(r.rest) } }, + ); + continue; + } + if let Some(m) = T_COMP.captures(&s) { + let mut sig = CompSig { own: vec![], requires: vec![], props: IndexMap::new(), shape: None }; + let inner = &m[2][1..m[2].len() - 1]; + for f in split(inner, ';') { + let (fname0, spec) = match f.find(':') { + Some(c) => (crate::parse::js_trim(&f[..c]).to_string(), crate::parse::js_trim(&f[c + 1..]).to_string()), + None => (crate::parse::js_trim(&f).to_string(), String::new()), + }; + let fname = if fname0 == "...props" { "*".to_string() } else { fname0 }; + let mut v = PropSig { f: Catch::Set(IndexSet::new()), p: Catch::Set(IndexSet::new()), rendered: true, action: None }; + let a = P_ACTION.captures(&spec); + let rest = match &a { + Some(a) => { + v.action = Some((catch_of(&a[3]), a[2].to_string())); + let whole = a.get(0).unwrap(); + let start = whole.end() - (whole.as_str().len() - a.get(1).unwrap().as_str().len()); + let matched = &spec[start..whole.end()]; + spec.replacen(matched, "", 1) + } + None => spec.clone(), + }; + if P_UNRENDERED.is_match(&rest) { + v.rendered = false; + v.f = Catch::All; + v.p = Catch::All; + } + if let Some(c) = P_CATCHES.captures(&rest) { + v.f = catch_of(&c[2]); + } + if let Some(p) = P_PROVIDES.captures(&rest) { + v.p = if crate::parse::js_trim(&p[2]) == "*" { + Catch::All + } else { + Catch::Set(p[2].split(',').map(|k| abs_key(file, crate::parse::js_trim(k))).filter(|x| !x.is_empty()).collect()) + }; + } + sig.props.insert(fname, v); + } + let tail = &m[3]; + if let Some(fl) = C_FAILS.captures(tail) { + let at = file.split('/').last().map(String::from); + sig.own = classes_of(&fl[2]).into_iter().map(|c| Own { cls: c, what: format!("{}'s root", &m[1]), at: at.clone() }).collect(); + } + if let Some(us) = C_USES.captures(tail) { + sig.requires = us[2].split(',').map(|k| abs_key(file, crate::parse::js_trim(k))).collect(); + } + sig.shape = Some(shape_of(C_SHAPE.captures(tail).map(|x| x.get(2).unwrap().as_str())).map_err(|e| fail(&e))?); + exports.insert(m[1].to_string(), ExportSig::Comp { sig }); + continue; + } + if let Some(m) = T_EQ.captures(&s) { + let r = row_words(m.get(2).map(|x| x.as_str()).unwrap_or("")); + if !r.rest.is_empty() { + return Err(fail("an equation's rows")); + } + let io = r.rows.iter().any(|x| x == "io"); + exports.insert(m[1].to_string(), ExportSig::Eq { rows: r.rows.into_iter().filter(|x| x != "io").collect(), fails: r.fails, io }); + continue; + } + return Err(fail("not an export")); + } + Ok(ModSig { file: file.into(), exports }) +} diff --git a/compiler/test/node.mjs b/compiler/test/node.mjs new file mode 100644 index 0000000..7ebeec0 --- /dev/null +++ b/compiler/test/node.mjs @@ -0,0 +1,19 @@ +// The Node-API build loads on this platform and compiles a program for every target: what CI checks on each binary +// before it is published. Usage: node test/node.mjs (after `npm run build`) +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const { app } = createRequire(import.meta.url)("../index.js"); +const examples = fileURLToPath(new URL("../../examples/", import.meta.url)); +let fail = 0; +for (const [entry, target] of [["todo.rx", "dom"], ["todo.rx", "ssr"], ["shop/main.rx", "universal"]]) { + const out = JSON.parse(app(path.join(examples, entry), target)); + const modules = Object.keys(out.modules ?? {}); + const ok = modules.length > 0 && !out.error && !out.diagnostics; + if (!ok) fail++; + console.log(`${ok ? "✓" : "✗"} ${entry} (${target}): ${ok ? `${modules.length} modules` : JSON.stringify(out).slice(0, 300)}`); +} +const bad = JSON.parse(app(path.join(examples, "missing.rx"))); +if (!bad.error) { fail++; console.log("✗ a missing entry is not an error"); } else console.log("✓ a missing entry is an error"); +process.exit(fail ? 1 : 0); diff --git a/compiler/test/vite.mjs b/compiler/test/vite.mjs new file mode 100644 index 0000000..e3a2400 --- /dev/null +++ b/compiler/test/vite.mjs @@ -0,0 +1,117 @@ +// `.rx` in Vite end to end: each example app built by Vite with plain `solid()` from @solidjs/vite-plugin, which finds +// this package (`@reactivescript/compiler/vite`) in the project, then driven in Chromium; and the todo app again under +// the dev server. Vite, solid-js and @solidjs/web come from the solid-next checkout (ref/SOLID_NEXT), as ref/ takes +// Solid. Solid's plugin needs built-in `.rx` (solid-vite-plugin PR, see README): SOLID_VITE_PLUGIN names a built +// checkout of it, else the one pnpm installed in solid-next. The Node-API build first: `npm run build` here. +// Usage: SOLID_VITE_PLUGIN=../solid-vite-plugin node compiler/test/vite.mjs [filter] +import { readdirSync, mkdirSync, rmSync, symlinkSync, writeFileSync, cpSync, readFileSync, existsSync } from "node:fs"; +import { createServer } from "node:http"; +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import path from "node:path"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const pkg = path.resolve(here, ".."); +const repo = path.resolve(pkg, ".."); +const store = path.resolve(repo, "../solid-next/node_modules/.pnpm"); +const installed = path.join(store, readdirSync(store).filter(d => d.startsWith("@solidjs+vite-plugin@")).sort().pop(), "node_modules/@solidjs/vite-plugin"); +const pluginDir = process.env.SOLID_VITE_PLUGIN ? path.resolve(process.env.SOLID_VITE_PLUGIN) : installed; +const deps = path.resolve(installed, "../.."); // the installed plugin's node_modules: vite, solid-js, @solidjs/web +const vite = await import(pathToFileURL(path.join(deps, "vite/dist/node/index.js")).href); +const solid = (await import(pathToFileURL(path.join(pluginDir, "dist/esm/index.mjs")).href)).default; +const { chromium } = createRequire(import.meta.url)("playwright"); +const rtWeb = path.join(repo, "ref/compile/rt-web.ts"); + +// notes/ calls server functions: with no server behind the static build, it shows its own error boundary +const apps = { + todo: "todo.rx", cart: "cart.rx", signup: "signup.rx", users: "users.rx", shop: "shop/main.rx", notes: "notes/main.rx", + "ai-feed": "ai/feed.rx", "ai-grid": "ai/grid.rx", "ai-wizard": "ai/wizard.rx", + "ai2-search": "ai2/search/main.rx", "ai2-settings": "ai2/settings/main.rx", "ai2-tracker": "ai2/tracker/main.rx", +}; +const filter = process.argv[2]; +const out = path.join(here, ".out"); mkdirSync(out, { recursive: true }); + +/** A Vite project around an example: its sources, a page, an entry that mounts the program over rt-web.ts. */ +function project(name, entry) { + const dir = path.join(out, name); rmSync(dir, { recursive: true, force: true }); mkdirSync(dir, { recursive: true }); + // the project's node_modules: the plugin's (vite, solid-js, @solidjs/web) and this package, installed + const nm = path.join(dir, "node_modules"); mkdirSync(path.join(nm, "@reactivescript"), { recursive: true }); + for (const d of readdirSync(deps)) if (d !== "@reactivescript") symlinkSync(path.join(deps, d), path.join(nm, d)); + symlinkSync(pkg, path.join(nm, "@reactivescript/compiler")); + cpSync(path.join(repo, "examples"), path.join(dir, "src"), { recursive: true }); + writeFileSync(path.join(dir, "src/entry.js"), `import { createApp } from ${JSON.stringify(rtWeb)};\nimport main from "./${entry}";\nimport * as api from "./api.mjs";\nconst app = createApp({ root: document.getElementById("app"), api });\napp.mount(main(app));\n`); + writeFileSync(path.join(dir, "index.html"), `${name}
\n`); + return dir; +} +const config = (dir, entry) => ({ + root: dir, configFile: false, logLevel: "warn", clearScreen: false, + plugins: [solid()], + server: { fs: { allow: [repo, path.dirname(deps), path.resolve(repo, "../solid-next"), pluginDir] } }, + build: { outDir: "dist", sourcemap: true, emptyOutDir: true }, +}); + +const browser = await chromium.launch(); +let pass = 0, fail = 0; +async function drive(name, url, check) { + const page = await browser.newPage(); const errors = []; + page.on("pageerror", e => errors.push(e.message)); page.on("console", m => { if (m.type() === "error") errors.push(m.text()); }); + try { + await page.goto(url); await page.waitForTimeout(900); + const text = (await page.textContent("#app"))?.replace(/\s+/g, " ").trim() ?? ""; + if (!text) throw new Error("nothing rendered"); + if (check) await check(page); + if (errors.length) throw new Error(errors.join(" | ")); + pass++; console.log(`✓ ${name}: ${text.slice(0, 90)}`); + } catch (e) { fail++; console.log(`✗ ${name}: ${e.message}`); } + finally { await page.close(); } +} +const todo = async page => { + await page.fill("input[placeholder]", "milk"); await page.click("text=Add"); await page.waitForTimeout(100); + const t1 = await page.textContent("#app"); if (!t1.includes("milk") || !t1.includes("1 left")) throw new Error(`after add: ${t1}`); + await page.click("input[type=checkbox]"); await page.waitForTimeout(100); + const t2 = await page.textContent("#app"); if (!t2.includes("0 left")) throw new Error(`after toggle: ${t2}`); +}; + +for (const [name, entry] of Object.entries(apps)) { + if (filter && !name.includes(filter)) continue; + const dir = project(name, entry); + try { await vite.build(config(dir, entry)); } + catch (e) { fail++; console.log(`✗ ${name}: build: ${e.message.split("\n")[0]}`); continue; } + // CP-1c: the bundle's map reaches the .rx sources + const maps = readdirSync(path.join(dir, "dist/assets")).filter(f => f.endsWith(".js.map")); + const sources = maps.flatMap(f => JSON.parse(readFileSync(path.join(dir, "dist/assets", f), "utf8")).sources); + if (!sources.some(s => s.endsWith(".rx"))) { fail++; console.log(`✗ ${name}: the bundle's source map names no .rx file`); continue; } + const dist = path.join(dir, "dist"); + const http = createServer((req, res) => { const f = path.join(dist, req.url === "/" ? "index.html" : req.url.split("?")[0]); if (!existsSync(f)) { res.statusCode = 404; return res.end(); } res.setHeader("content-type", f.endsWith(".js") ? "text/javascript" : f.endsWith(".css") ? "text/css" : "text/html"); res.end(readFileSync(f)); }); + await new Promise(r => http.listen(0, "127.0.0.1", r)); + await drive(`${name} (build)`, `http://127.0.0.1:${http.address().port}/`, name === "todo" ? todo : undefined); + http.close(); +} +if (!filter || "shared".includes(filter)) { // two programs on one page, sharing shop/catalog.rx: no entry option, one catalog module + const dir = project("shared", "shop/main.rx"); + writeFileSync(path.join(dir, "src/shop/count.rx"), `import { products } from "./catalog.rx"\n\nexport default component Count() {\n errored fallback(e) {

no catalogue

} {\n

{len(products)} products

\n }\n}\n`); + writeFileSync(path.join(dir, "src/entry.js"), `import { createApp } from ${JSON.stringify(rtWeb)};\nimport shop from "./shop/main.rx";\nimport count from "./shop/count.rx";\nimport * as api from "./api.mjs";\nfor (const [main, el] of [[shop, "app"], [count, "app2"]]) { const app = createApp({ root: document.getElementById(el), api }); app.mount(main(app)); }\n`); + writeFileSync(path.join(dir, "index.html"), readFileSync(path.join(dir, "index.html"), "utf8").replace('
', '
')); + try { + const bundle = await vite.build(config(dir, "shop/main.rx")); + const ids = (Array.isArray(bundle) ? bundle : [bundle]).flatMap(b => b.output.flatMap(o => Object.keys(o.modules ?? {}))); + const catalogs = ids.filter(m => m.includes("catalog.rx")); + if (catalogs.length !== 1) throw new Error(`catalog.rx bundled as ${JSON.stringify(catalogs)}`); + const dist = path.join(dir, "dist"); + const http = createServer((req, res) => { const f = path.join(dist, req.url === "/" ? "index.html" : req.url.split("?")[0]); if (!existsSync(f)) { res.statusCode = 404; return res.end(); } res.setHeader("content-type", f.endsWith(".js") ? "text/javascript" : f.endsWith(".css") ? "text/css" : "text/html"); res.end(readFileSync(f)); }); + await new Promise(r => http.listen(0, "127.0.0.1", r)); + await drive("shared (build)", `http://127.0.0.1:${http.address().port}/`, async page => { + const t = await page.textContent("#app2"); if (!/3 products/.test(t)) throw new Error(`second program: ${t}`); + }); + http.close(); + } catch (e) { fail++; console.log(`✗ shared: ${e.message.split("\n").slice(0, 6).join(" | ")}`); } +} +if (!filter || "todo".includes(filter)) { // the dev server: modules transformed on request + const dir = project("todo-dev", "todo.rx"); + const server = await vite.createServer(config(dir, "todo.rx")); await server.listen(0); + await drive("todo (dev)", `http://127.0.0.1:${server.httpServer.address().port}/`, todo); + await server.close(); +} +await browser.close(); +console.log(`\n${pass} pass, ${fail} fail (vite ${vite.version}, @solidjs/vite-plugin ${JSON.parse(readFileSync(path.join(pluginDir, "package.json"), "utf8")).version})`); +process.exit(fail ? 1 : 0); diff --git a/compiler/vite.d.mts b/compiler/vite.d.mts new file mode 100644 index 0000000..dd1b73d --- /dev/null +++ b/compiler/vite.d.mts @@ -0,0 +1,4 @@ +import type { Frontend } from "@solidjs/vite-plugin"; + +/** ReactiveScript's front end for `@solidjs/vite-plugin`, which loads it on its own for `.rx` when this package is installed. */ +export default function rx(): Frontend; diff --git a/compiler/vite.mjs b/compiler/vite.mjs new file mode 100644 index 0000000..e17b46f --- /dev/null +++ b/compiler/vite.mjs @@ -0,0 +1,64 @@ +// ReactiveScript's front end for @solidjs/vite-plugin. The plugin loads it from `@reactivescript/compiler/vite` when +// the project has the package, so `solid()` compiles `.rx` with no option; `solid({ frontends: { ".rx": rx() } })` +// names it explicitly. +// +// The crate (its Node-API build, `src/node.rs`: what `rxc app` does) already hands each module to Solid's compiler +// (spec/11 CP-1), so a `.rx` module comes back as Solid's output for the environment's `generate`, with its source map onto the `.rx` and, on a +// program's entry, the program's stylesheet. Solid's plugin then takes it the way it takes lowered `.tsrx`: primitive +// names, `lazy()`, the HMR refresh pass, server functions (12 SF-7a), the stylesheet as a CSS module. +// +// Entries find themselves. JavaScript imports only a program's entry (its default export mounts the app); an `.rx` +// dependency is a factory over the app (09 MO-1b) that only `.rx` imports. So an `.rx` module imported plainly is an +// entry: the program is checked and compiled whole from it (CP-2), once per target and again when one of its files +// changes. The crate's output imports its dependencies as `./x.rx?rx-dep`, and a dependency is served from the build +// of a program that contains it. A module several programs share is one module: its instance is keyed by its factory +// (11 CP-4b `module`), not by its place in a program. +import { statSync } from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; + +const { app } = createRequire(import.meta.url)("./index.js"); +const DEP = "rx-dep"; +const mtime = file => { try { return statSync(file).mtimeMs; } catch { return -1; } }; +const isDep = moduleId => new URLSearchParams(moduleId.split("?")[1] ?? "").has(DEP); +// `import __m0 from "./catalog.rx";` → `"./catalog.rx?rx-dep"` (one line each: the module's map is by line, CP-1c) +const tagDeps = code => code.replace(/^(import \w+ from ")(\.{1,2}\/[^"?]*\.rx)(";?)$/gm, `$1$2?${DEP}$3`); + +/** @returns {import("@solidjs/vite-plugin").Frontend} */ +export default function rx() { + const builds = new Map(); // entry\0target\0moduleName → { entry, target, moduleName, out, stamps } + const fresh = b => [...b.stamps].every(([f, t]) => mtime(f) === t); + function build(entry, target, moduleName) { + const key = `${entry}\0${target}\0${moduleName}`; + const prior = builds.get(key); + if (prior && fresh(prior)) return prior; + const out = JSON.parse(app(entry, target, moduleName)); + if (out.modules) out.modules = Object.fromEntries(Object.entries(out.modules).map(([p, m]) => [path.resolve(p), m])); + const files = out.modules ? Object.keys(out.modules) : out.diagnostics ? [...new Set([entry, ...out.diagnostics.map(d => d.file)])] : [entry]; + const b = { entry, target, moduleName, out, stamps: new Map(files.map(f => [f, mtime(f)])) }; + builds.set(key, b); + return b; + } + /** A dependency: from a build of a program that has it, rebuilt if that program changed. */ + function containing(file, target, moduleName) { + const has = [...builds.values()].filter(b => b.target === target && b.moduleName === moduleName && b.out.modules?.[file]); + const b = has.find(fresh) ?? has[0]; + if (!b) throw new Error(`${file} is imported as an .rx dependency, but no program built so far contains it`); + return build(b.entry, target, moduleName); + } + return { + compile(_source, { id, moduleId, solid, addWatchFile }) { + const file = path.resolve(id); + const target = solid.generate === "ssr" ? "ssr" : solid.generate === "universal" ? "universal" : "dom"; + const moduleName = solid.moduleName ?? "@solidjs/web"; + const dep = isDep(moduleId); + const { entry, out } = dep ? containing(file, target, moduleName) : build(file, target, moduleName); + if (out.diagnostics) throw new Error(out.diagnostics.map(d => `${d.file}:${d.line}: error ${d.code} [${d.rule}] ${d.msg}`).join("\n")); + if (out.error) throw new Error(out.error); + const m = out.modules[file]; + for (const p of Object.keys(out.modules)) if (p !== file) addWatchFile(p); + // CSS-4: the program's stylesheet, linked once, from its entry + return { code: tagDeps(m.code), map: m.map, css: !dep && file === entry ? out.css : "" }; + }, + }; +} diff --git a/ref/README.md b/ref/README.md index 962ab2e..1acce90 100644 --- a/ref/README.md +++ b/ref/README.md @@ -35,6 +35,20 @@ ref/check.ts [--signature] file.rx…` prints diagnostics for files and the signature as `.d.rx` text — the text an importer is checked against when the module's `.rx` is not there (MO-9, `src/signature.ts`). +Resolved model (spec/11 CP-6): `src/model.ts`, filled by the checker's walk. +`node --experimental-strip-types ref/model.ts file.rx…` prints, per module, its +regions, symbols and computations, each computation with its reads (tracked, +snapshot, pre), writes and calls. + +The Rust crate (`compiler/`, spec/11 CP-7) is diffed against this directory by +`npm --prefix ref run crate` (`crate.ts`): every program in `scenarios/` and +`examples/` is loaded, parsed, checked, its signatures printed and its model +built by both, and the two must agree exactly. Needs cargo; not Solid. With +`-- --lower`, both also lower every accepted program through Solid's compiler +for each target and must write the same JavaScript (needs cargo and Solid at +SOLID_NEXT); with `RX_CRATE=1`, `npm run compiled` and `npm run ssr` run the +scenarios on the crate's lowering (`compile/lowered.ts`). + Phases: 1. core: sources, equations, `pre`, `peek`, effects, instants (02-core §4) 2. regions: if/for as scoped sub-programs with disposal (03) diff --git a/ref/compile/README.md b/ref/compile/README.md index cabe251..6f20d7d 100644 --- a/ref/compile/README.md +++ b/ref/compile/README.md @@ -10,6 +10,7 @@ time instead of run time. | `emit.ts` | the code generator: `compile(program) → string`. Lexical scopes become JS scopes; every region is a function, every declaration a `const`, in dependency order (E-7 + C-1 make it a DAG). Names are mangled: `v$x` the value or accessor, `w$x` a source's writer, `p$x` a companion `pre` memo. | | `rt.ts` | the runtime the module calls (`__rt`, whose names spec/11 CP-4b pins): sources (atomic → signal; structured → store behind a signal slot), question sources (with an optimistic layer), effects with an error arm, actions as generators, provisions as context, view helpers over `@solidjs/universal`. Renderer node operations are a parameter: a string tree for tests, `domNodes(document)` for the browser. | | `run.ts` | compiles every scenario's program, imports it, drives it through the scenario's rows. The output is Solid code, so it must show what the oracle shows. | +| `lowered.ts` | a scenario program compiled for `run.ts` and `ssr/run.ts`: by `emit.ts` and Solid's `transform()`, or with `RX_CRATE=1` by the Rust crate (`compiler/`, `rxc lower`). | | `build.ts` | `node --experimental-strip-types ref/compile/build.ts app.rx` → `ref/compile/.out/web/app/` (index.html with an import map, app.mjs, rt.mjs, api.mjs). Serve `ref/` and open `/compile/.out/web/app/`. | ``` diff --git a/ref/compile/lowered.ts b/ref/compile/lowered.ts new file mode 100644 index 0000000..7be5923 --- /dev/null +++ b/ref/compile/lowered.ts @@ -0,0 +1,40 @@ +// A scenario program compiled for the runners: ref/compile/emit.ts then Solid's transform() — or, with RX_CRATE=1, the +// Rust crate (compiler/, `rxc lower`, spec/11 CP-7), so that compile/run.ts and ssr/run.ts run the crate's output. +// `handlers` are a table's handler rows (their statements, unparsed), compiled into the program as both runners need. +import { readFileSync, existsSync, statSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { moduleSet, type Scenario } from "../src/scenario.ts"; +import { parseStmts } from "../src/parse.ts"; +import { loadModules } from "../src/modules.ts"; +import { compileModules, CompileError } from "./emit.ts"; +import { lowerViews, STRING_RENDERER, type Target } from "./jsx.ts"; + +export type Built = { entry: string; css: string; views: (target: Target) => Map }; +export const crate = !!process.env.RX_CRATE; +const rxc = fileURLToPath(new URL("../../compiler/target/release/rxc", import.meta.url)); +if (crate) execFileSync("cargo", ["build", "--release", "--quiet"], { cwd: fileURLToPath(new URL("../../compiler/", import.meta.url)), stdio: "inherit" }); + +/** Throws CompileError when the program is not compiled; the caller has run the checker. */ +export function build(sc: Scenario, src: string, handlers: string[]): Built { + if (!crate) { + const out = compileModules(moduleSet(sc, src), { extraStmts: handlers.map(h => { try { return parseStmts(h); } catch { return []; } }) }); + return { entry: out.entry, css: out.css, views: t => lowerViews(out.files, t) }; + } + const files: Record = {}; + const read = (p: string) => { const s = p === sc.entry ? src : sc.modules.get(p) ?? (existsSync(p) && statSync(p).isFile() ? readFileSync(p, "utf8") : undefined); if (s !== undefined) files[p] = s; return s; }; + loadModules(sc.entry, read); + const [r] = JSON.parse(execFileSync(rxc, ["lower"], { input: JSON.stringify([{ id: sc.name, entry: sc.entry, files, extra: handlers, renderer: STRING_RENDERER }]), maxBuffer: 1 << 30 }).toString()); + if (r.compileError) throw new CompileError(r.compileError); + if (r.refused || r.error || r.panic) throw new Error(`crate: ${JSON.stringify(r)}`); + const views = (t: Target) => { + const m = new Map(); + for (const [name, ts] of Object.entries(r.files as Record>)) { + const code = ts[t]; + if (typeof code !== "string") throw new Error(`crate: ${name} ${t}: ${code.error}`); + m.set(name, code); + } + return m; + }; + return { entry: r.entry, css: r.css, views }; +} diff --git a/ref/compile/rt-server.ts b/ref/compile/rt-server.ts index 0fb587b..98af70f 100644 --- a/ref/compile/rt-server.ts +++ b/ref/compile/rt-server.ts @@ -77,7 +77,7 @@ export function createApp(opts: { api?: Record unknown> const flights = new Map(); const lastKey = new Map(); const registry = new Map(); - const modules = new Map>(); + const modules = new Map>(); const log: string[] = []; const declared: unknown[] = []; let currentEq: string | null = null; let serverSeq = 0; let classSeq = 0; @@ -189,7 +189,7 @@ export function createApp(opts: { api?: Record unknown> affects: onServer("affects"), preOr: (v: unknown, init: () => unknown) => v === undefined ? init() : v, - module(key: string, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, + module(key: unknown, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, // ---- context (RG-7): the server build's owner context, keyed by the declaration ---- owner: () => getOwner(), context(name: string, dflt: (() => unknown) | null): Ctx { return { name, id: Symbol(name), dflt, owner: getOwner(), dv: dflt ? { get: createMemo(dflt as any) as Acc } : null }; }, diff --git a/ref/compile/rt-web.ts b/ref/compile/rt-web.ts index 36766cb..872d9db 100644 --- a/ref/compile/rt-web.ts +++ b/ref/compile/rt-web.ts @@ -89,7 +89,7 @@ export function createApp(opts: { root: any; api?: Record(); const contexts = new Map>>(); const storeAccs = new WeakSet(); - const modules = new Map>(); + const modules = new Map>(); const hoistedBy = new Map(); // an equation's hoisted awaits, re-asked with it by `refresh` const log: string[] = []; let currentEq: string | null = null; let serverSeq = 0; let classSeq = 0; @@ -235,7 +235,7 @@ export function createApp(opts: { root: any; api?: Record unknown) => v === undefined ? init() : v, // ---- modules (09 MO-1): a dependency's factory runs once per app; its exports are shared by every importer ---- - module(key: string, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, + module(key: unknown, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, // ---- context (RG-7) ---- owner: () => getOwner(), /** `context x: T = d` (RG-7): the declaration is the identity; `d`, if any, is evaluated once, owned where it is declared. */ diff --git a/ref/compile/rt.ts b/ref/compile/rt.ts index 80a9194..8d5963e 100644 --- a/ref/compile/rt.ts +++ b/ref/compile/rt.ts @@ -99,7 +99,7 @@ export function createApp(opts: { api?: Record unknown> const registry = new Map(); const contexts = new Map>>(); const storeAccs = new WeakSet(); - const modules = new Map>(); + const modules = new Map>(); const hoistedBy = new Map(); // an equation's hoisted awaits, re-asked with it by `refresh` const log: string[] = []; let currentEq: string | null = null; let serverSeq = 0; @@ -243,7 +243,7 @@ export function createApp(opts: { api?: Record unknown> preOr: (v: unknown, init: () => unknown) => v === undefined ? init() : v, // ---- modules (09 MO-1): a dependency's factory runs once per app; its exports are shared by every importer ---- - module(key: string, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, + module(key: unknown, factory: (rt: unknown) => Record) { let m = modules.get(key); if (!m) { m = factory(app); modules.set(key, m); } return m; }, // ---- context (RG-7) ---- owner: () => getOwner(), /** `context x: T = d` (RG-7): the declaration is the identity; `d`, if any, is evaluated once, owned where it is declared. */ diff --git a/ref/compile/run.ts b/ref/compile/run.ts index 9c5feea..0434ae2 100644 --- a/ref/compile/run.ts +++ b/ref/compile/run.ts @@ -8,9 +8,9 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { parseScenario, moduleSet, type Scenario } from "../src/scenario.ts"; import { parseProgram, parseStmts } from "../src/parse.ts"; import { checkModules } from "../src/check.ts"; -import { compileModules, compileDynamic, compileDynamicExpr, CompileError } from "./emit.ts"; +import { compileDynamic, compileDynamicExpr, CompileError } from "./emit.ts"; import { createApp, flush } from "./rt.ts"; -import { lowerViews } from "./jsx.ts"; +import { build } from "./lowered.ts"; import { resetErrorHalt } from "@solidjs/signals"; process.on("unhandledRejection", () => {}); @@ -25,15 +25,17 @@ async function runTable(sc: Scenario, t: Scenario["tables"][number], base: Scena let rows = t.rows; let own = 0; if (t !== base && base && rows.length && rows[0].instant > 0) { const pre = base.rows.filter(r => r.instant < rows[0].instant); own = pre.length; rows = [...pre, ...rows]; } const chains: typeof rows[] = []; for (const r of rows) { if (r.action === "(effect)" && chains.length) chains[chains.length - 1].push(r); else chains.push([r]); } - const handlerStmts = rows.filter(r => r.action.startsWith("handler")).map(r => { try { return parseStmts(r.action.slice(7)); } catch { return []; } }); resetErrorHalt(); - let out: ReturnType; - try { const set = moduleSet(sc, src); if (checkModules(set).diags.length) { lines.push(` ~ ${t.title}: refused by the checker`); return ok; } out = compileModules(set, { extraStmts: handlerStmts }); } + let entry: string, views: Map; + try { + const set = moduleSet(sc, src); if (checkModules(set).diags.length) { lines.push(` ~ ${t.title}: refused by the checker`); return ok; } + const out = build(sc, src, rows.filter(r => r.action.startsWith("handler")).map(r => r.action.slice(7))); entry = out.entry; views = out.views("universal"); + } catch (e) { if (e instanceof CompileError) { lines.push(` ~ ${t.title}: not compiled — ${e.message}`); return ok; } lines.push(` ✗ ${t.title}: ${(e as Error).message}`); return false; } const dir = `${outDir}${sc.name}.${++seq}/`; mkdirSync(dir, { recursive: true }); const local = JSON.stringify(fileURLToPath(new URL("./server-functions-local.ts", import.meta.url))); - for (const [name, js] of lowerViews(out.files, "universal")) writeFileSync(dir + name, js.replaceAll('"@solidjs/web/server-functions"', local)); // SF-7b: no network here - const file = dir + out.entry; + for (const [name, js] of views) writeFileSync(dir + name, js.replaceAll('"@solidjs/web/server-functions"', local)); // SF-7b: no network here + const file = dir + entry; const app = createApp(); let dispose: () => void = () => {}; try { @@ -81,7 +83,7 @@ for (const f of readdirSync(dir).filter(f => f.endsWith(".md") && f !== "TEMPLAT for (const t of sc.tables) ok = (await runTable(sc, t, base, lines)) && ok; if (sc.css !== null) { // 07 §6b: the program's stylesheet, as TSRX's engine scoped it — hashes renamed h1, h2… in order of appearance try { - const hashes: string[] = []; const css = compileModules(moduleSet(sc, sc.program)).css.replace(/tsrx-[0-9a-f]{8}/g, h => `h${(hashes.includes(h) ? hashes.indexOf(h) : hashes.push(h) - 1) + 1}`); + const hashes: string[] = []; const css = build(sc, sc.program, []).css.replace(/tsrx-[0-9a-f]{8}/g, h => `h${(hashes.includes(h) ? hashes.indexOf(h) : hashes.push(h) - 1) + 1}`); if (norm(css) === norm(sc.css)) lines.push(` ✓ Scoped CSS: ${norm(css)}`); else { ok = false; lines.push(` ✗ Scoped CSS: expected '${norm(sc.css)}', got '${norm(css)}'`); } } catch (e) { ok = false; lines.push(` ✗ Scoped CSS: ${(e as Error).message}`); } } diff --git a/ref/crate.ts b/ref/crate.ts new file mode 100644 index 0000000..7fb68bb --- /dev/null +++ b/ref/crate.ts @@ -0,0 +1,188 @@ +// CP-5: the Rust crate (compiler/) against ref/ on every program the scenarios and examples hold. For each program both +// sides load the modules, parse, check, print signatures and build the resolved model (CP-6); the two must agree exactly. +// With --lower (CP-1): every program the checker accepts is also compiled by both — ref/compile/emit.ts then Solid's +// transform(), the crate's emit.rs then compile_program() — for the universal, dom and ssr targets, with a scenario table's +// handler rows as compile/run.ts passes them; the modules must agree (comments dropped, both printed by esbuild), and the +// crate's source maps onto .rx must hold (checkMap, CP-1c). +// Usage: node --experimental-strip-types ref/crate.ts [--lower] [filter] (builds compiler/ with cargo first; VERBOSE=1 lists each; +// DUMP=dir writes the first differing module of a failure, both sides, to dir/{ref,crate}..js) +import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { parseScenario } from "./src/scenario.ts"; +import { loadModules } from "./src/modules.ts"; +import { checkModules } from "./src/check.ts"; +import { printSignature } from "./src/signature.ts"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const lowerMode = process.argv.includes("--lower"); +const filter = process.argv.slice(2).find(a => !a.startsWith("--")); +const verbose = !!process.env.VERBOSE; +type Req = { id: string; entry: string; files: Record; extra?: string[] }; +const reqs: Req[] = []; + +/** A program: its entry and every file the loader reads for it (MO-8 `.d.rx`, MO-9 signatures, imported modules). */ +function add(id: string, entry: string, read: (p: string) => string | undefined, extra?: string[]) { + if (filter && !id.includes(filter)) return; + const files: Record = {}; + const rec = (p: string) => { const s = read(p); if (s !== undefined) files[p] = s; return s; }; + try { loadModules(entry, rec); } catch { /* the files read up to the error are what both sides see */ } + reqs.push({ id, entry, files, ...(extra ? { extra } : {}) }); +} +const disk = (p: string) => existsSync(p) && statSync(p).isFile() ? readFileSync(p, "utf8") : undefined; + +const sdir = join(root, "scenarios"); +for (const f of readdirSync(sdir).filter(f => f.endsWith(".md") && f !== "TEMPLATE.md").sort()) { + const sc = parseScenario(f.replace(/\.md$/, ""), readFileSync(join(sdir, f), "utf8"), sdir); + const read = (src: string) => (p: string) => p === sc.entry ? src : sc.modules.get(p) ?? disk(p); + if (lowerMode) { // what compile/run.ts compiles: each table, with its handler rows + const base = sc.tables.find(t => t.title === "Frames"); + for (const t of sc.tables) { + let rows = t.rows; + if (t !== base && base && rows.length && rows[0].instant > 0) rows = [...base.rows.filter(r => r.instant < rows[0].instant), ...rows]; + add(`${sc.name} ${t.title}`, sc.entry, read(t.program ?? sc.program), rows.filter(r => r.action.startsWith("handler")).map(r => r.action.slice(7))); + } + if (!sc.tables.length && sc.program) add(`${sc.name} Program`, sc.entry, read(sc.program), []); + continue; + } + if (sc.program) add(`${sc.name} Program`, sc.entry, read(sc.program)); + for (const t of sc.tables) if (t.program) add(`${sc.name} ${t.title}`, sc.entry, read(t.program)); + sc.rejected.forEach((r, i) => add(`${sc.name} ${r.types ? "types" : r.warn ? "warn" : "reject"}(${r.rule}) #${i}`, sc.entry, read(r.program))); +} +const walk = (d: string): string[] => readdirSync(d, { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(join(d, e.name)) : e.name.endsWith(".rx") && !e.name.endsWith(".d.rx") ? [join(d, e.name)] : []); +for (const f of walk(join(root, "examples")).sort()) add(`examples/${f.slice(join(root, "examples").length + 1)}`, f, disk, lowerMode ? [] : undefined); + +const plain = (x: unknown) => JSON.parse(JSON.stringify(x, (_k, v) => v instanceof Map ? Object.fromEntries(v) : v instanceof Set ? [...v] : v)); +function refOf(r: Req) { + const read = (p: string) => r.files[p]; + let set; try { set = loadModules(r.entry, read); } catch (e) { return { error: (e as Error).message }; } + let checked; try { checked = checkModules(set); } catch (e) { return { throws: (e as Error).message }; } + return plain({ + order: set.order, + asts: Object.fromEntries(set.order.map(p => [p, set.programs.get(p)])), + diags: checked.diags, + sigs: Object.fromEntries(set.order.map(p => [p, printSignature(checked.sigs.get(p)!)])), + models: Object.fromEntries(checked.models), + }); +} +/** The first place two JSON values differ, as a path. */ +function diff(a: any, b: any, at = ""): string | null { + if (a === b) return null; + if (typeof a !== typeof b || a === null || b === null || typeof a !== "object" || Array.isArray(a) !== Array.isArray(b)) + return `${at || "/"}: ref ${JSON.stringify(a)?.slice(0, 200)} ≠ crate ${JSON.stringify(b)?.slice(0, 200)}`; + const keys = Array.isArray(a) ? [...Array(Math.max(a.length, b.length)).keys()] : [...new Set([...Object.keys(a), ...Object.keys(b)])]; + for (const k of keys) { const d = diff(a[k as any], b[k as any], `${at}/${k}`); if (d) return d; } + return null; +} + +execFileSync("cargo", ["build", "--release", "--quiet"], { cwd: join(root, "compiler"), stdio: "inherit" }); +if (lowerMode) process.exit(await lower()); +const out = execFileSync(join(root, "compiler/target/release/rxc"), ["conform"], { input: JSON.stringify(reqs), maxBuffer: 1 << 30 }).toString(); +const got = new Map((JSON.parse(out) as any[]).map(x => [x.id, x])); +let bad = 0; +for (const r of reqs) { + const want = refOf(r); const { id: _, ...have } = got.get(r.id) ?? {}; + const d = diff(want, have); + if (d) { bad++; console.log(`FAIL ${r.id}\n ${d}`); } else if (verbose) console.log(`ok ${r.id}`); +} +console.log(`${reqs.length - bad}/${reqs.length} programs: the crate agrees with ref/ (AST, diagnostics, signatures, resolved model)`); +process.exit(bad ? 1 : 0); + +/** --lower: ref/compile's modules against the crate's, through Solid's compiler for each target. */ +async function lower(): Promise { + const { compileModules, CompileError } = await import("./compile/emit.ts"); + const { lowerViews, STRING_RENDERER } = await import("./compile/jsx.ts"); + const { transformSync } = await import("esbuild"); + const { parseStmts } = await import("./src/parse.ts"); + const norm = (code: string) => { try { return transformSync(code, { loader: "js", format: "esm", legalComments: "none" }).code; } catch (e) { return `esbuild: ${(e as Error).message}\n${code}`; } }; + const out = execFileSync(join(root, "compiler/target/release/rxc"), ["lower"], { input: JSON.stringify(reqs.map(r => ({ ...r, renderer: STRING_RENDERER, maps: true }))), maxBuffer: 1 << 30 }).toString(); + const got = new Map((JSON.parse(out) as any[]).map(x => [x.id, x])); + let bad = 0, compiled = 0, modules = 0, mapped = 0; + for (const r of reqs) { + const have = got.get(r.id); + let set: ReturnType | undefined; + let want: any; + try { + set = loadModules(r.entry, p => r.files[p]); + if (checkModules(set).diags.length) want = { refused: true }; + else { + const extra = (r.extra ?? []).map(s => { try { return parseStmts(s); } catch { return []; } }); + let c; try { c = compileModules(set, { extraStmts: extra }); } catch (e) { if (!(e instanceof CompileError)) throw e; c = null; want = { compileError: (e as Error).message }; } + if (c) { + const files: Record> = {}; + for (const [k, target] of [["universal", "universal"], ["dom", "dom"], ["ssr", "ssr"]] as const) { + for (const [name, code] of lowerViews(c.files, target)) (files[name] ??= {})[k] = code; + } + want = { entry: c.entry, css: c.css, files }; + } + } + } catch (e) { want = { error: (e as Error).message }; } + let d: string | null = null; + if (want.files && have?.files) { + compiled++; + if (want.entry !== have.entry) d = `entry: ref ${want.entry} ≠ crate ${have.entry}`; + else if (want.css !== have.css) d = `css: ref ${JSON.stringify(want.css).slice(0, 300)} ≠ crate ${JSON.stringify(have.css).slice(0, 300)}`; + else for (const name of new Set([...Object.keys(want.files), ...Object.keys(have.files)])) { + modules++; + for (const k of ["universal", "dom", "ssr"]) { + const a = want.files[name]?.[k], b = have.files[name]?.[k]; + if (typeof b !== "string") { d = `${name} ${k}: crate ${JSON.stringify(b)}`; break; } + const [x, y] = [norm(a), norm(b)]; + if (x !== y && process.env.DUMP) { const { writeFileSync } = await import("node:fs"); writeFileSync(`${process.env.DUMP}/ref.${k}.js`, x); writeFileSync(`${process.env.DUMP}/crate.${k}.js`, y); } + if (x !== y) { const xl = x.split("\n"), yl = y.split("\n"); const i = xl.findIndex((l, j) => l !== yl[j]); d = `${name} ${k} line ${i + 1}:\n ref: ${xl[i]}\n crate: ${yl[i]}`; break; } + } + if (d) break; + } + if (!d) for (const name of Object.keys(have.files)) { + const path = name === "main.mjs" ? set!.entry : set!.order[+name.slice(1, -4)]; + for (const k of ["universal", "dom", "ssr"]) { d = checkMap(have.files[name][k], have.maps?.[name]?.[k], set!.programs.get(path)!, r.files[path] ?? "", k); if (d) { d = `${name} ${k} map: ${d}`; break; } mapped++; } + if (d) break; + } + } else d = diff(want, (({ id: _, ...x }) => x)(have ?? {})); + if (d) { bad++; console.log(`FAIL ${r.id}\n ${d}`); } else if (verbose) console.log(`ok ${r.id}`); + } + console.log(`${reqs.length - bad}/${reqs.length} programs lowered alike (${compiled} compiled, ${modules} modules × universal, dom, ssr; ${mapped} source maps onto .rx checked)`); + return bad ? 1 : 0; +} + +/** Base64 VLQ `mappings` → [generated line, generated column, source line] (0-based). */ +function decodeMappings(mappings: string): [number, number, number][] { + const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const out: [number, number, number][] = []; let srcLine = 0; + mappings.split(";").forEach((line, gl) => { + let gc = 0; + for (const seg of line.split(",").filter(Boolean)) { + const v: number[] = []; let shift = 0, n = 0; + for (const ch of seg) { const d = B64.indexOf(ch); n += (d & 31) << shift; if (d & 32) shift += 5; else { v.push(n & 1 ? -(n >>> 1) : n >>> 1); n = 0; shift = 0; } } + gc += v[0]; if (v.length >= 4) { srcLine += v[2]; out.push([gl, gc, srcLine]); } + } + }); + return out; +} +/** CP-1c: a module's source map onto its `.rx`. Every mapped line is the module's; each source or equation declared once + * is declared (`const v$x`, `const [v$x, w$x]`) at a position mapped to its line; on dom, every element's line is mapped. */ +function checkMap(code: string, map: string | undefined, prog: any, text: string, target: string): string | null { + if (!map) return "none"; + const m = JSON.parse(map); const nlines = text.split("\n").length; + if (m.sourcesContent?.[0] !== text) return "sourcesContent is not the module's text"; + const toks = decodeMappings(m.mappings); + const outside = toks.find(t => t[2] < 0 || t[2] >= nlines); if (outside) return `line ${outside[2] + 1} is past the module's ${nlines}`; + const decls: { name: string; line: number }[] = []; const els = new Set(); + const markup = (ms: any[]) => { for (const x of ms ?? []) { if (x.k === "el" && x.tag && x.line) els.add(x.line); for (const v of Object.values(x)) if (Array.isArray(v) && v.length && typeof v[0] === "object" && v[0]?.k) markup(v as any[]); if (Array.isArray(x.decls)) walk(x.decls); } }; + const walk = (ds: any[]) => { for (const x of ds) { if (x.k === "source" || x.k === "eq") decls.push(x); if (x.decls) walk(x.decls); if (x.view) markup(x.view); } }; + walk(prog.decls); + const lines = code.split("\n"); let checked = 0; + for (const dcl of decls) { + if (decls.filter(x => x.name === dcl.name).length > 1) continue; + const re = new RegExp(`(?:const|let|var)\\s+\\[?\\s*v\\$${dcl.name.replace(/\$/g, "\\$")}\\b`, "g"); + const hits = [...code.matchAll(re)]; if (hits.length !== 1) continue; + const before = code.slice(0, hits[0].index!); const gl = before.split("\n").length - 1; const gc = gl ? before.length - before.lastIndexOf("\n") - 1 : before.length; + const at = toks.filter(t => t[0] === gl && t[1] <= gc).pop(); + if (!at) return `${dcl.name} (line ${dcl.line}): no mapping at ${gl + 1}:${gc + 1} — ${lines[gl]?.trim().slice(0, 80)}`; + if (at[2] + 1 !== dcl.line) return `${dcl.name} maps to line ${at[2] + 1}, declared on ${dcl.line} — ${lines[gl]?.trim().slice(0, 80)}`; + checked++; + } + if (target === "dom") { const seen = new Set(toks.map(t => t[2] + 1)); const miss = [...els].find(l => !seen.has(l)); if (miss) return `element on line ${miss} is not mapped`; } + return null; +} diff --git a/ref/model.ts b/ref/model.ts new file mode 100644 index 0000000..3af99dd --- /dev/null +++ b/ref/model.ts @@ -0,0 +1,16 @@ +// Usage: node --experimental-strip-types ref/model.ts file.rx... +// Prints the resolved model (spec/11 CP-6) of each file and the .rx modules it imports, as JSON: regions, symbols, and the +// computations with their reads (tracked, snapshot, pre), writes and calls. The checker's diagnostics come first, if any. +import { readFileSync, existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { loadModules } from "./src/modules.ts"; +import { checkModules } from "./src/check.ts"; +let bad = 0; +for (const f of process.argv.slice(2)) { + const set = loadModules(resolve(f), p => existsSync(p) ? readFileSync(p, "utf8") : undefined); + const { diags, models } = checkModules(set); + for (const d of diags) { bad++; console.error(`${d.file ?? f}:${d.line}: error ${d.code} [${d.rule}] ${d.msg}`); } + const rel = (p: string) => p.replace(process.cwd() + "/", ""); + console.log(JSON.stringify(set.order.map(p => ({ ...models.get(p)!, file: rel(p) })), null, 1).replace(/\n\s+(?=[^\s{\[])/g, " ").replace(/\n\s+([}\]])/g, " $1")); +} +process.exit(bad ? 1 : 0); diff --git a/ref/package.json b/ref/package.json index 6d64a18..55d3708 100644 --- a/ref/package.json +++ b/ref/package.json @@ -8,7 +8,9 @@ "oracle": "node --experimental-strip-types --conditions=browser --conditions=development oracle/run.ts", "compiled": "node --experimental-strip-types --conditions=browser --conditions=development compile/run.ts", "ssr": "node --experimental-strip-types --conditions=development ssr/run.ts", - "types": "node --experimental-strip-types types/run.ts" + "types": "node --experimental-strip-types types/run.ts", + "crate": "node --experimental-strip-types crate.ts", + "model": "node --experimental-strip-types model.ts" }, "dependencies": { "@solidjs/compiler": "file:../../solid-next/packages/compiler", diff --git a/ref/src/check.ts b/ref/src/check.ts index bc87955..60e0e28 100644 --- a/ref/src/check.ts +++ b/ref/src/check.ts @@ -7,6 +7,7 @@ import type { Diagnostic } from "./errors.ts"; import type { ModuleSet } from "./modules.ts"; import type { JsDecl } from "./parse.ts"; import { misnested, misnestedInside, P_CLOSERS, P_SCOPE, EMPTY_SHAPE, type Shape } from "./content.ts"; +import { ModelBuilder, type Model, type RegionKind, type CompKind } from "./model.ts"; const BUILTINS = new Set(["len", "str", "map", "filter", "sum", "fail", "fetch", "every"]); // the scenario prelude; all pure const isHandler = (attr: string) => /^on[A-Z:]/.test(attr); // SC-4 @@ -176,16 +177,18 @@ export type ModSig = { file: string; exports: Map }; type Opts = { sigs?: Map; file?: string; declarations?: Map }; export function check(p: Program, opts: Opts = {}): Diagnostic[] { const c = new Checker(opts); c.run(p); return c.diags; } -export function checkWithSignature(p: Program, opts: Opts = {}): { diags: Diagnostic[]; sig: ModSig } { const c = new Checker(opts); c.run(p); return { diags: c.diags, sig: c.signature(p) }; } +export function checkWithSignature(p: Program, opts: Opts = {}): { diags: Diagnostic[]; sig: ModSig; model: Model } { const c = new Checker(opts); c.run(p); return { diags: c.diags, sig: c.signature(p), model: c.model.build(opts.file ?? "main.rx") }; } /** CN-4b / MO-3: check every module of a program, dependencies first, each against its imports' signatures only. */ -export function checkModules(set: ModuleSet): { diags: Diagnostic[]; sigs: Map } { +export function checkModules(set: ModuleSet): { diags: Diagnostic[]; sigs: Map; models: Map } { const sigs = new Map(set.signatures ?? []); const diags: Diagnostic[] = [...set.diags]; // MO-9: a module known by its .d.rx - for (const path of set.order) { const r = checkWithSignature(set.programs.get(path)!, { sigs, file: path, declarations: set.declarations }); diags.push(...r.diags); sigs.set(path, r.sig); } - return { diags, sigs }; + const models = new Map(); // CP-6: each module's resolved model + for (const path of set.order) { const r = checkWithSignature(set.programs.get(path)!, { sigs, file: path, declarations: set.declarations }); diags.push(...r.diags); sigs.set(path, r.sig); models.set(path, r.model); } + return { diags, sigs, models }; } class Checker { diags: Diagnostic[] = []; + model = new ModelBuilder(); // CP-6: the resolved facts, recorded as the walk resolves them private opts: Opts; private rootEnv: Env | null = null; constructor(opts: Opts = {}) { this.opts = opts; } @@ -201,14 +204,19 @@ class Checker { private compSyms = new Map(); private rowsBusy = new Set(); private err(code: string, rule: string, msg: string) { if (!this.diags.some(d => d.code === code && d.line === this.line && d.msg === msg)) this.diags.push({ code, rule, line: this.line, msg, ...(this.opts.file ? { file: this.opts.file } : {}) }); } - private region(parent: Region | null, opts: { component?: boolean; provParent?: Region | null } = {}): Region { + private region(parent: Region | null, kind: RegionKind, opts: { component?: boolean; provParent?: Region | null; name?: string } = {}): Region { const r: Region = { id: this.seq++, depth: parent ? parent.depth + 1 : 0, parent, component: null, isComponentRoot: !!opts.component, provParent: opts.provParent === undefined ? parent : opts.provParent, appSites: [], provides: new Set() }; r.component = opts.component ? r : parent?.component ?? null; + this.model.region(r.id, kind, parent, opts.name); return r; } + /** CP-6: a computation the walk enters — its reads, writes and calls are recorded on it. */ + private comp(kind: CompKind, env: Env, info: { name?: string; at?: string }, f: () => void) { this.model.within(kind, env.region, this.line, info, f); } + /** CP-6: a symbol bound in `env`, recorded for the model. */ + private bind(env: Env, name: string, sym: Sym) { env.vars.set(name, sym); this.model.sym(sym); } run(p: Program) { - const root = new Env(null, this.region(null)); this.rootEnv = root; + const root = new Env(null, this.region(null, "program")); this.rootEnv = root; if (p.legacyView) { this.line = p.view[0]?.line ?? 1; this.err("TOP_LEVEL_MARKUP", "MO-1c", "markup at a module's top level: a module is declarations; the app's view is a component — `export default component App() { … }`"); } this.block(p.decls, p.view, root, { scope: "reactive" }); this.later.push(() => this.contentCheck(p.view, root, [])); @@ -223,13 +231,13 @@ class Checker { this.line = d.line; if (d.k === "import" && d.resolved) { this.importRx(d, env); continue; } // MO-1/MO-3: an .rx module's signature if (d.k === "import" && d.declared) { this.importDeclared(d, env); continue; } // MO-8: a JS module's declaration file - if (d.k === "import") { for (const { alias } of d.names) env.vars.set(alias, { kind: "import", name: alias, region: env.region, line: d.line, io: !d.pure, rejects: new Set(d.fails ?? ["Error"]), side: d.side }); continue; } // R-io-4, R-f-8, SF-5 + if (d.k === "import") { for (const { alias } of d.names) this.bind(env, alias, { kind: "import", name: alias, region: env.region, line: d.line, io: !d.pure, rejects: new Set(d.fails ?? ["Error"]), side: d.side }); continue; } // R-io-4, R-f-8, SF-5 if (d.k === "effect" || d.k === "provide" || d.k === "typedecl") continue; if (d.k === "context") { // RG-7: a declared context — its identity is the declaration, not its spelling if (env.vars.has(d.name)) this.err("DUPLICATE_DECLARATION", "E-7", `${d.name} is declared twice in one block`); const contract = (d.type && ROW_WORDS.test(d.type)) || d.exported ? this.contractOf(d.type) : null; // RG-7e: written rows, or exported (its type is then the contract: no row words, no rows) if (d.source && d.dflt) this.err("INVALID_CONTEXT_DEFAULT", "RG-7c", `context ${d.name}: source … has no default — a provided source is its provider's; a default would be one source every user writes (declare that source instead)`); - env.vars.set(d.name, { kind: "context", name: d.name, region: env.region, line: d.line, ctx: { key: `${this.opts.file ?? "main.rx"}#${d.name}${env.region.parent ? `@${this.seq++}` : ""}`, source: d.source, dflt: !!d.dflt, contract } }); + this.bind(env, d.name, { kind: "context", name: d.name, region: env.region, line: d.line, ctx: { key: `${this.opts.file ?? "main.rx"}#${d.name}${env.region.parent ? `@${this.seq++}` : ""}`, source: d.source, dflt: !!d.dflt, contract } }); if (d.dflt) this.provisions.push({ key: env.vars.get(d.name)!.ctx!.key, e: d.dflt, env }); continue; } @@ -239,10 +247,10 @@ class Checker { if (d.k === "source") sym.question = !!d.question; if (d.k === "action") sym.io = true; // R-io-2: an action is io if (d.k === "fn") { sym.params = d.params; sym.instantiating = d.params.some(p => p.ref) || d.decls.length > 0; sym.fnDecl = d; sym.declEnv = env; } - if (d.k === "component") { sym.comp = d; sym.compRegion = this.region(env.region, { component: true }); this.compSyms.set(sym.compRegion, sym); } + if (d.k === "component") { sym.comp = d; sym.compRegion = this.region(env.region, "component", { component: true, name: d.name }); this.compSyms.set(sym.compRegion, sym); } if (d.k === "eq") { sym.mapsRecords = !d.key && mapsIntoRecords(d.e); if (d.e.k === "id") { const t = env.lookup(d.e.name); if (t?.io) sym.io = true; } } if (d.k === "eq") { const sf = unwrapServer(d.e); if (sf) { sym.kind = "server"; sym.params = sf.params.map(name => ({ name, ref: false })); this.serverExprs.set(sf, sym); } } // SF-1d: a wrapped server fn is one, its rows the function's - env.vars.set(d.name, sym); + this.bind(env, d.name, sym); } this.serverIo(decls, env); for (const d of decls) if (d.k === "source" && !d.question) env.vars.get(d.name)!.failsT = this.failsThunk(d.init, env); // R-f-2: a source holds what its initializer gave it — an error, if that failed (found by S-ai2-settings) @@ -282,8 +290,8 @@ class Checker { for (const { name, alias } of d.names) { const base: Sym = { kind: "import", name: alias, region: env.region, line: d.line }; const x = sig?.exports.get(name); - if (!sig) { env.vars.set(alias, base); continue; } // the module was not found or not checked (reported there) - if (!x) { this.err("NOT_EXPORTED", "MO-6", `${d.from} does not export ${name === "default" ? "a default" : name}`); env.vars.set(alias, base); continue; } + if (!sig) { this.bind(env, alias, base); continue; } // the module was not found or not checked (reported there) + if (!x) { this.err("NOT_EXPORTED", "MO-6", `${d.from} does not export ${name === "default" ? "a default" : name}`); this.bind(env, alias, base); continue; } if (x.kind === "type") continue; // a type name: text to the checker (07 §2), nothing to bind const rowsOf = (r: string[]) => () => new Set(r); const failsOf = (f: string[]) => () => ({ cls: new Set(f), props: [] as PropRef[] }); @@ -295,11 +303,11 @@ class Checker { case "action": sym = { kind: "action", name: alias, region: env.region, line: d.line, io: true }; break; case "server": { const rj = new Set(x.rejects); sym = { kind: "server", name: alias, region: env.region, line: d.line, io: x.io, params: x.params.map(n => ({ name: n, ref: false })), serverRejects: () => rj }; break; } case "errclass": sym = { kind: "errclass", name: alias, region: env.region, line: d.line }; break; - case "comp": { const r = this.region(null, { component: true }); r.sig = x.sig; sym = { kind: "comp", name: alias, region: env.region, line: d.line, compRegion: r }; break; } + case "comp": { const r = this.region(null, "imported", { component: true, name: alias }); r.sig = x.sig; sym = { kind: "comp", name: alias, region: env.region, line: d.line, compRegion: r }; break; } case "context": sym = { kind: "context", name: alias, region: env.region, line: d.line, ctx: { key: x.key, source: x.source, dflt: x.dflt, contract: { rows: new Set(x.rows), fails: new Set(x.fails) } } }; break; } if (d.pure && sym.io) this.err("IMPORT_ROW_MISMATCH", "MO-4", `${name} is io in ${d.from}'s signature; \`pure\` cannot lower an .rx row`); - env.vars.set(alias, sym); + this.bind(env, alias, sym); } } /** RG-7: the context a `provide`/`use` names — lexically, like any name. */ @@ -322,12 +330,12 @@ class Checker { if (d.fails) this.err("IMPORT_ROW_MISMATCH", "MO-8", `${d.from}'s rows are declared in ${file}: \`fails\` on the import cannot restate them`); for (const { name, alias } of d.names) { const x = decls.find(y => y.name === name); - if (!x) { this.err("NOT_EXPORTED", "MO-8", `${file} declares no export ${name} (declare it, or it is not ${d.from}'s)`); env.vars.set(alias, { kind: "import", name: alias, region: env.region, line: d.line, io: true }); continue; } + if (!x) { this.err("NOT_EXPORTED", "MO-8", `${file} declares no export ${name} (declare it, or it is not ${d.from}'s)`); this.bind(env, alias, { kind: "import", name: alias, region: env.region, line: d.line, io: true }); continue; } if (x.kind === "type") continue; - if (x.kind === "error") { env.vars.set(alias, { kind: "errclass", name: alias, region: env.region, line: d.line }); continue; } + if (x.kind === "error") { this.bind(env, alias, { kind: "errclass", name: alias, region: env.region, line: d.line }); continue; } if (d.pure && !x.pure) this.err("IMPORT_ROW_MISMATCH", "MO-8", `${name} is declared io in ${file} (\`export fn\`, not \`export pure fn\`); \`pure\` on the import cannot lower it`); if (d.side && x.side !== d.side) this.err("IMPORT_ROW_MISMATCH", "SF-5", `${name} is declared ${x.side ? `\`${x.side}\`` : "for both sides"} in ${file}; \`${d.side}\` on the import cannot restate it`); - env.vars.set(alias, { kind: "import", name: alias, region: env.region, line: d.line, io: !x.pure, rejects: new Set(x.rejects ?? ["Error"]), side: x.side }); + this.bind(env, alias, { kind: "import", name: alias, region: env.region, line: d.line, io: !x.pure, rejects: new Set(x.rejects ?? ["Error"]), side: x.side }); } } /** MO-3: this module's signature — every export as an importer sees it. */ @@ -406,19 +414,20 @@ class Checker { case "typedecl": return; case "error": if (d.msg) { const inner = new Env(env, env.region); for (const p of d.params) inner.vars.set(p, { kind: "value", name: p, region: env.region, line: d.line }); this.expr(d.msg, inner, { scope: "reactive", inFn: true }); } return; case "source": { - if (d.question) { this.expr(d.init, env, reactive); this.rowCheck(d.name, d.type, env); return; } + if (d.question) { this.comp("question", env, { name: d.name }, () => this.expr(d.init, env, reactive)); this.rowCheck(d.name, d.type, env); return; } const reads = new Set(); readsOutsidePeek(d.init, reads); const hit = [...reads].find(n => { const s = env.lookup(n); return s && reactiveKinds.has(s.kind); }); if (hit) this.err("SNAPSHOT_INITIALIZER", "E-4", `source ${d.name} := … reads ${hit} outside peek(…): write the snapshot as one (DV-6)`); - this.expr(d.init, env, { scope: "reactive", inFn: ctx.inFn }); + this.comp("seed", env, { name: d.name }, () => { this.model.snap++; this.expr(d.init, env, { scope: "reactive", inFn: ctx.inFn }); }); // E-4: a seed is a snapshot return; } case "eq": { - if (d.e.k === "call" && d.e.f.k === "id") { const f = env.lookup(d.e.f.name); if (f?.kind === "fn" && f.instantiating) { this.snapshotArgs(d.e, f, env); d.e.args.forEach(a => this.expr(a, env, reactive)); this.rowCheck(d.name, d.type, env); return; } } - this.expr(d.e, env, reactive); this.rowCheck(d.name, d.type, env); return; + const kind = d.deferred ? "deferred" : d.key ? "keyed" : "memo"; + if (d.e.k === "call" && d.e.f.k === "id") { const f = env.lookup(d.e.f.name); if (f?.kind === "fn" && f.instantiating) { const call = d.e; this.snapshotArgs(call, f, env); this.comp(kind, env, { name: d.name }, () => { this.model.call(f); call.args.forEach((a, i) => { const value = !!f.params![i] && !f.params![i].ref; if (value) this.model.snap++; this.expr(a, env, reactive); if (value) this.model.snap--; }); }); this.rowCheck(d.name, d.type, env); return; } } // E-10: a value parameter's argument is a snapshot + this.comp(kind, env, { name: d.name }, () => this.expr(d.e, env, reactive)); this.rowCheck(d.name, d.type, env); return; } case "provide": { - this.expr(d.e, env, reactive); + this.comp("provide", env, { name: d.name }, () => this.expr(d.e, env, reactive)); const c = this.ctxOf(d.name, env); if (!c) return; env.region.provides.add(c.ctx!.key); this.provisions.push({ key: c.ctx!.key, e: d.e, env }); if (c.ctx!.source) { const t = d.e.k === "id" ? env.lookup(d.e.name) : undefined; if (!t || t.kind !== "source" || t.imported) this.err("PROVIDE_NOT_A_SOURCE", "RG-7c", `context ${d.name} carries a source: provide one of this module's sources by name (\`provide ${d.name} = x\`)`); } @@ -431,41 +440,42 @@ class Checker { }); return; } - case "context": if (d.dflt) this.expr(d.dflt, env, reactive); return; + case "context": if (d.dflt) { const dflt = d.dflt; this.comp("default", env, { name: d.name }, () => this.expr(dflt, env, reactive)); } return; case "effect": { - for (const i of d.inputs) this.expr(i, env, reactive); // EF-1: the compute half is reactive scope - this.stmts(d.body, new Env(env, env.region), { scope: "imperative" }); + this.comp("effect", env, {}, () => { + for (const i of d.inputs) this.expr(i, env, reactive); // EF-1: the compute half is reactive scope + this.comp("apply", env, {}, () => this.stmts(d.body, new Env(env, env.region), { scope: "imperative" })); + }); this.ownSourceWrites(d, env); return; } case "action": { const inner = new Env(env, env.region); for (const p of d.params) inner.vars.set(p, { kind: "value", name: p, region: env.region, line: d.line }); - this.stmts(d.body, inner, { scope: "imperative", inAction: true }); + this.comp("action", env, { name: d.name }, () => this.stmts(d.body, inner, { scope: "imperative", inAction: true })); this.actions.push({ name: d.name, body: d.body, env: inner, line: d.line }); return; } case "server": { // 12 SF-1/SF-3/SF-4: a statement block that runs on the server const inner = new Env(env, env.region); for (const p of d.params) inner.vars.set(p, { kind: "value", name: p, region: env.region, line: d.line }); - this.stmts(d.body, inner, { scope: "imperative", server: true }); + this.comp("server", env, { name: d.name }, () => this.stmts(d.body, inner, { scope: "imperative", server: true })); let cache: Set | null = null; env.lookup(d.name)!.serverRejects = () => cache ??= new Set([...this.stmtsFails(d.body, inner, new Map()).cls, "Error"]); // SF-6b: every call can also fail in transit return; } case "fn": { - const r = d.decls.length ? this.region(env.region) : env.region; // RG-6: an instantiating fn is a region + const r = d.decls.length ? this.region(env.region, "fn", { name: d.name }) : env.region; // RG-6: an instantiating fn is a region const inner = new Env(env, r); - for (const p of d.params) inner.vars.set(p.name, { kind: p.ref ? "eq" : "value", name: p.name, region: r, line: d.line }); - this.block(d.decls, [], inner, { scope: "reactive", inFn: true }); - this.expr(d.tail, inner, { scope: "reactive", inFn: true }); + for (const p of d.params) this.bind(inner, p.name, { kind: p.ref ? "eq" : "value", name: p.name, region: r, line: d.line }); + this.comp("fn", env, { name: d.name }, () => { this.block(d.decls, [], inner, { scope: "reactive", inFn: true }); this.expr(d.tail, inner, { scope: "reactive", inFn: true }); }); { const sym = env.lookup(d.name)!; sym.fnTail = d.tail; sym.fnEnv = inner; sym.failsT = this.failsThunk(d.tail, inner); sym.rows = this.rowsThunk(d.tail, inner); } return; } case "component": { const sym = env.lookup(d.name)!; const r = sym.compRegion!; const inner = new Env(env, r); - inner.vars.set(d.props, { kind: "props", name: d.props, region: r, line: d.line }); + this.bind(inner, d.props, { kind: "props", name: d.props, region: r, line: d.line }); sym.compEnv = inner; this.block(d.decls, d.view, inner, { scope: "reactive" }); this.later.push(() => this.contentCheck(d.view, inner, [])); @@ -568,21 +578,24 @@ class Checker { } return null; } - private expr(e: Expr, env: Env, ctx: Ctx): void { + /** `field`: the member read of an identifier (`props.title`), for the model (CP-6). */ + private expr(e: Expr, env: Env, ctx: Ctx, field?: string): void { const go = (x: Expr) => this.expr(x, env, ctx); switch (e.k) { case "lit": return; - case "id": { const s = this.resolve(e.name, env, ctx); if (s?.kind === "comp") this.err("COMPONENT_AS_VALUE", "RG-4a", `component ${e.name} used as a value: apply it in a view`); if (s?.kind === "context") this.err("CONTEXT_AS_VALUE", "RG-7", `context ${e.name} used as a value: read the nearest provision with \`use ${e.name}\``); return; } - case "pre": { const s = this.resolve(e.name, env, ctx); if (s && !reactiveKinds.has(s.kind)) this.err("PRE_OF_NON_REACTIVE", "C-2", `pre of ${e.name}, which is not reactive`); if (e.init) go(e.init); return; } - case "use": { if (ctx.server) this.err("SERVER_CAPTURE", "SF-3", `a server function's body reads \`use ${e.name}\`: a provision is the client's — pass it as an argument`); const c = this.ctxOf(e.name, env); if (c && !c.ctx!.dflt) this.uses.push({ region: env.region, name: c.ctx!.key, line: this.line, display: e.name }); return; } - case "peek": case "latest": case "pending": case "un": case "spread": go(e.e); return; + case "id": { const s = this.resolve(e.name, env, ctx); if (s) this.model.read(s, ctx.scope === "imperative", false, field); if (s?.kind === "comp") this.err("COMPONENT_AS_VALUE", "RG-4a", `component ${e.name} used as a value: apply it in a view`); if (s?.kind === "context") this.err("CONTEXT_AS_VALUE", "RG-7", `context ${e.name} used as a value: read the nearest provision with \`use ${e.name}\``); return; } + case "pre": { const s = this.resolve(e.name, env, ctx); if (s) this.model.read(s, ctx.scope === "imperative", true); if (s && !reactiveKinds.has(s.kind)) this.err("PRE_OF_NON_REACTIVE", "C-2", `pre of ${e.name}, which is not reactive`); if (e.init) go(e.init); return; } + case "use": { if (ctx.server) this.err("SERVER_CAPTURE", "SF-3", `a server function's body reads \`use ${e.name}\`: a provision is the client's — pass it as an argument`); const c = this.ctxOf(e.name, env); if (c) this.model.read(c, ctx.scope === "imperative"); if (c && !c.ctx!.dflt) this.uses.push({ region: env.region, name: c.ctx!.key, line: this.line, display: e.name }); return; } + case "peek": this.model.snap++; go(e.e); this.model.snap--; return; // E-5: a snapshot + case "latest": case "pending": { const save = this.model.probe; this.model.probe = e.k; go(e.e); this.model.probe = save; return; } // R-9, R-12: probes + case "un": case "spread": go(e.e); return; case "await": if (ctx.scope === "imperative" && !this.stmtAwaits.has(e)) this.err("AWAIT_IN_EXPRESSION", "AC-1", `\`${show(e)}\` inside an expression of imperative scope: a run suspends only at a statement — bind it first, \`let v = ${show(e)}\` (a \`catch (e) => alt\` may follow it)`); this.awaitedCalls.add(e.e); go(e.e); if (e.init) go(e.init); return; case "serverfn": { // SF-1d: as \`server fn\`, wherever it is written const inner = new Env(env, env.region); for (const p of e.params) inner.vars.set(p, { kind: "value", name: p, region: env.region, line: this.line }); - this.stmts(e.body, inner, { scope: "imperative", server: true }); + this.comp("server", env, {}, () => this.stmts(e.body, inner, { scope: "imperative", server: true })); const sym = this.serverExprs.get(e); let cache: Set | null = null; if (sym) sym.serverRejects = () => cache ??= new Set([...this.stmtsFails(e.body, inner, new Map()).cls, "Error"]); return; @@ -596,7 +609,7 @@ class Checker { case "bin": go(e.l); go(e.r); return; case "if": go(e.c); go(e.t); go(e.f); return; case "tpl": for (const p of e.parts) if (typeof p !== "string") go(p); return; - case "member": go(e.o); return; + case "member": if (e.o.k === "id") this.expr(e.o, env, ctx, e.p); else go(e.o); return; case "index": go(e.o); go(e.i); return; case "arr": e.items.forEach(go); return; case "rec": for (const f of e.fields) go("spread" in f ? f.spread : f.v); return; @@ -617,6 +630,7 @@ class Checker { if (s?.side === "client" && ctx.scope === "reactive" && !ctx.server && !this.awaitedCalls.has(e)) this.err("CLIENT_NOT_AWAITED", "SF-5b", `${e.f.name}(…) is \`client\` JS: in reactive scope it is awaited — a client question, asked once the browser has the page (SV-7b) — never called for its value now`); if (s?.kind === "fn" && s.instantiating) this.err("INSTANTIATION_NOT_BOUND", "E-9", `${e.f.name}(…) declares state: bind it, \`x = ${e.f.name}(…)\` (RG-6)`); if (s?.kind === "comp") this.err("COMPONENT_AS_VALUE", "RG-4a", `component ${e.f.name} called as a function: apply it in a view`); + this.model.call(s); } go(e.f); e.args.forEach(go); return; } @@ -666,10 +680,10 @@ class Checker { let target: Sym | undefined; if (s.target.ctx) { const c = this.ctxOf(s.target.name, env); - if (c) { this.uses.push({ region: env.region, name: c.ctx!.key, line: this.line, display: s.target.name }); if (!c.ctx!.source) this.err("WRITE_TO_NON_SOURCE", "RG-7c", `context ${s.target.name} is not declared \`source\`: what is provided is read-only (\`context ${s.target.name}: source T\` to write through it)`); } + if (c) { this.model.write(c, s.k, s.target.path.length > 0); this.uses.push({ region: env.region, name: c.ctx!.key, line: this.line, display: s.target.name }); if (!c.ctx!.source) this.err("WRITE_TO_NON_SOURCE", "RG-7c", `context ${s.target.name} is not declared \`source\`: what is provided is read-only (\`context ${s.target.name}: source T\` to write through it)`); } } else { - target = this.resolve(s.target.name, env, ctx); + target = this.resolve(s.target.name, env, ctx); this.model.write(target, s.k, s.target.path.length > 0); if (target && target.kind !== "source") this.err("WRITE_TO_NON_SOURCE", "W-1", `${s.target.name} is ${target.kind === "eq" ? "an equation" : target.kind === "props" ? "props" : `a ${target.kind}`}: only sources are written (W-1)`); else if (target?.imported) this.err("WRITE_TO_NON_SOURCE", "MO-2", `${s.target.name} is a source of ${target.imported}: a module boundary is a value boundary — write it through an action ${target.imported} exports`); } @@ -683,9 +697,9 @@ class Checker { break; } case "refresh": case "affects": case "action": if (ctx.server) { this.err("SERVER_CAPTURE", "SF-3", `\`${s.k}${s.k === "action" ? " { … }" : ` ${(s as { name: string }).name}`}\` in a server function's body: it is the client's (do it in the action that awaited the call)`); break; } - if (s.k === "action") { this.stmts(s.body, env, { scope: "imperative", inAction: true }); this.actions.push({ name: "(inline)", body: s.body, env, line: this.line }); break; } - if (s.k === "affects") { const t = this.resolve(s.name, env, ctx); if (t && !reactiveKinds.has(t.kind)) this.err("INVALID_AFFECTS_TARGET", "AC-5", `${s.name} is not reactive`); break; } - { const t = this.resolve(s.name, env, ctx); if (t && !(t.kind === "eq" || (t.kind === "source" && t.question))) this.err("INVALID_REFRESH_TARGET", "AC-4", `${s.name} has no question to re-ask`); break; } + if (s.k === "action") { this.comp("action", env, { name: "(inline)" }, () => this.stmts(s.body, env, { scope: "imperative", inAction: true })); this.actions.push({ name: "(inline)", body: s.body, env, line: this.line }); break; } + if (s.k === "affects") { const t = this.resolve(s.name, env, ctx); this.model.write(t, "affects"); if (t && !reactiveKinds.has(t.kind)) this.err("INVALID_AFFECTS_TARGET", "AC-5", `${s.name} is not reactive`); break; } + { const t = this.resolve(s.name, env, ctx); this.model.write(t, "refresh"); if (t && !(t.kind === "eq" || (t.kind === "source" && t.question))) this.err("INVALID_REFRESH_TARGET", "AC-4", `${s.name} has no question to re-ask`); break; } case "sif": this.expr(s.c, env, ctx); this.stmts(s.t, env, ctx); this.stmts(s.f, env, ctx); break; case "try": { this.stmts(s.body, env, ctx); @@ -741,50 +755,50 @@ class Checker { if (!viewRoot) this.err("STYLE_PLACEMENT", "CSS-1", "a