diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a946d07 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +# CI runs test, clippy, and fmt. Uses rustup and run steps only (no Node-based actions) so it works with act. +# Container is ubuntu:22.04 (has curl; node image has no curl). Cache omitted so we don't need Node. On Apple M-series: act --container-architecture linux/amd64 + +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ubuntu-latest + container: + image: ubuntu:22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install curl, build-essential, and Rust + run: | + apt-get update && apt-get install -y curl build-essential + curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + "$HOME/.cargo/bin/rustup" component add rustfmt clippy + + - name: cargo test + run: . "$HOME/.cargo/env" && cargo test + + clippy: + name: Clippy + runs-on: ubuntu-latest + container: + image: ubuntu:22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install curl, build-essential, and Rust + run: | + apt-get update && apt-get install -y curl build-essential + curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + "$HOME/.cargo/bin/rustup" component add clippy + + - name: cargo clippy + run: . "$HOME/.cargo/env" && cargo clippy + + fmt: + name: Format + runs-on: ubuntu-latest + container: + image: ubuntu:22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install curl, build-essential, and Rust + run: | + apt-get update && apt-get install -y curl build-essential + curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + "$HOME/.cargo/bin/rustup" component add rustfmt + + - name: cargo fmt --check + run: . "$HOME/.cargo/env" && cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore index 2f7896d..f8ff3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ target/ + +# macOS +.DS_Store + +# IDE +.idea/ +.vscode/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5ca82b3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1825 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[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 = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "instability", + "itertools", + "lru", + "paste", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[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.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[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 = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "v2r" +version = "0.1.0" +dependencies = [ + "crossterm", + "ratatui", + "regex", + "reqwest", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..658a829 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# B2 — scripting runtime +# Usage: make [target]; run script with: make run SCRIPT=hello.s + +.PHONY: build test run repl install check fmt clean help + +# Default: build +all: build + +build: + cargo build + +# Release build +release: + cargo build --release + +test: + cargo test + +# Run a script (e.g. make run SCRIPT=hello.s) +run: + @if [ -z "$(SCRIPT)" ]; then \ + echo "Usage: make run SCRIPT=path/to/script.s"; \ + exit 1; \ + fi + cargo run -- $(SCRIPT) + +# Run REPL +repl: + cargo run -- repl + +# Install binary to cargo bin dir +install: + cargo install --path . + +# Lint and format check (no write) +check: + cargo clippy -- -D warnings + cargo fmt -- --check + +# Format code +fmt: + cargo fmt + +# Remove build artifacts +clean: + cargo clean + +help: + @echo "B2 Makefile targets:" + @echo " make build - build debug binary (default)" + @echo " make release - build release binary" + @echo " make test - run tests" + @echo " make run SCRIPT=file.s - run a B2 script" + @echo " make repl - start interactive REPL" + @echo " make install - install binary (cargo install --path .)" + @echo " make check - clippy + fmt check" + @echo " make fmt - format code" + @echo " make clean - remove build artifacts" + @echo " make help - show this help" diff --git a/README.md b/README.md index 8714e95..c1a6bfb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,50 @@ -# B2 +# B2 -A toy scripting language runtime in Rust. Spiritual successor to Shelltrac, which was intended to easily shell or ssh out and script with those results, but faster. +A toy scripting language runtime in Rust. B2 is built for **shell and SSH orchestration**: run local or remote commands, get structured results (stdout, stderr, exit code), then process them with a simple language—functions, arrays, loops, and parallel iteration. +Spiritual successor to Shelltrac: script with shell/SSH results, but faster. -## Running Scripts: +--- + +## Prerequisites + +- [Rust](https://rustup.rs) (1.85+ for edition 2024; or set `edition = "2021"` in `Cargo.toml` for older toolchains) + +--- + +## Quick start + +**Run a script:** + +```bash +cargo run -- hello.s +``` + +**Interactive REPL:** + +```bash +cargo run -- repl +``` + +**Run from stdin:** + +```bash +cargo run -- < script.s +``` + +--- + +## Language basics + +- **Variables:** `let x = 10` +- **Functions:** `fn greet(name, suffix = "!") { return "hello, #{name}#{suffix}" }` +- **Interpolation:** `"hello, #{name}"` +- **Arrays:** `[1, 2, 3]` +- **Named args:** `greet(name = "world", suffix = "!")` +- **Lambdas:** `find(nums, n -> n > 3)` +- **Control flow:** `if` / `else`, `for`, `while`, `try` / `catch` / `finally` + +Example: ```bash cat > hello.s <<'EOF' @@ -21,10 +62,102 @@ EOF cargo run -- hello.s ``` -Explore the code to discover: `sh`/`ssh` blocks, `parallel for` loops. +--- + +## Shell and SSH + +### `sh` — run local commands + +Run a command string or a block. Returns a value with `stdout`, `stderr`, `exit_code`, and `duration_ms`. + +**Expression form:** + +```text +let r = sh "ls -la /tmp" +log r.stdout +if r.exit_code != 0 { log "Failed: " + r.stderr } +``` + +**Block form (multi-line):** + +```text +let r = sh { + echo "hello" + whoami + date +} +log r.stdout +``` + +### `ssh` — run remote commands + +Same idea, with a host first: `ssh host command` or `ssh host { ... }`. + +```text +let r = ssh "myserver" "uptime" +log r.stdout + +parallel for host in ["server1", "server2"] { + let r = ssh host "df -h /" + log host + ": " + r.stdout +} +``` + +--- + +## Parallel loops + +`parallel for` runs loop iterations in parallel (threads). Use it to run the same task across many items without waiting for each one in sequence. + +```text +parallel for host in ["host1", "host2", "host3"] { + let r = ssh host "uptime" + log host + " -> " + r.stdout +} +``` + +--- + +## Built-in functions + +| Function | Description | +|----------|-------------| +| `log(x)` | Print with timestamp (or no args for newline). | +| `echo(x)` | Print value, no timestamp. | +| `read_file(path)` | Read file contents as a string. | +| `wait(ms)` / `wait(duration)` | Sleep (e.g. `wait(100)` or `wait("2s")`). | +| `getType(x)` | Return type name as string. | +| `unwrap(x)` | Unwrap a nominal wrapper value. | +| `map(iterable, fn)` | Map over collection. | +| `filter(iterable, fn)` | Filter by predicate. | +| `find(iterable, fn)` | First element matching predicate (or null). | +| `findIndex(iterable, fn)` | Index of first match, or -1. | +| `reduce(iterable, fn, initial)` | Reduce to single value. | +| `flatMap(iterable, fn)` | Map and flatten. | +| `any(iterable, fn)` | True if any element matches. | +| `all(iterable, fn)` | True if all match. | +| `sort(iterable)` / `sort(iterable, comparator)` | Sort (optional comparator). | +| `sortBy(iterable, selector, descending?)` | Sort by key. | +| `groupBy(iterable, selector)` | Group into dict by key. | +| `partition(iterable, predicate)` | Split into [pass, fail]. | +| `distinct(iterable)` / `distinct(iterable, selector)` | Unique elements. | +| `collect(iterable)` | Collect iterator/generator to array. | +| `next(generator)` | Advance generator. | +| `validateType(value, typeName)` | Type check, returns bool. | + +**String methods** (e.g. `s.method(args)`): `length`, `toUpper`, `toLower`, `trim`, `split(sep)`, `lines`, `contains(sub)`, `startsWith(sub)`, `endsWith(sub)`, `replace(from, to)`, `substring(start, len)`, `indexOf(sub)`, `join(array)`, `padLeft(n)`, `padRight(n)`, `remove(start, len)`. + +**Number methods**: `abs`, `floor`, `ceil`, `round`, `min(x)`, `max(x)`, `clamp(lo, hi)`, `pow(exp)`, `sqrt`, `toInt`, `toFloat`, `isInt`, `toString`. + +**`net` module** (e.g. `net.http(url)`): `ping(host)`, `http(url | options)`, `tcp_connect(host, port)`, `tcp_listen(host, port)`. + +--- ## REPL ```bash cargo run -- repl ``` + +Evaluate expressions, call `sh`/`ssh`, inspect results. Use REPL commands (e.g. `vars`) if the REPL supports them—see the REPL help or source. + diff --git a/docs/feature-plan.md b/docs/feature-plan.md new file mode 100644 index 0000000..4a6b23f --- /dev/null +++ b/docs/feature-plan.md @@ -0,0 +1,126 @@ +# B2 product feature plan + +Jan and Niek are boring as hell. + +--- + +## Intro + +B2 today gives you: a small language (functions, lambdas, arrays/dicts, control flow, types/records), **sh** / **ssh** returning `{ stdout, stderr, exit_code, duration_ms }`, **parallel for**, builtins (log, read_file, map/filter/sort/etc.), **net** (ping, http, tcp), string/number methods, a TUI REPL, and script imports. There is **read_file** but no **write_file**; no script arguments or env access; no JSON; no timeout or cwd for shell; REPL has no way to load a script file. + +--- + +## 1. High value, low effort + +**File I/O** + +- **write_file(path, contents)** — Scripts often need to write results or configs. Today only [core.rs](../src/runtime/builtins/core.rs) has `read_file`. Add a native that does `fs::write`. Optional: append mode. +- **list_dir(path)** — Return array of filenames (or names + metadata) for "run command per file" or "find files matching" workflows. +- **path_exists(path)** / **is_file** / **is_dir** — Avoid calling `sh "test -f ..."` for simple checks. + +**Script arguments and env** + +- **argv** — Global or builtin that exposes script arguments (e.g. `cargo run -- script.s --foo bar` → script can read args). Requires [args.rs](../src/cli/args.rs) and [io.rs](../src/cli/io.rs) to pass args through to the executor and expose them (e.g. as a global array or `args()` builtin). +- **env** — Read env vars from script (e.g. `env("HOME")`) so scripts can adapt to environment without `sh "echo $HOME"`. + +**Shell result ergonomics** + +- **.ok** on the dict returned by `sh`/`ssh` — e.g. `r.ok` true when `exit_code == 0`. Convenience for conditionals. +- **.lines** — Return array of lines from `stdout` (or a method on string that's already there, used on `r.stdout`). Very common after running a command. + +These fit the "script with shell results" story and are straightforward to add in the runtime and builtins. + +--- + +## 2. High value, mid effort + +**JSON** + +- **parse_json(string)** and **to_json(value)** — Many scripts consume API output or config files. Today you'd have to call an external tool. Adding a dependency (e.g. `serde_json`) and two builtins gives a big upgrade for "script + HTTP + data" use cases. + +**Shell/SSH upgrades** + +- **Timeout** for `sh` and `ssh` — Optional timeout argument so long-running or stuck commands don't hang the script. [sh.rs](../src/runtime/shell/sh.rs) and [ssh.rs](../src/runtime/shell/ssh.rs) would need to run the command with a timeout (e.g. Rust timeout or background thread + kill). +- **Working directory** for `sh` — Optional `cwd` so scripts can run commands in a given directory without building cd into the command string. +- **SSH options** — Key path, port, user (or parse `user@host`). Makes SSH usable in more real-world setups. + +**Stdio** + +- **stdin()** — Read stdin (e.g. whole input or line-by-line) so B2 scripts can be used in pipes: `cat list.txt | b2 script.s`. +- **stderr** builtin or channel — Explicit "log to stderr" for tooling (vs log to stdout). + +**REPL** + +- **Load script** — Command or builtin to evaluate a `.s` file inside the REPL (e.g. `:load script.s`) so you can experiment on top of a script's definitions. + +--- + +## 3. Medium value, variable effort + +**Concurrency control** + +- **Rate limit / semaphore for parallel for** — Cap concurrency (e.g. "at most 5 SSH at a time") to avoid thundering herd and respect remote limits. Could be a modifier on `parallel for` or a wrapper builtin. + +**Assertions and testing** + +- **assert(condition, message?)** — Fail script with a clear message. Useful for scripts that validate state. +- **Test runner** — Optional `cargo test`-driven runner for `.s` files (run script, assert on exit code or stdout). Builds on existing e2e-style tests. + +**Packaging and distribution** + +- **Shebang** — Support `#!/usr/bin/env b2` so scripts can be executed directly (e.g. `./script.s`) once `b2` is on PATH. +- **Install** — Document or streamline `cargo install --path .` so users get a single `b2` (or `v2r`) binary. + +**Docs and discovery** + +- **REPL help** — `:help` or `?log` showing builtin/module docs. Requires storing doc strings and exposing them in the REPL. +- **Standard library doc** — One place (e.g. README or `docs/stdlib.md`) listing builtins and `net` with signatures and short examples. + +--- + +## 4. Large/niche additions + +- **Regex builtin** — Match/split/replace by regex without calling out to `grep`/`sed`. Depends on whether you want to keep "shell-oriented" or add more string power in-process. +- **Config file** — Optional config (e.g. default SSH key, timeout, parallelism) so scripts don't hardcode. +- **Debug / trace** — `trace(expr)` or `breakpoint()` that prints value and span; or a debugger hook for stepping (much larger). +- **More net** — HTTPS client options (headers, POST body, auth), or a small WebSocket helper if needed for scripting UIs or services. + +--- + +## Prio + +1. **Quick wins** — `write_file`, `path_exists`, `argv`, `env`, and `.ok` / `.lines` on shell results. These directly support "script that runs commands and writes results or branches on args/env." +2. **JSON** — `parse_json` / `to_json` plus `net.http` make "call API and process JSON" a first-class workflow. +3. **Shell/SSH** — Timeout and optional cwd for `sh`; SSH key/port/user. Then stdin/stderr for pipes and tooling. +4. **REPL and DX** — Load script in REPL, assert builtin, then REPL help and stdlib doc. +5. **Scale and polish** — Concurrency cap for parallel for, shebang, test runner, then config and niche features as needed. + +--- + +## Summary diagram + +```mermaid +flowchart LR + subgraph quick [Quick wins] + write_file[write_file] + list_dir[list_dir] + path_exists[path_exists] + argv[argv] + env[env] + shell_ok[.ok / .lines] + end + subgraph next [Next] + json[JSON parse/to_json] + sh_timeout[sh/ssh timeout] + sh_cwd[sh cwd] + ssh_opts[SSH key/port] + stdin[stdin] + end + subgraph dx [DX] + repl_load[REPL load script] + assert_builtin[assert] + repl_help[REPL help] + end + quick --> next + next --> dx +``` diff --git a/src/cli/io.rs b/src/cli/io.rs index 1049595..15c1663 100644 --- a/src/cli/io.rs +++ b/src/cli/io.rs @@ -5,8 +5,7 @@ use super::args::CliArgs; pub fn load_source(args: &CliArgs) -> Result<(String, String), String> { if let Some(path) = &args.script_path { - let src = fs::read_to_string(path) - .map_err(|e| format!("Failed to read {path}: {e}"))?; + let src = fs::read_to_string(path).map_err(|e| format!("Failed to read {path}: {e}"))?; return Ok((src, path.clone())); } diff --git a/src/lexer/scanner.rs b/src/lexer/scanner.rs index 0ea1c34..c13b03a 100644 --- a/src/lexer/scanner.rs +++ b/src/lexer/scanner.rs @@ -30,7 +30,12 @@ impl<'a> Scanner<'a> { None => {} } } - tokens.push(Token::new(TokenKind::Eof, self.line, self.column, self.current)); + tokens.push(Token::new( + TokenKind::Eof, + self.line, + self.column, + self.current, + )); Ok(tokens) } @@ -200,7 +205,9 @@ impl<'a> Scanner<'a> { } let slice = &self.source[self.start..self.current]; if is_time_literal(slice) { - return Ok(Some(self.token_from(TokenKind::TimeLiteral(slice.to_string())))); + return Ok(Some( + self.token_from(TokenKind::TimeLiteral(slice.to_string())), + )); } self.current = suffix_start; } @@ -271,16 +278,28 @@ impl<'a> Scanner<'a> { } fn peek(&self) -> char { - if self.is_at_end() { '\0' } else { self.chars[self.current] } + if self.is_at_end() { + '\0' + } else { + self.chars[self.current] + } } fn peek_next(&self) -> char { - if self.current + 1 >= self.chars.len() { '\0' } else { self.chars[self.current + 1] } + if self.current + 1 >= self.chars.len() { + '\0' + } else { + self.chars[self.current + 1] + } } fn match_char(&mut self, expected: char) -> bool { - if self.is_at_end() { return false; } - if self.chars[self.current] != expected { return false; } + if self.is_at_end() { + return false; + } + if self.chars[self.current] != expected { + return false; + } self.current += 1; self.column += 1; true @@ -321,9 +340,102 @@ fn unescape_string(s: &str) -> String { fn is_time_literal(s: &str) -> bool { let mut idx = 0; for c in s.chars() { - if c.is_ascii_digit() { idx += 1; } else { break; } + if c.is_ascii_digit() { + idx += 1; + } else { + break; + } + } + if idx == 0 || idx >= s.len() { + return false; } - if idx == 0 || idx >= s.len() { return false; } let unit = &s[idx..]; matches!(unit, "ms" | "s" | "m" | "h" | "d") } + +#[cfg(test)] +mod tests { + use super::Scanner; + use crate::lexer::token::TokenKind; + + fn token_kinds(source: &str) -> Vec { + let mut scanner = Scanner::new(source); + let tokens = scanner.scan_tokens().unwrap(); + tokens.into_iter().map(|t| t.kind).collect() + } + + #[test] + fn scan_number() { + let kinds = token_kinds("42"); + assert_eq!(kinds.len(), 2); // Number, Eof + assert!(matches!(&kinds[0], TokenKind::Number(s) if s == "42")); + } + + #[test] + fn scan_arithmetic() { + let kinds = token_kinds("1 + 2"); + assert_eq!(kinds.len(), 4); // Number, Plus, Number, Eof + assert!(matches!(&kinds[0], TokenKind::Number(_))); + assert_eq!(kinds[1], TokenKind::Plus); + assert!(matches!(&kinds[2], TokenKind::Number(_))); + assert_eq!(kinds[3], TokenKind::Eof); + } + + #[test] + fn scan_identifiers_and_keywords() { + let kinds = token_kinds("let x = fn"); + assert_eq!(kinds[0], TokenKind::Let); + assert!(matches!(&kinds[1], TokenKind::Identifier(s) if s == "x")); + assert_eq!(kinds[2], TokenKind::Equal); + assert_eq!(kinds[3], TokenKind::Fn); + } + + #[test] + fn scan_string_literal() { + let kinds = token_kinds(r#" "hello" "#); + assert_eq!(kinds.len(), 2); + assert!(matches!(&kinds[0], TokenKind::String(s) if s == "hello")); + } + + #[test] + fn scan_true_false() { + let kinds = token_kinds("true false"); + assert_eq!(kinds[0], TokenKind::True); + assert_eq!(kinds[1], TokenKind::False); + } + + #[test] + fn scan_comparison_ops() { + let kinds = token_kinds("== != < <= > >="); + assert_eq!(kinds[0], TokenKind::EqualEqual); + assert_eq!(kinds[1], TokenKind::NotEqual); + assert_eq!(kinds[2], TokenKind::Less); + assert_eq!(kinds[3], TokenKind::LessEqual); + assert_eq!(kinds[4], TokenKind::Greater); + assert_eq!(kinds[5], TokenKind::GreaterEqual); + } + + #[test] + fn scan_sh_ssh_keywords() { + let kinds = token_kinds("sh ssh parallel"); + assert_eq!(kinds[0], TokenKind::Sh); + assert_eq!(kinds[1], TokenKind::Ssh); + assert_eq!(kinds[2], TokenKind::Parallel); + } + + #[test] + fn scan_brackets_and_braces() { + let kinds = token_kinds("[ ] { }"); + assert_eq!(kinds[0], TokenKind::LBracket); + assert_eq!(kinds[1], TokenKind::RBracket); + assert_eq!(kinds[2], TokenKind::LBrace); + assert_eq!(kinds[3], TokenKind::RBrace); + } + + #[test] + fn scan_arrow_and_fat_arrow() { + let kinds = token_kinds("-> =>"); + assert_eq!(kinds[0], TokenKind::Arrow); + assert_eq!(kinds[1], TokenKind::FatArrow); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..45633c5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,39 @@ +pub mod cli; +pub mod lexer; +pub mod parser; +pub mod repl; +pub mod runtime; +pub mod utils; + +use std::sync::{Arc, Mutex}; + +/// Run a script with default behavior (log/echo go to stdout). Returns an error on parse or runtime failure. +pub fn run_script(source: &str, filename: &str) -> Result<(), String> { + let mut scanner = lexer::scanner::Scanner::new(source); + let tokens = scanner.scan_tokens()?; + let mut parser = parser::parser::Parser::new(tokens, source, filename.to_string()); + let program = parser.parse_program()?; + let mut executor = runtime::executor::Executor::new(filename.to_string(), source.to_string()); + runtime::builtins::register_builtins(&mut executor); + executor.execute(&program).map_err(|e| e.to_string())?; + Ok(()) +} + +/// Run a script and capture all log/echo output lines. Returns error on parse or runtime failure. +pub fn run_script_capture(source: &str) -> Result, String> { + let mut scanner = lexer::scanner::Scanner::new(source); + let tokens = scanner.scan_tokens()?; + let mut parser = parser::parser::Parser::new(tokens, source, "".to_string()); + let program = parser.parse_program()?; + let mut executor = runtime::executor::Executor::new("".to_string(), source.to_string()); + runtime::builtins::register_builtins(&mut executor); + let output = Arc::new(Mutex::new(Vec::::new())); + let out = output.clone(); + executor.set_output_sink(Some(Arc::new(move |line: String| { + if let Ok(mut v) = out.lock() { + v.push(line); + } + }))); + executor.execute(&program).map_err(|e| e.to_string())?; + Ok(output.lock().map(|v| v.clone()).unwrap_or_default()) +} diff --git a/src/main.rs b/src/main.rs index f6bd110..4fa4f57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,7 @@ -mod cli; -mod lexer; -mod parser; -mod repl; -mod runtime; -mod utils; - -use cli::args::{CliArgs, CliCommand}; -use cli::io::load_source; -use runtime::builtins::register_builtins; -use runtime::executor::Executor; +use v2r::cli::args::{CliArgs, CliCommand}; +use v2r::cli::io::load_source; +use v2r::repl; +use v2r::run_script; fn main() { let args = CliArgs::parse(); @@ -28,28 +21,7 @@ fn main() { } }; - let mut scanner = lexer::scanner::Scanner::new(&source); - let tokens = match scanner.scan_tokens() { - Ok(t) => t, - Err(err) => { - eprintln!("{err}"); - std::process::exit(1); - } - }; - - let mut parser = parser::parser::Parser::new(tokens, &source, filename.clone()); - let program = match parser.parse_program() { - Ok(p) => p, - Err(err) => { - eprintln!("{err}"); - std::process::exit(1); - } - }; - - let mut executor = Executor::new(filename, source); - register_builtins(&mut executor); - - if let Err(err) = executor.execute(&program) { + if let Err(err) = run_script(&source, &filename) { eprintln!("{err}"); std::process::exit(1); } diff --git a/src/parser/ast.rs b/src/parser/ast.rs index b4557a6..e529ea5 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -50,15 +50,54 @@ pub enum ImportSource { #[derive(Debug, Clone)] pub enum Stmt { - Expr { expr: Expr, span: Span }, - Let { name: String, expr: Expr, span: Span }, - LetDestructure { pattern: Pattern, expr: Expr, span: Span }, - Assign { name: String, expr: Expr, span: Span }, - IndexAssign { target: Expr, index: Expr, expr: Expr, span: Span }, - If { cond: Expr, then_branch: Vec, else_branch: Vec, span: Span }, - For { pattern: Pattern, iterable: Expr, body: Vec, span: Span }, - ParallelFor { pattern: Pattern, iterable: Expr, body: Vec, span: Span }, - While { cond: Expr, body: Vec, span: Span }, + Expr { + expr: Expr, + span: Span, + }, + Let { + name: String, + expr: Expr, + span: Span, + }, + LetDestructure { + pattern: Pattern, + expr: Expr, + span: Span, + }, + Assign { + name: String, + expr: Expr, + span: Span, + }, + IndexAssign { + target: Expr, + index: Expr, + expr: Expr, + span: Span, + }, + If { + cond: Expr, + then_branch: Vec, + else_branch: Vec, + span: Span, + }, + For { + pattern: Pattern, + iterable: Expr, + body: Vec, + span: Span, + }, + ParallelFor { + pattern: Pattern, + iterable: Expr, + body: Vec, + span: Span, + }, + While { + cond: Expr, + body: Vec, + span: Span, + }, Try { try_block: Vec, catch_name: Option, @@ -74,33 +113,113 @@ pub enum Stmt { is_generator: bool, span: Span, }, - Return { value: Option, span: Span }, - Yield { value: Option, span: Span }, - Continue { span: Span }, - Break { span: Span }, - Throw { value: Expr, span: Span }, - Defer { body: Vec, span: Span }, - Use { module: String, alias: Option, span: Span }, - ImportNamed { items: Vec, source: ImportSource, span: Span }, - ImportNamespace { alias: String, source: ImportSource, span: Span }, - TypeAlias { name: String, target: TypeExpr, span: Span }, - RecordDef { name: String, fields: Vec, span: Span }, - Invoke { name: String, expr: Expr, span: Span }, + Return { + value: Option, + span: Span, + }, + Yield { + value: Option, + span: Span, + }, + Continue { + span: Span, + }, + Break { + span: Span, + }, + Throw { + value: Expr, + span: Span, + }, + Defer { + body: Vec, + span: Span, + }, + Use { + module: String, + alias: Option, + span: Span, + }, + ImportNamed { + items: Vec, + source: ImportSource, + span: Span, + }, + ImportNamespace { + alias: String, + source: ImportSource, + span: Span, + }, + TypeAlias { + name: String, + target: TypeExpr, + span: Span, + }, + RecordDef { + name: String, + fields: Vec, + span: Span, + }, + Invoke { + name: String, + expr: Expr, + span: Span, + }, } #[derive(Debug, Clone)] pub enum Expr { - Literal { value: Value, span: Span }, - Var { name: String, span: Span }, - Binary { left: Box, op: String, right: Box, span: Span }, - Unary { op: String, expr: Box, span: Span }, - Call { callee: Box, args: Vec, span: Span }, - Member { object: Box, name: String, span: Span }, - NamespaceMember { object: Box, name: String, span: Span }, - Index { object: Box, index: Box, span: Span }, - Array { items: Vec, span: Span }, - Tuple { items: Vec, span: Span }, - Dict { items: Vec<(Expr, Expr)>, span: Span }, + Literal { + value: Value, + span: Span, + }, + Var { + name: String, + span: Span, + }, + Binary { + left: Box, + op: String, + right: Box, + span: Span, + }, + Unary { + op: String, + expr: Box, + span: Span, + }, + Call { + callee: Box, + args: Vec, + span: Span, + }, + Member { + object: Box, + name: String, + span: Span, + }, + NamespaceMember { + object: Box, + name: String, + span: Span, + }, + Index { + object: Box, + index: Box, + span: Span, + }, + Array { + items: Vec, + span: Span, + }, + Tuple { + items: Vec, + span: Span, + }, + Dict { + items: Vec<(Expr, Expr)>, + span: Span, + }, ArrayComprehension { pattern: Pattern, iterable: Box, @@ -108,17 +227,61 @@ pub enum Expr { map_expr: Box, span: Span, }, - Range { start: Box, end: Box, span: Span }, - IfExpr { cond: Box, then_branch: Vec, else_branch: Vec, span: Span }, - Match { subject: Box, arms: Vec, span: Span }, - ParallelFor { pattern: Pattern, iterable: Box, body: Vec, span: Span }, - TaskBlock { body: Vec, span: Span }, - TaskCall { callee: Box, args: Vec, span: Span }, - Await { expr: Box, safe: bool, span: Span }, - Lambda { params: Vec, return_type: Option, body: Vec, span: Span }, - InterpolatedString { parts: Vec, span: Span }, - Sh { command: Box, span: Span }, - Ssh { host: Box, command: Box, span: Span }, + Range { + start: Box, + end: Box, + span: Span, + }, + IfExpr { + cond: Box, + then_branch: Vec, + else_branch: Vec, + span: Span, + }, + Match { + subject: Box, + arms: Vec, + span: Span, + }, + ParallelFor { + pattern: Pattern, + iterable: Box, + body: Vec, + span: Span, + }, + TaskBlock { + body: Vec, + span: Span, + }, + TaskCall { + callee: Box, + args: Vec, + span: Span, + }, + Await { + expr: Box, + safe: bool, + span: Span, + }, + Lambda { + params: Vec, + return_type: Option, + body: Vec, + span: Span, + }, + InterpolatedString { + parts: Vec, + span: Span, + }, + Sh { + command: Box, + span: Span, + }, + Ssh { + host: Box, + command: Box, + span: Span, + }, } #[derive(Debug, Clone)] diff --git a/src/parser/parser.rs b/src/parser/parser.rs index c2dfc5d..dda69b3 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -1,6 +1,9 @@ +use super::ast::{ + CallArg, Expr, ImportItem, ImportSource, InterpPart, MatchArm, MatchArmKind, ParamSpec, + Pattern, Program, RecordField, Span, Stmt, +}; use crate::lexer::token::{Token, TokenKind}; use crate::runtime::value::Value; -use super::ast::{CallArg, Expr, ImportItem, ImportSource, InterpPart, MatchArm, MatchArmKind, ParamSpec, Pattern, Program, RecordField, Span, Stmt}; pub struct Parser<'a> { tokens: Vec, @@ -11,13 +14,20 @@ pub struct Parser<'a> { impl<'a> Parser<'a> { pub fn new(tokens: Vec, source: &'a str, filename: String) -> Self { - Parser { tokens, current: 0, source, filename } + Parser { + tokens, + current: 0, + source, + filename, + } } pub fn parse_program(&mut self) -> Result { let mut statements = Vec::new(); while !self.is_at_end() { - if self.match_kind(&TokenKind::Semicolon) { continue; } + if self.match_kind(&TokenKind::Semicolon) { + continue; + } statements.push(self.parse_stmt()?); } Ok(Program { statements }) @@ -27,14 +37,21 @@ impl<'a> Parser<'a> { if self.match_kind(&TokenKind::Let) { let span = self.prev_span(); // Destructuring if next tokens indicate a pattern - if self.check(&TokenKind::LParen) || self.check(&TokenKind::Underscore) || self.check(&TokenKind::Identifier(String::new())) { + if self.check(&TokenKind::LParen) + || self.check(&TokenKind::Underscore) + || self.check(&TokenKind::Identifier(String::new())) + { // Peek for comma to distinguish simple let let is_destructure = self.lookahead_has_comma_before_equal(); if is_destructure { let pattern = self.parse_pattern_list()?; self.consume(&TokenKind::Equal, "Expected '=' after pattern")?; let expr = self.parse_expression()?; - return Ok(Stmt::LetDestructure { pattern, expr, span }); + return Ok(Stmt::LetDestructure { + pattern, + expr, + span, + }); } } let name = self.consume_identifier("Expected variable name after 'let'")?; @@ -66,9 +83,15 @@ impl<'a> Parser<'a> { let value = if values.len() == 1 { values.remove(0) } else { - Expr::Tuple { items: values, span } + Expr::Tuple { + items: values, + span, + } }; - return Ok(Stmt::Return { value: Some(value), span }); + return Ok(Stmt::Return { + value: Some(value), + span, + }); } if self.match_kind(&TokenKind::Yield) { let span = self.prev_span(); @@ -155,12 +178,20 @@ impl<'a> Parser<'a> { if self.match_kind(&TokenKind::Log) { let span = self.prev_span(); let expr = self.parse_expression()?; - return Ok(Stmt::Invoke { name: "log".to_string(), expr, span }); + return Ok(Stmt::Invoke { + name: "log".to_string(), + expr, + span, + }); } if self.match_kind(&TokenKind::Echo) { let span = self.prev_span(); let expr = self.parse_expression()?; - return Ok(Stmt::Invoke { name: "echo".to_string(), expr, span }); + return Ok(Stmt::Invoke { + name: "echo".to_string(), + expr, + span, + }); } // assignment or expression @@ -171,14 +202,22 @@ impl<'a> Parser<'a> { let span = assignable.span(); return match assignable { Expr::Var { name, .. } => Ok(Stmt::Assign { name, expr, span }), - Expr::Index { object, index, .. } => Ok(Stmt::IndexAssign { target: *object, index: *index, expr, span }), + Expr::Index { object, index, .. } => Ok(Stmt::IndexAssign { + target: *object, + index: *index, + expr, + span, + }), _ => Err(self.error_at_current("Invalid assignment target")), }; } } self.current = saved; let expr = self.parse_expression()?; - Ok(Stmt::Expr { expr: expr.clone(), span: expr.span() }) + Ok(Stmt::Expr { + expr: expr.clone(), + span: expr.span(), + }) } fn parse_function_stmt(&mut self) -> Result { @@ -256,7 +295,12 @@ impl<'a> Parser<'a> { } else { Vec::new() }; - Ok(Stmt::If { cond, then_branch, else_branch, span }) + Ok(Stmt::If { + cond, + then_branch, + else_branch, + span, + }) } fn parse_for_stmt(&mut self) -> Result { @@ -268,7 +312,12 @@ impl<'a> Parser<'a> { } let iterable = self.parse_expression()?; let body = self.parse_block()?; - Ok(Stmt::For { pattern, iterable, body, span }) + Ok(Stmt::For { + pattern, + iterable, + body, + span, + }) } fn parse_parallel_for_stmt(&mut self) -> Result { @@ -281,7 +330,12 @@ impl<'a> Parser<'a> { } let iterable = self.parse_expression()?; let body = self.parse_block()?; - Ok(Stmt::ParallelFor { pattern, iterable, body, span }) + Ok(Stmt::ParallelFor { + pattern, + iterable, + body, + span, + }) } fn parse_parallel_for_expr(&mut self) -> Result { @@ -294,7 +348,12 @@ impl<'a> Parser<'a> { } let iterable = self.parse_expression()?; let body = self.parse_block()?; - Ok(Expr::ParallelFor { pattern, iterable: Box::new(iterable), body, span }) + Ok(Expr::ParallelFor { + pattern, + iterable: Box::new(iterable), + body, + span, + }) } fn parse_while_stmt(&mut self) -> Result { @@ -312,7 +371,11 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Stmt::Use { module, alias, span }) + Ok(Stmt::Use { + module, + alias, + span, + }) } fn parse_import_stmt(&mut self) -> Result { @@ -322,14 +385,22 @@ impl<'a> Parser<'a> { let alias = self.consume_identifier("Expected alias name after 'as'")?; self.consume(&TokenKind::From, "Expected 'from' in import statement")?; let source = self.parse_import_source()?; - return Ok(Stmt::ImportNamespace { alias, source, span }); + return Ok(Stmt::ImportNamespace { + alias, + source, + span, + }); } if self.match_kind(&TokenKind::LBrace) { let items = self.parse_import_items()?; self.consume(&TokenKind::From, "Expected 'from' in import statement")?; let source = self.parse_import_source()?; - return Ok(Stmt::ImportNamed { items, source, span }); + return Ok(Stmt::ImportNamed { + items, + source, + span, + }); } let name = self.consume_identifier("Expected imported symbol after 'import'")?; @@ -392,7 +463,12 @@ impl<'a> Parser<'a> { let op = self.previous_lexeme(); let right = self.parse_and()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } @@ -403,7 +479,12 @@ impl<'a> Parser<'a> { let op = self.previous_lexeme(); let right = self.parse_equality()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } @@ -414,18 +495,33 @@ impl<'a> Parser<'a> { let op = self.previous_lexeme(); let right = self.parse_comparison()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } fn parse_comparison(&mut self) -> Result { let mut expr = self.parse_term()?; - while self.match_any(&[TokenKind::Less, TokenKind::LessEqual, TokenKind::Greater, TokenKind::GreaterEqual]) { + while self.match_any(&[ + TokenKind::Less, + TokenKind::LessEqual, + TokenKind::Greater, + TokenKind::GreaterEqual, + ]) { let op = self.previous_lexeme(); let right = self.parse_term()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } @@ -436,7 +532,12 @@ impl<'a> Parser<'a> { let op = self.previous_lexeme(); let right = self.parse_factor()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } @@ -447,7 +548,12 @@ impl<'a> Parser<'a> { let op = self.previous_lexeme(); let right = self.parse_unary()?; let span = expr.span(); - expr = Expr::Binary { left: Box::new(expr), op, right: Box::new(right), span }; + expr = Expr::Binary { + left: Box::new(expr), + op, + right: Box::new(right), + span, + }; } Ok(expr) } @@ -457,13 +563,21 @@ impl<'a> Parser<'a> { let span = self.prev_span(); let safe = self.match_kind(&TokenKind::Question); let expr = self.parse_unary()?; - return Ok(Expr::Await { expr: Box::new(expr), safe, span }); + return Ok(Expr::Await { + expr: Box::new(expr), + safe, + span, + }); } if self.match_any(&[TokenKind::Bang, TokenKind::Minus]) { let op = self.previous_lexeme(); let span = self.prev_span(); let expr = self.parse_unary()?; - return Ok(Expr::Unary { op, expr: Box::new(expr), span }); + return Ok(Expr::Unary { + op, + expr: Box::new(expr), + span, + }); } self.parse_postfix() } @@ -486,33 +600,53 @@ impl<'a> Parser<'a> { if self.match_kind(&TokenKind::Dot) { let span = self.prev_span(); let name = self.consume_identifier("Expected member name after '.'")?; - expr = Expr::Member { object: Box::new(expr), name, span }; + expr = Expr::Member { + object: Box::new(expr), + name, + span, + }; continue; } if self.match_kind(&TokenKind::ColonColon) { let span = self.prev_span(); let name = self.consume_identifier("Expected namespace member after '::'")?; - expr = Expr::NamespaceMember { object: Box::new(expr), name, span }; + expr = Expr::NamespaceMember { + object: Box::new(expr), + name, + span, + }; continue; } if self.match_kind(&TokenKind::LParen) { let span = self.prev_span(); let args = self.parse_arguments()?; self.consume(&TokenKind::RParen, "Expected ')' after arguments")?; - expr = Expr::Call { callee: Box::new(expr), args, span }; + expr = Expr::Call { + callee: Box::new(expr), + args, + span, + }; continue; } if self.match_kind(&TokenKind::LBracket) { let span = self.prev_span(); let index = self.parse_expression()?; self.consume(&TokenKind::RBracket, "Expected ']' after index")?; - expr = Expr::Index { object: Box::new(expr), index: Box::new(index), span }; + expr = Expr::Index { + object: Box::new(expr), + index: Box::new(index), + span, + }; continue; } if self.match_kind(&TokenKind::DotDot) { let span = self.prev_span(); let end = self.parse_expression()?; - expr = Expr::Range { start: Box::new(expr), end: Box::new(end), span }; + expr = Expr::Range { + start: Box::new(expr), + end: Box::new(end), + span, + }; continue; } break; @@ -536,7 +670,10 @@ impl<'a> Parser<'a> { } return Err(self.error_at_current("Expected ',' or '}' in record constructor")); } - self.consume(&TokenKind::RBrace, "Expected '}' after record constructor fields")?; + self.consume( + &TokenKind::RBrace, + "Expected '}' after record constructor fields", + )?; Ok(args) } @@ -561,29 +698,46 @@ impl<'a> Parser<'a> { fn parse_primary(&mut self) -> Result { if self.match_kind(&TokenKind::True) { let span = self.prev_span(); - return Ok(Expr::Literal { value: Value::Bool(true), span }); + return Ok(Expr::Literal { + value: Value::Bool(true), + span, + }); } if self.match_kind(&TokenKind::False) { let span = self.prev_span(); - return Ok(Expr::Literal { value: Value::Bool(false), span }); + return Ok(Expr::Literal { + value: Value::Bool(false), + span, + }); } if self.match_kind(&TokenKind::Number(String::new())) { if let TokenKind::Number(text) = self.previous().kind.clone() { let span = self.prev_span(); - let val: f64 = text.parse().map_err(|_| self.error_at_current("Invalid number"))?; - return Ok(Expr::Literal { value: Value::Number(val), span }); + let val: f64 = text + .parse() + .map_err(|_| self.error_at_current("Invalid number"))?; + return Ok(Expr::Literal { + value: Value::Number(val), + span, + }); } } if self.match_kind(&TokenKind::TimeLiteral(String::new())) { if let TokenKind::TimeLiteral(text) = self.previous().kind.clone() { let span = self.prev_span(); - return Ok(Expr::Literal { value: Value::Duration(Value::parse_duration(&text)?), span }); + return Ok(Expr::Literal { + value: Value::Duration(Value::parse_duration(&text)?), + span, + }); } } if self.match_kind(&TokenKind::String(String::new())) { if let TokenKind::String(text) = self.previous().kind.clone() { let span = self.prev_span(); - return Ok(Expr::Literal { value: Value::String(text), span }); + return Ok(Expr::Literal { + value: Value::String(text), + span, + }); } } if self.match_kind(&TokenKind::InterpolatedString(String::new())) { @@ -609,8 +763,17 @@ impl<'a> Parser<'a> { let span = self.prev_span(); let cond = self.parse_expression()?; let then_branch = self.parse_block()?; - let else_branch = if self.match_kind(&TokenKind::Else) { self.parse_block()? } else { Vec::new() }; - return Ok(Expr::IfExpr { cond: Box::new(cond), then_branch, else_branch, span }); + let else_branch = if self.match_kind(&TokenKind::Else) { + self.parse_block()? + } else { + Vec::new() + }; + return Ok(Expr::IfExpr { + cond: Box::new(cond), + then_branch, + else_branch, + span, + }); } if self.match_kind(&TokenKind::Match) { return self.parse_match_expr(); @@ -628,18 +791,28 @@ impl<'a> Parser<'a> { let mut args = self.parse_arguments()?; self.consume(&TokenKind::RParen, "Expected ')' after task arguments")?; if args.is_empty() { - return Err(self.error_at_current("task(...) expects callable as first argument")); + return Err( + self.error_at_current("task(...) expects callable as first argument") + ); } let callee = match args.remove(0) { CallArg::Positional(expr) => expr, CallArg::Spread(_) => { - return Err(self.error_at_current("task(...) expects callable as first positional argument")); + return Err(self.error_at_current( + "task(...) expects callable as first positional argument", + )); } CallArg::Named { .. } => { - return Err(self.error_at_current("task(...) expects callable as first positional argument")); + return Err(self.error_at_current( + "task(...) expects callable as first positional argument", + )); } }; - return Ok(Expr::TaskCall { callee: Box::new(callee), args, span }); + return Ok(Expr::TaskCall { + callee: Box::new(callee), + args, + span, + }); } return Err(self.error_at_current("Expected '{' or '(' after 'task'")); } @@ -654,7 +827,12 @@ impl<'a> Parser<'a> { None }; let body = self.parse_block()?; - return Ok(Expr::Lambda { params, return_type, body, span }); + return Ok(Expr::Lambda { + params, + return_type, + body, + span, + }); } if self.match_kind(&TokenKind::Sh) { let span = self.prev_span(); @@ -663,7 +841,10 @@ impl<'a> Parser<'a> { } else { self.parse_expression()? }; - return Ok(Expr::Sh { command: Box::new(command), span }); + return Ok(Expr::Sh { + command: Box::new(command), + span, + }); } if self.match_kind(&TokenKind::Ssh) { let span = self.prev_span(); @@ -673,7 +854,11 @@ impl<'a> Parser<'a> { } else { self.parse_expression()? }; - return Ok(Expr::Ssh { host: Box::new(host), command: Box::new(command), span }); + return Ok(Expr::Ssh { + host: Box::new(host), + command: Box::new(command), + span, + }); } if self.match_kind(&TokenKind::LParen) { let span = self.prev_span(); @@ -699,7 +884,10 @@ impl<'a> Parser<'a> { } let elements = self.parse_elements()?; self.consume(&TokenKind::RBracket, "Expected ']' after array")?; - return Ok(Expr::Array { items: elements, span }); + return Ok(Expr::Array { + items: elements, + span, + }); } if self.match_kind(&TokenKind::LBrace) { let span = self.prev_span(); @@ -769,7 +957,11 @@ impl<'a> Parser<'a> { )); } - Ok(Expr::Match { subject: Box::new(subject), arms, span }) + Ok(Expr::Match { + subject: Box::new(subject), + arms, + span, + }) } fn parse_array_comprehension(&mut self, span: Span) -> Result { @@ -786,7 +978,10 @@ impl<'a> Parser<'a> { }; self.consume(&TokenKind::FatArrow, "Expected '=>' in array comprehension")?; let map_expr = self.parse_expression()?; - self.consume(&TokenKind::RBracket, "Expected ']' after array comprehension")?; + self.consume( + &TokenKind::RBracket, + "Expected ']' after array comprehension", + )?; Ok(Expr::ArrayComprehension { pattern, iterable: Box::new(iterable), @@ -801,7 +996,10 @@ impl<'a> Parser<'a> { return self.parse_block(); } let expr = self.parse_expression()?; - Ok(vec![Stmt::Expr { expr: expr.clone(), span: expr.span() }]) + Ok(vec![Stmt::Expr { + expr: expr.clone(), + span: expr.span(), + }]) } fn parse_param_list(&mut self) -> Result, String> { @@ -812,7 +1010,9 @@ impl<'a> Parser<'a> { loop { let variadic = self.match_kind(&TokenKind::Ellipsis); if saw_variadic { - return Err(self.error_at_current("Variadic parameter must be the final parameter")); + return Err( + self.error_at_current("Variadic parameter must be the final parameter") + ); } let name = self.consume_identifier("Expected parameter name")?; let ty = if self.match_kind(&TokenKind::Colon) { @@ -822,7 +1022,9 @@ impl<'a> Parser<'a> { }; let default = if self.match_kind(&TokenKind::Equal) { if variadic { - return Err(self.error_at_current("Variadic parameter cannot have a default value")); + return Err( + self.error_at_current("Variadic parameter cannot have a default value") + ); } saw_default = true; Some(self.parse_expression()?) @@ -833,10 +1035,19 @@ impl<'a> Parser<'a> { saw_variadic = true; } if saw_default && default.is_none() { - return Err(self.error_at_current("Required parameters cannot follow parameters with defaults")); + return Err(self.error_at_current( + "Required parameters cannot follow parameters with defaults", + )); + } + params.push(ParamSpec { + name, + ty, + default, + variadic, + }); + if !self.match_kind(&TokenKind::Comma) { + break; } - params.push(ParamSpec { name, ty, default, variadic }); - if !self.match_kind(&TokenKind::Comma) { break; } } } Ok(params) @@ -855,7 +1066,9 @@ impl<'a> Parser<'a> { args.push(CallArg::Named { name, value }); } else { if saw_named { - return Err(self.error_at_current("Positional arguments cannot appear after named arguments")); + return Err(self.error_at_current( + "Positional arguments cannot appear after named arguments", + )); } if self.match_kind(&TokenKind::Ellipsis) { let spread_expr = self.parse_expression()?; @@ -864,7 +1077,9 @@ impl<'a> Parser<'a> { args.push(CallArg::Positional(self.parse_expression()?)); } } - if !self.match_kind(&TokenKind::Comma) { break; } + if !self.match_kind(&TokenKind::Comma) { + break; + } } } Ok(args) @@ -875,7 +1090,9 @@ impl<'a> Parser<'a> { if !self.check(&TokenKind::RBracket) { loop { elements.push(self.parse_expression()?); - if !self.match_kind(&TokenKind::Comma) { break; } + if !self.match_kind(&TokenKind::Comma) { + break; + } } } Ok(elements) @@ -889,23 +1106,32 @@ impl<'a> Parser<'a> { self.consume(&TokenKind::Colon, "Expected ':' in dict")?; let value = self.parse_expression()?; pairs.push((key, value)); - if !self.match_kind(&TokenKind::Comma) { break; } + if !self.match_kind(&TokenKind::Comma) { + break; + } } } Ok(pairs) } fn parse_dict_key(&mut self) -> Result { - if self.check(&TokenKind::Identifier(String::new())) && matches!(self.peek_next().kind, TokenKind::Colon) { + if self.check(&TokenKind::Identifier(String::new())) + && matches!(self.peek_next().kind, TokenKind::Colon) + { let key = self.consume_identifier("Expected dict key")?; let span = self.prev_span(); - return Ok(Expr::Literal { value: Value::String(key), span }); + return Ok(Expr::Literal { + value: Value::String(key), + span, + }); } self.parse_expression() } fn is_dict_start(&self) -> bool { - if self.check(&TokenKind::RBrace) { return true; } + if self.check(&TokenKind::RBrace) { + return true; + } match &self.peek().kind { TokenKind::Identifier(_) | TokenKind::String(_) => { matches!(self.peek_next().kind, TokenKind::Colon) @@ -931,8 +1157,11 @@ impl<'a> Parser<'a> { let bytes = self.source.as_bytes(); while pos < bytes.len() && depth > 0 { let c = bytes[pos] as char; - if c == '{' { depth += 1; } - else if c == '}' { depth -= 1; } + if c == '{' { + depth += 1; + } else if c == '}' { + depth -= 1; + } pos += 1; } if depth != 0 { @@ -945,9 +1174,15 @@ impl<'a> Parser<'a> { if raw.contains("#{") { let parts = self.parse_interpolated_string(raw)?; - Ok(Expr::InterpolatedString { parts, span: self.span_from_token(&open) }) + Ok(Expr::InterpolatedString { + parts, + span: self.span_from_token(&open), + }) } else { - Ok(Expr::Literal { value: Value::String(raw.trim().to_string()), span: self.span_from_token(&open) }) + Ok(Expr::Literal { + value: Value::String(raw.trim().to_string()), + span: self.span_from_token(&open), + }) } } @@ -964,8 +1199,12 @@ impl<'a> Parser<'a> { let mut depth = 1; while j < raw.len() && depth > 0 { let ch = raw.as_bytes()[j] as char; - if ch == '{' { depth += 1; } - if ch == '}' { depth -= 1; } + if ch == '{' { + depth += 1; + } + if ch == '}' { + depth -= 1; + } j += 1; } if depth != 0 { @@ -992,7 +1231,12 @@ impl<'a> Parser<'a> { self.consume(&TokenKind::Arrow, "Expected '->' after parameter")?; let body = self.parse_lambda_body()?; Ok(Expr::Lambda { - params: vec![ParamSpec { name: param, ty: None, default: None, variadic: false }], + params: vec![ParamSpec { + name: param, + ty: None, + default: None, + variadic: false, + }], return_type: None, body, span, @@ -1006,7 +1250,12 @@ impl<'a> Parser<'a> { self.consume(&TokenKind::RParen, "Expected ')' after lambda params")?; self.consume(&TokenKind::Arrow, "Expected '->' after lambda params")?; let body = self.parse_lambda_body()?; - Ok(Expr::Lambda { params, return_type: None, body, span }) + Ok(Expr::Lambda { + params, + return_type: None, + body, + span, + }) } fn parse_lambda_body(&mut self) -> Result, String> { @@ -1014,7 +1263,10 @@ impl<'a> Parser<'a> { return self.parse_block(); } let expr = self.parse_expression()?; - Ok(vec![Stmt::Return { value: Some(expr.clone()), span: expr.span() }]) + Ok(vec![Stmt::Return { + value: Some(expr.clone()), + span: expr.span(), + }]) } fn parse_type(&mut self) -> Result { @@ -1073,7 +1325,9 @@ impl<'a> Parser<'a> { if !self.check(&TokenKind::Greater) { loop { args.push(self.parse_type()?); - if !self.match_kind(&TokenKind::Comma) { break; } + if !self.match_kind(&TokenKind::Comma) { + break; + } } } self.consume(&TokenKind::Greater, "Expected '>' after fn")?; @@ -1106,7 +1360,9 @@ impl<'a> Parser<'a> { if !self.check(&TokenKind::RParen) { loop { parts.push(self.parse_pattern()?); - if !self.match_kind(&TokenKind::Comma) { break; } + if !self.match_kind(&TokenKind::Comma) { + break; + } } } self.consume(&TokenKind::RParen, "Expected ')' after pattern")?; @@ -1140,13 +1396,19 @@ impl<'a> Parser<'a> { let index = self.parse_expression()?; self.consume(&TokenKind::RBracket, "Expected ']' after index")?; let span = e.span(); - e = Expr::Index { object: Box::new(e), index: Box::new(index), span }; + e = Expr::Index { + object: Box::new(e), + index: Box::new(index), + span, + }; } Ok(e) } fn consume(&mut self, kind: &TokenKind, message: &str) -> Result { - if self.check(kind) { return Ok(self.advance().clone()); } + if self.check(kind) { + return Ok(self.advance().clone()); + } Err(self.error_at_current(message)) } @@ -1159,23 +1421,35 @@ impl<'a> Parser<'a> { } fn match_kind(&mut self, kind: &TokenKind) -> bool { - if self.check(kind) { self.advance(); true } else { false } + if self.check(kind) { + self.advance(); + true + } else { + false + } } fn match_any(&mut self, kinds: &[TokenKind]) -> bool { for k in kinds { - if self.check(k) { self.advance(); return true; } + if self.check(k) { + self.advance(); + return true; + } } false } fn check(&self, kind: &TokenKind) -> bool { - if self.is_at_end() { return false; } + if self.is_at_end() { + return false; + } std::mem::discriminant(&self.peek().kind) == std::mem::discriminant(kind) } fn advance(&mut self) -> &Token { - if !self.is_at_end() { self.current += 1; } + if !self.is_at_end() { + self.current += 1; + } self.previous() } @@ -1188,7 +1462,11 @@ impl<'a> Parser<'a> { } fn peek_next(&self) -> &Token { - if self.current + 1 >= self.tokens.len() { self.peek() } else { &self.tokens[self.current + 1] } + if self.current + 1 >= self.tokens.len() { + self.peek() + } else { + &self.tokens[self.current + 1] + } } fn previous(&self) -> &Token { @@ -1233,7 +1511,10 @@ impl<'a> Parser<'a> { } fn span_from_token(&self, tok: &Token) -> Span { - Span { line: tok.line, col: tok.column } + Span { + line: tok.line, + col: tok.column, + } } fn peek_next_is_arrow(&self) -> bool { @@ -1270,7 +1551,9 @@ impl<'a> Parser<'a> { fn parse_expression_from_str(expr_text: &str, filename: &str) -> Result { let mut scanner = crate::lexer::scanner::Scanner::new(expr_text); - let tokens = scanner.scan_tokens().map_err(|e| format!("{filename}: {e}"))?; + let tokens = scanner + .scan_tokens() + .map_err(|e| format!("{filename}: {e}"))?; let mut parser = Parser::new(tokens, expr_text, filename.to_string()); parser.parse_expression() } @@ -1317,10 +1600,11 @@ fn stmt_contains_yield(stmt: &Stmt) -> bool { | Stmt::Throw { value: expr, .. } | Stmt::Invoke { expr, .. } => expr_contains_yield(expr), Stmt::IndexAssign { - target, index, expr, .. - } => { - expr_contains_yield(target) || expr_contains_yield(index) || expr_contains_yield(expr) - } + target, + index, + expr, + .. + } => expr_contains_yield(target) || expr_contains_yield(index) || expr_contains_yield(expr), Stmt::Return { value: None, .. } | Stmt::Continue { .. } | Stmt::Break { .. } @@ -1344,8 +1628,12 @@ fn expr_contains_yield(expr: &Expr) -> bool { CallArg::Named { value, .. } => expr_contains_yield(value), }) } - Expr::Member { object, .. } | Expr::NamespaceMember { object, .. } => expr_contains_yield(object), - Expr::Index { object, index, .. } => expr_contains_yield(object) || expr_contains_yield(index), + Expr::Member { object, .. } | Expr::NamespaceMember { object, .. } => { + expr_contains_yield(object) + } + Expr::Index { object, index, .. } => { + expr_contains_yield(object) || expr_contains_yield(index) + } Expr::Array { items, .. } | Expr::Tuple { items, .. } => { items.iter().any(expr_contains_yield) } @@ -1371,16 +1659,22 @@ fn expr_contains_yield(expr: &Expr) -> bool { then_branch, else_branch, .. - } => expr_contains_yield(cond) || contains_yield(then_branch) || contains_yield(else_branch), + } => { + expr_contains_yield(cond) || contains_yield(then_branch) || contains_yield(else_branch) + } Expr::Match { subject, arms, .. } => { expr_contains_yield(subject) || arms.iter().any(|arm| match &arm.kind { MatchArmKind::Value(e) => expr_contains_yield(e) || contains_yield(&arm.body), - MatchArmKind::Compare { rhs, .. } => expr_contains_yield(rhs) || contains_yield(&arm.body), + MatchArmKind::Compare { rhs, .. } => { + expr_contains_yield(rhs) || contains_yield(&arm.body) + } MatchArmKind::Wildcard => contains_yield(&arm.body), }) } - Expr::ParallelFor { iterable, body, .. } => expr_contains_yield(iterable) || contains_yield(body), + Expr::ParallelFor { iterable, body, .. } => { + expr_contains_yield(iterable) || contains_yield(body) + } Expr::TaskBlock { .. } => false, Expr::TaskCall { callee, args, .. } => { expr_contains_yield(callee) @@ -1396,7 +1690,9 @@ fn expr_contains_yield(expr: &Expr) -> bool { InterpPart::Expr(e) => expr_contains_yield(e), }), Expr::Sh { command, .. } => expr_contains_yield(command), - Expr::Ssh { host, command, .. } => expr_contains_yield(host) || expr_contains_yield(command), + Expr::Ssh { host, command, .. } => { + expr_contains_yield(host) || expr_contains_yield(command) + } Expr::Literal { .. } | Expr::Var { .. } => false, } } @@ -1419,3 +1715,92 @@ fn shorten(s: &str) -> String { s.to_string() } } + +#[cfg(test)] +mod tests { + use super::Parser; + use crate::lexer::scanner::Scanner; + use crate::parser::ast::Stmt; + + fn parse_program(source: &str) -> Result { + let mut scanner = Scanner::new(source); + let tokens = scanner.scan_tokens()?; + let mut parser = Parser::new(tokens, source, "".to_string()); + parser.parse_program() + } + + fn parse_program_ok(source: &str) -> crate::parser::ast::Program { + parse_program(source).unwrap() + } + + #[test] + fn parse_let() { + let program = parse_program_ok("let x = 1"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::Let { name, .. } if name == "x")); + } + + #[test] + fn parse_fn() { + let program = parse_program_ok("fn add(a, b) { return a + b }"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::Function { name, .. } if name == "add")); + } + + #[test] + fn parse_expr_stmt() { + // "log 42" is parsed as Stmt::Invoke (log is a keyword), not a bare Expr + let program = parse_program_ok("log 42"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::Invoke { name, .. } if name == "log")); + // A bare expression like "1 + 2" becomes Stmt::Expr + let program2 = parse_program_ok("1 + 2"); + assert_eq!(program2.statements.len(), 1); + assert!(matches!(&program2.statements[0], Stmt::Expr { .. })); + } + + #[test] + fn parse_multiple_stmts() { + let program = parse_program_ok("let a = 1; let b = 2"); + assert_eq!(program.statements.len(), 2); + } + + #[test] + fn parse_if_stmt() { + let program = parse_program_ok("if true { log 1 }"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::If { .. })); + } + + #[test] + fn parse_for_stmt() { + let program = parse_program_ok("for x in [1,2,3] { log x }"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::For { .. })); + } + + #[test] + fn parse_while_stmt() { + let program = parse_program_ok("while false { log 0 }"); + assert_eq!(program.statements.len(), 1); + assert!(matches!(&program.statements[0], Stmt::While { .. })); + } + + #[test] + fn parse_array_and_dict_expr() { + let program = parse_program_ok("let a = [1, 2]; let b = { \"k\": 1 }"); + assert_eq!(program.statements.len(), 2); + } + + #[test] + fn parse_lambda() { + let program = parse_program_ok("let f = x -> x + 1"); + assert_eq!(program.statements.len(), 1); + } + + #[test] + fn parse_invalid_fails() { + assert!(parse_program("let x = ").is_err()); + assert!(parse_program("fn add( ").is_err()); + } +} diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 2821081..a65c997 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -6,13 +6,15 @@ use std::time::{Duration, Instant}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::execute; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::terminal::{ + EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, +}; +use ratatui::Terminal; use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::style::{Color, Style}; use ratatui::text::Line; use ratatui::widgets::{Paragraph, Wrap}; -use ratatui::Terminal; use crate::lexer::scanner::Scanner; use crate::parser::parser::Parser; @@ -40,9 +42,11 @@ pub fn run() -> Result<(), String> { enable_raw_mode().map_err(|e| format!("Failed to enable raw mode: {e}"))?; let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen).map_err(|e| format!("Failed to enter alt screen: {e}"))?; + execute!(stdout, EnterAlternateScreen) + .map_err(|e| format!("Failed to enter alt screen: {e}"))?; let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend).map_err(|e| format!("Failed to create terminal: {e}"))?; + let mut terminal = + Terminal::new(backend).map_err(|e| format!("Failed to create terminal: {e}"))?; let run_result = tui_loop(&mut terminal, &mut app); @@ -108,7 +112,9 @@ impl ReplApp { output_exec_hint: None, }; app.push_info("v2r repl"); - app.push_info("Notebook mode: Enter newline | Ctrl+R / Ctrl+Enter / F5 run cell | Ctrl+D quit"); + app.push_info( + "Notebook mode: Enter newline | Ctrl+R / Ctrl+Enter / F5 run cell | Ctrl+D quit", + ); app } @@ -120,7 +126,12 @@ impl ReplApp { self.push_entry_with_exec(kind, text, None); } - fn push_entry_with_exec(&mut self, kind: EntryKind, text: impl Into, exec_id: Option) { + fn push_entry_with_exec( + &mut self, + kind: EntryKind, + text: impl Into, + exec_id: Option, + ) { self.entries.push(ReplEntry { kind, text: text.into(), @@ -178,7 +189,13 @@ impl ReplApp { self.output_exec_hint = None; } - fn eval_source(&mut self, filename: String, source: String, auto_print_result: bool, exec_id: u64) { + fn eval_source( + &mut self, + filename: String, + source: String, + auto_print_result: bool, + exec_id: u64, + ) { let start = Instant::now(); self.executor.set_context(filename.clone(), source.clone()); @@ -374,7 +391,10 @@ impl ReplApp { lines.push(Line::styled(format!("{prefix}{first}"), style)); } for cont in iter { - lines.push(Line::styled(format!("{}{}", " ".repeat(prefix.len()), cont), style)); + lines.push(Line::styled( + format!("{}{}", " ".repeat(prefix.len()), cont), + style, + )); } } let draft_prefix = format!("In [{}]: ", self.next_exec_id); @@ -521,14 +541,18 @@ fn tui_loop( continue; } match (key.code, key.modifiers) { - (KeyCode::Char('d'), m) if m.contains(KeyModifiers::CONTROL) => app.should_quit = true, + (KeyCode::Char('d'), m) if m.contains(KeyModifiers::CONTROL) => { + app.should_quit = true + } (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => { app.input.clear(); app.input_cursor = 0; } (KeyCode::Char('f'), m) if m.contains(KeyModifiers::CONTROL) => app.move_right(), (KeyCode::Char('b'), m) if m.contains(KeyModifiers::CONTROL) => app.move_left(), - (KeyCode::Char('a'), m) if m.contains(KeyModifiers::CONTROL) => app.move_line_start(), + (KeyCode::Char('a'), m) if m.contains(KeyModifiers::CONTROL) => { + app.move_line_start() + } (KeyCode::Char('e'), m) if m.contains(KeyModifiers::CONTROL) => app.move_line_end(), (KeyCode::Char('p'), m) if m.contains(KeyModifiers::CONTROL) => app.move_up(), (KeyCode::Char('n'), m) if m.contains(KeyModifiers::CONTROL) => app.move_down(), @@ -642,7 +666,10 @@ fn notebook_cursor_position(app: &ReplApp) -> (usize, u16) { let prefix = format!("In [{}]: ", app.next_exec_id); let (_, _, col, line_offset) = line_bounds_and_col(&app.input, app.input_cursor); - (line_idx + line_offset, (prefix.chars().count() + col) as u16) + ( + line_idx + line_offset, + (prefix.chars().count() + col) as u16, + ) } fn char_to_byte_idx(s: &str, char_idx: usize) -> usize { @@ -797,12 +824,16 @@ fn run_line_repl() -> Result<(), String> { let mut buffer = String::new(); println!("v2r repl (line mode)"); - println!("commands: :help :quit :clear :vars :load :ast :tokens :time :history export :history import "); + println!( + "commands: :help :quit :clear :vars :load :ast :tokens :time :history export :history import " + ); loop { let prompt = if buffer.is_empty() { "v2r> " } else { "...> " }; print!("{prompt}"); - io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {e}"))?; + io::stdout() + .flush() + .map_err(|e| format!("Failed to flush stdout: {e}"))?; let mut line = String::new(); let read = io::stdin() @@ -921,12 +952,16 @@ fn line_mode_command( match cmd { "help" => { - println!(":help :quit :clear :vars :load :ast :tokens :time :history export :history import "); + println!( + ":help :quit :clear :vars :load :ast :tokens :time :history export :history import " + ); } "quit" | "q" | "exit" => return Ok(true), "clear" => { print!("\x1B[2J\x1B[1;1H"); - io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {e}"))?; + io::stdout() + .flush() + .map_err(|e| format!("Failed to flush stdout: {e}"))?; } "vars" => { let vars = executor.list_globals(); @@ -943,7 +978,14 @@ fn line_mode_command( eprintln!("Usage: :load "); } else { match fs::read_to_string(arg) { - Ok(src) => line_mode_eval(executor, pending_output, arg.to_string(), src, false, *timing_enabled), + Ok(src) => line_mode_eval( + executor, + pending_output, + arg.to_string(), + src, + false, + *timing_enabled, + ), Err(err) => eprintln!("Failed to read {arg}: {err}"), } } diff --git a/src/runtime/builtins/core.rs b/src/runtime/builtins/core.rs index 9f934a9..921b16f 100644 --- a/src/runtime/builtins/core.rs +++ b/src/runtime/builtins/core.rs @@ -5,8 +5,8 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -use crate::runtime::executor::{Executor, RuntimeCallArg}; use crate::runtime::errors::RuntimeError; +use crate::runtime::executor::{Executor, RuntimeCallArg}; use crate::runtime::value::Value; pub fn register(exec: &mut Executor) { @@ -30,8 +30,8 @@ pub fn register(exec: &mut Executor) { Some(v) => v.as_string(), None => return Err("read_file expects path".to_string()), }; - let contents = fs::read_to_string(&path) - .map_err(|e| format!("Failed to read {path}: {e}"))?; + let contents = + fs::read_to_string(&path).map_err(|e| format!("Failed to read {path}: {e}"))?; Ok(Value::String(contents)) }); @@ -111,7 +111,8 @@ pub fn register(exec: &mut Executor) { let items = iter_values(iterable, exec, span)?; let mut out = Vec::new(); for item in items { - let result = exec.call_value(func.clone(), vec![RuntimeCallArg::Positional(item.clone())])?; + let result = + exec.call_value(func.clone(), vec![RuntimeCallArg::Positional(item.clone())])?; if result.is_truthy() { out.push(item); } @@ -121,7 +122,10 @@ pub fn register(exec: &mut Executor) { exec.register_native_exec("reduce", |exec, args, span| { if args.len() < 3 { - return Err(exec.make_error("reduce expects iterable, fn, initial (or iterable, initial, fn)", span)); + return Err(exec.make_error( + "reduce expects iterable, fn, initial (or iterable, initial, fn)", + span, + )); } let iterable = args[0].clone(); let (func, mut acc) = match (&args[1], &args[2]) { @@ -137,7 +141,10 @@ pub fn register(exec: &mut Executor) { for item in items { let result = exec.call_value( func.clone(), - vec![RuntimeCallArg::Positional(acc), RuntimeCallArg::Positional(item)], + vec![ + RuntimeCallArg::Positional(acc), + RuntimeCallArg::Positional(item), + ], )?; acc = result; } @@ -271,7 +278,9 @@ pub fn register(exec: &mut Executor) { if descending { decorated.reverse(); } - Ok(Value::array(decorated.into_iter().map(|(item, _)| item).collect())) + Ok(Value::array( + decorated.into_iter().map(|(item, _)| item).collect(), + )) }); exec.register_native_exec("groupBy", |exec, args, span| { @@ -342,7 +351,9 @@ pub fn register(exec: &mut Executor) { fn chrono_now() -> String { use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); let secs = now.as_secs() % 86400; let h = secs / 3600; let m = (secs % 3600) / 60; @@ -350,7 +361,11 @@ fn chrono_now() -> String { format!("{:02}:{:02}:{:02}", h, m, s) } -fn expect_two(mut args: Vec, exec: &Executor, span: crate::parser::ast::Span) -> Result<(Value, Value), RuntimeError> { +fn expect_two( + mut args: Vec, + exec: &Executor, + span: crate::parser::ast::Span, +) -> Result<(Value, Value), RuntimeError> { if args.len() < 2 { return Err(exec.make_error("Expected 2 arguments", span)); } @@ -359,7 +374,12 @@ fn expect_two(mut args: Vec, exec: &Executor, span: crate::parser::ast::S Ok((first, second)) } -fn expect_callable(value: &Value, exec: &Executor, span: crate::parser::ast::Span, name: &str) -> Result<(), RuntimeError> { +fn expect_callable( + value: &Value, + exec: &Executor, + span: crate::parser::ast::Span, + name: &str, +) -> Result<(), RuntimeError> { match value { Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) => Ok(()), _ => Err(exec.make_error(&format!("{name} expects a callable argument"), span)), @@ -370,14 +390,24 @@ fn call_unary(exec: &mut Executor, func: &Value, arg: Value) -> Result Result { +fn call_binary( + exec: &mut Executor, + func: &Value, + a: Value, + b: Value, +) -> Result { exec.call_value( func.clone(), vec![RuntimeCallArg::Positional(a), RuntimeCallArg::Positional(b)], ) } -fn expect_number(value: &Value, exec: &Executor, span: crate::parser::ast::Span, where_: &str) -> Result { +fn expect_number( + value: &Value, + exec: &Executor, + span: crate::parser::ast::Span, + where_: &str, +) -> Result { match value { Value::Number(n) => Ok(*n), _ => Err(exec.make_error(&format!("{where_} must return number"), span)), @@ -393,7 +423,12 @@ fn compare_values(a: &Value, b: &Value) -> Ordering { } } -fn as_hash_key(value: &Value, exec: &Executor, span: crate::parser::ast::Span, where_: &str) -> Result { +fn as_hash_key( + value: &Value, + exec: &Executor, + span: crate::parser::ast::Span, + where_: &str, +) -> Result { match value { Value::Null => Ok("null:null".to_string()), Value::Bool(b) => Ok(format!("bool:{b}")), @@ -426,7 +461,11 @@ where Ok(()) } -fn iter_values(value: Value, exec: &Executor, span: crate::parser::ast::Span) -> Result, RuntimeError> { +fn iter_values( + value: Value, + exec: &Executor, + span: crate::parser::ast::Span, +) -> Result, RuntimeError> { exec.collect_iterable(value, span) } @@ -445,7 +484,9 @@ fn value_type_label(value: &Value) -> String { Value::Task(_) => "task".to_string(), Value::Generator(_) => "generator".to_string(), Value::Module(_) => "module".to_string(), - Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) => "fn".to_string(), + Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) => { + "fn".to_string() + } Value::Ufcs { .. } => "ufcs".to_string(), } } diff --git a/src/runtime/builtins/net.rs b/src/runtime/builtins/net.rs index 90f9fcf..78ce2f0 100644 --- a/src/runtime/builtins/net.rs +++ b/src/runtime/builtins/net.rs @@ -23,7 +23,10 @@ pub fn register(exec: &mut Executor) { fn native_exec(f: F) -> Value where - F: Fn(&mut Executor, Vec, crate::parser::ast::Span) -> Result + Send + Sync + 'static, + F: Fn(&mut Executor, Vec, crate::parser::ast::Span) -> Result + + Send + + Sync + + 'static, { Value::NativeFunctionExec(Arc::new(f)) } @@ -47,7 +50,10 @@ fn expect_string(value: Option<&Value>, name: &str) -> Result { fn expect_number(value: Option<&Value>, name: &str) -> Result { match value { Some(Value::Number(n)) => Ok(*n), - Some(v) => v.as_string().parse::().map_err(|_| format!("Expected numeric {name}")), + Some(v) => v + .as_string() + .parse::() + .map_err(|_| format!("Expected numeric {name}")), None => Err(format!("Expected numeric {name}")), } } @@ -112,7 +118,11 @@ fn parse_ping_latency_ms(stdout: &str) -> Option { None } -fn net_ping(exec: &mut Executor, args: Vec, span: crate::parser::ast::Span) -> Result { +fn net_ping( + exec: &mut Executor, + args: Vec, + span: crate::parser::ast::Span, +) -> Result { let host = expect_string(args.get(0), "host").map_err(|e| err(exec, span, &e))?; let timeout = optional_timeout_ms(args.get(1)).map_err(|e| err(exec, span, &e))?; let timeout_secs = timeout.as_secs().max(1); @@ -128,8 +138,12 @@ fn net_ping(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa .output() .map_err(|e| err(exec, span, &format!("Failed to run ping: {e}")))?; let duration = start.elapsed(); - let stdout = String::from_utf8_lossy(&output.stdout).trim_end().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim_end().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout) + .trim_end() + .to_string(); + let stderr = String::from_utf8_lossy(&output.stderr) + .trim_end() + .to_string(); let exit_code = output.status.code().unwrap_or(-1); let mut out = HashMap::new(); @@ -143,13 +157,23 @@ fn net_ping(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa } out.insert("stdout".to_string(), Value::String(stdout)); out.insert("stderr".to_string(), Value::String(stderr)); - out.insert("duration_ms".to_string(), Value::Number(duration.as_millis() as f64)); + out.insert( + "duration_ms".to_string(), + Value::Number(duration.as_millis() as f64), + ); Ok(value_dict(out)) } -fn net_http(exec: &mut Executor, args: Vec, span: crate::parser::ast::Span) -> Result { +fn net_http( + exec: &mut Executor, + args: Vec, + span: crate::parser::ast::Span, +) -> Result { let req_map = match args.get(0) { - Some(Value::Dict(map)) => map.lock().map_err(|_| err(exec, span, "Request map lock poisoned"))?.clone(), + Some(Value::Dict(map)) => map + .lock() + .map_err(|_| err(exec, span, "Request map lock poisoned"))? + .clone(), _ => return Err(err(exec, span, "net.http expects a request dict")), }; @@ -159,7 +183,8 @@ fn net_http(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa .unwrap_or_else(|| "GET".to_string()); let method = reqwest::Method::from_bytes(method_text.as_bytes()) .map_err(|e| err(exec, span, &format!("Invalid HTTP method: {e}")))?; - let timeout = optional_timeout_ms(dict_get(&req_map, "timeout_ms")).map_err(|e| err(exec, span, &e))?; + let timeout = + optional_timeout_ms(dict_get(&req_map, "timeout_ms")).map_err(|e| err(exec, span, &e))?; let client = reqwest::blocking::Client::builder() .timeout(timeout) @@ -168,7 +193,9 @@ fn net_http(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa let mut request = client.request(method, &url); if let Some(Value::Dict(headers)) = dict_get(&req_map, "headers") { - let hdrs = headers.lock().map_err(|_| err(exec, span, "Headers map lock poisoned"))?; + let hdrs = headers + .lock() + .map_err(|_| err(exec, span, "Headers map lock poisoned"))?; for (k, v) in hdrs.iter() { request = request.header(k, v.as_string()); } @@ -179,7 +206,9 @@ fn net_http(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa } let started = Instant::now(); - let response = request.send().map_err(|e| err(exec, span, &format!("HTTP request failed: {e}")))?; + let response = request + .send() + .map_err(|e| err(exec, span, &format!("HTTP request failed: {e}")))?; let duration = started.elapsed(); let status = response.status().as_u16() as f64; @@ -200,11 +229,17 @@ fn net_http(exec: &mut Executor, args: Vec, span: crate::parser::ast::Spa let mut out = HashMap::new(); out.insert("status".to_string(), Value::Number(status)); - out.insert("ok".to_string(), Value::Bool((200.0..300.0).contains(&status))); + out.insert( + "ok".to_string(), + Value::Bool((200.0..300.0).contains(&status)), + ); out.insert("headers".to_string(), value_dict(headers_map)); out.insert("body_text".to_string(), Value::String(body_text)); out.insert("body_bytes".to_string(), bytes_to_value_array(&body_bytes)); - out.insert("duration_ms".to_string(), Value::Number(duration.as_millis() as f64)); + out.insert( + "duration_ms".to_string(), + Value::Number(duration.as_millis() as f64), + ); Ok(value_dict(out)) } @@ -212,10 +247,15 @@ fn resolve_addr(addr: &str) -> Result { let mut iter = addr .to_socket_addrs() .map_err(|e| format!("Invalid address '{addr}': {e}"))?; - iter.next().ok_or_else(|| format!("Could not resolve address '{addr}'")) + iter.next() + .ok_or_else(|| format!("Could not resolve address '{addr}'")) } -fn net_tcp_connect(exec: &mut Executor, args: Vec, span: crate::parser::ast::Span) -> Result { +fn net_tcp_connect( + exec: &mut Executor, + args: Vec, + span: crate::parser::ast::Span, +) -> Result { let addr = expect_string(args.get(0), "addr").map_err(|e| err(exec, span, &e))?; let timeout = optional_timeout_ms(args.get(1)).map_err(|e| err(exec, span, &e))?; let socket_addr = resolve_addr(&addr).map_err(|e| err(exec, span, &e))?; @@ -230,13 +270,22 @@ fn net_tcp_connect(exec: &mut Executor, args: Vec, span: crate::parser::a Ok(make_connection_value(stream, timeout)) } -fn net_tcp_listen(exec: &mut Executor, args: Vec, span: crate::parser::ast::Span) -> Result { +fn net_tcp_listen( + exec: &mut Executor, + args: Vec, + span: crate::parser::ast::Span, +) -> Result { let addr = expect_string(args.get(0), "addr").map_err(|e| err(exec, span, &e))?; let accept_timeout = optional_timeout_ms(args.get(1)).map_err(|e| err(exec, span, &e))?; - let listener = TcpListener::bind(&addr).map_err(|e| err(exec, span, &format!("tcp_listen failed: {e}")))?; - listener - .set_nonblocking(true) - .map_err(|e| err(exec, span, &format!("Failed to set nonblocking listener: {e}")))?; + let listener = TcpListener::bind(&addr) + .map_err(|e| err(exec, span, &format!("tcp_listen failed: {e}")))?; + listener.set_nonblocking(true).map_err(|e| { + err( + exec, + span, + &format!("Failed to set nonblocking listener: {e}"), + ) + })?; Ok(make_listener_value(listener, accept_timeout)) } @@ -274,7 +323,8 @@ fn make_connection_value(stream: TcpStream, default_timeout: Duration) -> Value map.insert( "recv".to_string(), native_exec(move |exec, args, span| { - let max = expect_number(args.first(), "max_bytes").map_err(|e| err(exec, span, &e))? as i64; + let max = expect_number(args.first(), "max_bytes") + .map_err(|e| err(exec, span, &e))? as i64; if max <= 0 { return Err(err(exec, span, "max_bytes must be > 0")); } @@ -297,7 +347,10 @@ fn make_connection_value(stream: TcpStream, default_timeout: Duration) -> Value let mut buf = vec![0u8; max as usize]; let n = match stream.read(&mut buf) { Ok(n) => n, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + Err(e) + if e.kind() == std::io::ErrorKind::WouldBlock + || e.kind() == std::io::ErrorKind::TimedOut => + { return Err(err(exec, span, "recv timed out")); } Err(e) => return Err(err(exec, span, &format!("recv failed: {e}"))), @@ -305,7 +358,10 @@ fn make_connection_value(stream: TcpStream, default_timeout: Duration) -> Value buf.truncate(n); let mut out = HashMap::new(); - out.insert("text".to_string(), Value::String(String::from_utf8_lossy(&buf).to_string())); + out.insert( + "text".to_string(), + Value::String(String::from_utf8_lossy(&buf).to_string()), + ); out.insert("bytes".to_string(), bytes_to_value_array(&buf)); out.insert("count".to_string(), Value::Number(n as f64)); Ok(value_dict(out)) diff --git a/src/runtime/builtins/numbers.rs b/src/runtime/builtins/numbers.rs index 7d60940..a4a279a 100644 --- a/src/runtime/builtins/numbers.rs +++ b/src/runtime/builtins/numbers.rs @@ -49,7 +49,10 @@ where fn expect_number(value: Option<&Value>) -> Result { match value { Some(Value::Number(n)) => Ok(*n), - Some(v) => v.as_string().parse::().map_err(|_| "Expected number argument".to_string()), + Some(v) => v + .as_string() + .parse::() + .map_err(|_| "Expected number argument".to_string()), None => Err("Expected number argument".to_string()), } } diff --git a/src/runtime/builtins/strings.rs b/src/runtime/builtins/strings.rs index 83ed4be..8dea53d 100644 --- a/src/runtime/builtins/strings.rs +++ b/src/runtime/builtins/strings.rs @@ -13,10 +13,16 @@ pub fn get_method(name: &str, receiver: &str) -> Option { "split" => native(move |args| { let sep = expect_string(args.get(0))?; if sep.is_empty() { - let parts: Vec = receiver.chars().map(|c| Value::String(c.to_string())).collect(); + let parts: Vec = receiver + .chars() + .map(|c| Value::String(c.to_string())) + .collect(); Ok(Value::Array(Arc::new(Mutex::new(parts)))) } else { - let parts: Vec = receiver.split(&sep).map(|s| Value::String(s.to_string())).collect(); + let parts: Vec = receiver + .split(&sep) + .map(|s| Value::String(s.to_string())) + .collect(); Ok(Value::Array(Arc::new(Mutex::new(parts)))) } }), @@ -48,7 +54,9 @@ pub fn get_method(name: &str, receiver: &str) -> Option { let start = expect_usize(args.get(0))?; let len = expect_usize(args.get(1))?; let chars: Vec = receiver.chars().collect(); - if start >= chars.len() { return Ok(Value::String("".to_string())); } + if start >= chars.len() { + return Ok(Value::String("".to_string())); + } let end = (start + len).min(chars.len()); Ok(Value::String(chars[start..end].iter().collect())) }), @@ -88,7 +96,9 @@ pub fn get_method(name: &str, receiver: &str) -> Option { let start = expect_usize(args.get(0))?; let len = expect_usize(args.get(1))?; let chars: Vec = receiver.chars().collect(); - if start >= chars.len() { return Ok(Value::String(receiver.clone())); } + if start >= chars.len() { + return Ok(Value::String(receiver.clone())); + } let end = (start + len).min(chars.len()); let mut out = String::new(); out.push_str(&chars[..start].iter().collect::()); @@ -102,9 +112,15 @@ pub fn get_method(name: &str, receiver: &str) -> Option { let len = chars.len() as isize; let mut s = if start < 0 { len + start } else { start }; let mut e = if end < 0 { len + end } else { end }; - if s < 0 { s = 0; } - if e < s { e = s; } - if s as usize >= chars.len() { return Ok(Value::String("".to_string())); } + if s < 0 { + s = 0; + } + if e < s { + e = s; + } + if s as usize >= chars.len() { + return Ok(Value::String("".to_string())); + } let e_usize = (e as usize).min(chars.len()); Ok(Value::String(chars[s as usize..e_usize].iter().collect())) }), @@ -141,7 +157,10 @@ fn expect_string_opt(value: Option<&Value>) -> Option { fn expect_usize(value: Option<&Value>) -> Result { match value { Some(Value::Number(n)) => Ok(*n as usize), - Some(v) => v.as_string().parse::().map_err(|_| "Expected integer argument".to_string()), + Some(v) => v + .as_string() + .parse::() + .map_err(|_| "Expected integer argument".to_string()), None => Err("Expected integer argument".to_string()), } } @@ -149,7 +168,10 @@ fn expect_usize(value: Option<&Value>) -> Result { fn expect_isize(value: Option<&Value>) -> Result { match value { Some(Value::Number(n)) => Ok(*n as isize), - Some(v) => v.as_string().parse::().map_err(|_| "Expected integer argument".to_string()), + Some(v) => v + .as_string() + .parse::() + .map_err(|_| "Expected integer argument".to_string()), None => Err("Expected integer argument".to_string()), } } diff --git a/src/runtime/errors.rs b/src/runtime/errors.rs index 3098c36..d0203cd 100644 --- a/src/runtime/errors.rs +++ b/src/runtime/errors.rs @@ -18,21 +18,43 @@ pub struct RuntimeError { } impl RuntimeError { - pub fn new(message: String, filename: String, line: usize, col: usize, snippet: Option, stack: Vec) -> Self { - RuntimeError { message, filename, line, col, snippet, stack } + pub fn new( + message: String, + filename: String, + line: usize, + col: usize, + snippet: Option, + stack: Vec, + ) -> Self { + RuntimeError { + message, + filename, + line, + col, + snippet, + stack, + } } } impl fmt::Display for RuntimeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "{}:{}:{}: {}", self.filename, self.line, self.col, self.message)?; + writeln!( + f, + "{}:{}:{}: {}", + self.filename, self.line, self.col, self.message + )?; if let Some(snippet) = &self.snippet { writeln!(f, "Snippet:\n{}", snippet)?; } if !self.stack.is_empty() { writeln!(f, "Stack trace:")?; for frame in &self.stack { - writeln!(f, " at {} ({}:{}:{})", frame.name, self.filename, frame.line, frame.col)?; + writeln!( + f, + " at {} ({}:{}:{})", + frame.name, self.filename, frame.line, frame.col + )?; } } Ok(()) diff --git a/src/runtime/executor.rs b/src/runtime/executor.rs index 1dcf841..1b77faa 100644 --- a/src/runtime/executor.rs +++ b/src/runtime/executor.rs @@ -1,15 +1,22 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}, mpsc}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + mpsc, +}; use std::thread; use std::time::Duration; use crate::lexer::scanner::Scanner; -use crate::parser::ast::{CallArg, Expr, ImportSource, InterpPart, MatchArmKind, ParamSpec, Pattern, Program, RecordField, Span, Stmt, TypeExpr}; +use crate::parser::ast::{ + CallArg, Expr, ImportSource, InterpPart, MatchArmKind, ParamSpec, Pattern, Program, + RecordField, Span, Stmt, TypeExpr, +}; use crate::parser::parser::Parser; -use crate::runtime::errors::{Frame, RuntimeError}; use crate::runtime::builtins::{numbers, strings}; +use crate::runtime::errors::{Frame, RuntimeError}; use crate::runtime::shell::{sh::run_sh, ssh::run_ssh}; use crate::runtime::value::{ModuleOrigin, ModuleValue, Value}; @@ -138,20 +145,17 @@ impl Executor { where F: Fn(Vec) -> Result + Send + Sync + 'static, { - self.scopes[0].insert( - name.to_string(), - Value::NativeFunction(Arc::new(func)), - ); + self.scopes[0].insert(name.to_string(), Value::NativeFunction(Arc::new(func))); } pub fn register_native_exec(&mut self, name: &str, func: F) where - F: Fn(&mut Executor, Vec, Span) -> Result + Send + Sync + 'static, + F: Fn(&mut Executor, Vec, Span) -> Result + + Send + + Sync + + 'static, { - self.scopes[0].insert( - name.to_string(), - Value::NativeFunctionExec(Arc::new(func)), - ); + self.scopes[0].insert(name.to_string(), Value::NativeFunctionExec(Arc::new(func))); } #[allow(dead_code)] @@ -165,13 +169,11 @@ impl Executor { exports, origin: ModuleOrigin::Builtin, }); - self.builtin_modules.insert(name.to_string(), module.clone()); + self.builtin_modules + .insert(name.to_string(), module.clone()); self.scopes[0].insert(name.to_string(), Value::Module(module.clone())); if let Ok(mut cache) = self.module_cache.lock() { - cache.insert( - format!("builtin:{name}"), - ModuleLoadState::Ready(module), - ); + cache.insert(format!("builtin:{name}"), ModuleLoadState::Ready(module)); } } @@ -186,10 +188,14 @@ impl Executor { match self.exec_stmt(stmt)? { ExecResult::Normal => {} ExecResult::Return(_) => {} - ExecResult::Continue => return Err(self.err("'continue' used outside loop", stmt.span())), + ExecResult::Continue => { + return Err(self.err("'continue' used outside loop", stmt.span())); + } ExecResult::Break => return Err(self.err("'break' used outside loop", stmt.span())), ExecResult::Throw(v) => { - return Err(self.err(&format!("Uncaught throw: {}", v.as_string()), stmt.span())) + return Err( + self.err(&format!("Uncaught throw: {}", v.as_string()), stmt.span()) + ); } } } @@ -207,10 +213,16 @@ impl Executor { _ => match self.exec_stmt(stmt)? { ExecResult::Normal => {} ExecResult::Return(_) => {} - ExecResult::Continue => return Err(self.err("'continue' used outside loop", stmt.span())), - ExecResult::Break => return Err(self.err("'break' used outside loop", stmt.span())), + ExecResult::Continue => { + return Err(self.err("'continue' used outside loop", stmt.span())); + } + ExecResult::Break => { + return Err(self.err("'break' used outside loop", stmt.span())); + } ExecResult::Throw(v) => { - return Err(self.err(&format!("Uncaught throw: {}", v.as_string()), stmt.span())) + return Err( + self.err(&format!("Uncaught throw: {}", v.as_string()), stmt.span()) + ); } }, } @@ -245,7 +257,11 @@ impl Executor { self.define(name, value); Ok(ExecResult::Normal) } - Stmt::LetDestructure { pattern, expr, span } => { + Stmt::LetDestructure { + pattern, + expr, + span, + } => { let value = self.eval_expr(expr)?; self.bind_pattern(pattern, value, *span)?; Ok(ExecResult::Normal) @@ -257,20 +273,36 @@ impl Executor { } Ok(ExecResult::Normal) } - Stmt::IndexAssign { target, index, expr, .. } => { + Stmt::IndexAssign { + target, + index, + expr, + .. + } => { let idx = self.eval_expr(index)?; let val = self.eval_expr(expr)?; match target { Expr::Var { name, .. } => { - let obj = self.lookup(name).ok_or_else(|| self.err("Undefined variable", target.span()))?; + let obj = self + .lookup(name) + .ok_or_else(|| self.err("Undefined variable", target.span()))?; let updated = self.assign_index(obj, idx, val)?; self.assign(name, updated); } - _ => return Err(self.err("Index assignment requires variable target", target.span())), + _ => { + return Err( + self.err("Index assignment requires variable target", target.span()) + ); + } } Ok(ExecResult::Normal) } - Stmt::If { cond, then_branch, else_branch, .. } => { + Stmt::If { + cond, + then_branch, + else_branch, + .. + } => { let c = self.eval_expr(cond)?; let res = if c.is_truthy() { self.exec_block(then_branch)? @@ -279,7 +311,12 @@ impl Executor { }; Ok(res) } - Stmt::For { pattern, iterable, body, span } => { + Stmt::For { + pattern, + iterable, + body, + span, + } => { let it = self.eval_expr(iterable)?; let items = self.iterate_value(it, *span)?; for item in items { @@ -308,14 +345,21 @@ impl Executor { } Ok(ExecResult::Normal) } - Stmt::ParallelFor { pattern, iterable, body, .. } => { + Stmt::ParallelFor { + pattern, + iterable, + body, + .. + } => { self.exec_parallel_for(pattern, iterable, body)?; Ok(ExecResult::Normal) } Stmt::While { cond, body, .. } => { loop { let c = self.eval_expr(cond)?; - if !c.is_truthy() { break; } + if !c.is_truthy() { + break; + } self.loop_depth += 1; let body_res = self.exec_block(body); self.loop_depth -= 1; @@ -335,10 +379,19 @@ impl Executor { } Ok(ExecResult::Normal) } - Stmt::Try { try_block, catch_name, catch_block, finally_block, .. } => { - let mut pending: Result = match self.exec_block(try_block) { + Stmt::Try { + try_block, + catch_name, + catch_block, + finally_block, + .. + } => { + let mut pending: Result = match self.exec_block(try_block) + { Ok(ExecResult::Throw(v)) => { - if let (Some(name), Some(block)) = (catch_name.as_ref(), catch_block.as_ref()) { + if let (Some(name), Some(block)) = + (catch_name.as_ref(), catch_block.as_ref()) + { self.push_scope(); self.define(name, v); let catch_res = self.exec_block(block); @@ -349,7 +402,9 @@ impl Executor { } Ok(other) => Ok(other), Err(e) => { - if let (Some(name), Some(block)) = (catch_name.as_ref(), catch_block.as_ref()) { + if let (Some(name), Some(block)) = + (catch_name.as_ref(), catch_block.as_ref()) + { let err_value = self.error_to_value(&e); self.push_scope(); self.define(name, err_value); @@ -406,7 +461,10 @@ impl Executor { } else { Value::Null }; - let ctx = self.generator_ctx.as_mut().expect("checked generator context"); + let ctx = self + .generator_ctx + .as_mut() + .expect("checked generator context"); if ctx.event_tx.send(GeneratorEvent::Yield(yielded)).is_err() { return Ok(ExecResult::Return(Value::Null)); } @@ -439,21 +497,36 @@ impl Executor { Err(self.err("No active scope for defer", stmt.span())) } } - Stmt::Use { module, alias, span } => { + Stmt::Use { + module, + alias, + span, + } => { self.exec_use(module, alias.as_deref(), *span)?; Ok(ExecResult::Normal) } - Stmt::ImportNamed { items, source, span } => { + Stmt::ImportNamed { + items, + source, + span, + } => { self.exec_import_named(items, source, *span)?; Ok(ExecResult::Normal) } - Stmt::ImportNamespace { alias, source, span } => { + Stmt::ImportNamespace { + alias, + source, + span, + } => { self.exec_import_namespace(alias, source, *span)?; Ok(ExecResult::Normal) } Stmt::TypeAlias { name, target, span } => { if self.record_defs.contains_key(name) { - return Err(self.err(&format!("Type name '{}' conflicts with existing record", name), *span)); + return Err(self.err( + &format!("Type name '{}' conflicts with existing record", name), + *span, + )); } if self.type_aliases.contains_key(name) { return Err(self.err(&format!("Type '{}' is already defined", name), *span)); @@ -463,7 +536,10 @@ impl Executor { } Stmt::RecordDef { name, fields, span } => { if self.type_aliases.contains_key(name) { - return Err(self.err(&format!("Record name '{}' conflicts with existing type alias", name), *span)); + return Err(self.err( + &format!("Record name '{}' conflicts with existing type alias", name), + *span, + )); } let mut seen = std::collections::HashSet::new(); for field in fields { @@ -471,10 +547,17 @@ impl Executor { return Err(self.err("Record field name '__type' is reserved", *span)); } if !seen.insert(field.name.clone()) { - return Err(self.err(&format!("Duplicate record field '{}'", field.name), *span)); + return Err( + self.err(&format!("Duplicate record field '{}'", field.name), *span) + ); } } - self.record_defs.insert(name.clone(), RecordDef { fields: fields.clone() }); + self.record_defs.insert( + name.clone(), + RecordDef { + fields: fields.clone(), + }, + ); Ok(ExecResult::Normal) } Stmt::Invoke { name, expr, span } => { @@ -513,7 +596,12 @@ impl Executor { self.exit_scope(pending) } - fn exec_parallel_for(&mut self, pattern: &Pattern, iterable: &Expr, body: &[Stmt]) -> Result<(), RuntimeError> { + fn exec_parallel_for( + &mut self, + pattern: &Pattern, + iterable: &Expr, + body: &[Stmt], + ) -> Result<(), RuntimeError> { let it = self.eval_expr(iterable)?; let items = self.iterate_value(it, iterable.span())?; if items.is_empty() { @@ -628,8 +716,15 @@ impl Executor { self.last_span = expr.span(); match expr { Expr::Literal { value, .. } => Ok(value.clone()), - Expr::Var { name, span } => self.lookup(name).ok_or_else(|| self.err("Undefined variable", *span)), - Expr::Binary { left, op, right, span } => { + Expr::Var { name, span } => self + .lookup(name) + .ok_or_else(|| self.err("Undefined variable", *span)), + Expr::Binary { + left, + op, + right, + span, + } => { if op == "&&" { let l = self.eval_expr(left)?; if !l.is_truthy() { @@ -695,19 +790,27 @@ impl Executor { let obj = self.eval_expr(object)?; self.namespace_access(obj, name, *span) } - Expr::Index { object, index, span } => { + Expr::Index { + object, + index, + span, + } => { let obj = self.eval_expr(object)?; let idx = self.eval_expr(index)?; self.index_access(obj, idx, *span) } Expr::Array { items, .. } => { let mut vals = Vec::new(); - for e in items { vals.push(self.eval_expr(e)?); } + for e in items { + vals.push(self.eval_expr(e)?); + } Ok(Value::Array(Arc::new(Mutex::new(vals)))) } Expr::Tuple { items, .. } => { let mut vals = Vec::new(); - for e in items { vals.push(self.eval_expr(e)?); } + for e in items { + vals.push(self.eval_expr(e)?); + } Ok(Value::tuple(vals)) } Expr::Dict { items, .. } => { @@ -752,9 +855,14 @@ impl Executor { Expr::Range { start, end, .. } => { let s = self.eval_expr(start)?; let e = self.eval_expr(end)?; - Ok(Value::Range(to_i64(s)? , to_i64(e)?)) + Ok(Value::Range(to_i64(s)?, to_i64(e)?)) } - Expr::IfExpr { cond, then_branch, else_branch, .. } => { + Expr::IfExpr { + cond, + then_branch, + else_branch, + .. + } => { let c = self.eval_expr(cond)?; if c.is_truthy() { self.eval_block_expr(then_branch) @@ -762,7 +870,11 @@ impl Executor { self.eval_block_expr(else_branch) } } - Expr::Match { subject, arms, span } => { + Expr::Match { + subject, + arms, + span, + } => { let subject_value = self.eval_expr(subject)?; for arm in arms { let matched = match &arm.kind { @@ -773,7 +885,8 @@ impl Executor { } MatchArmKind::Compare { op, rhs } => { let rhs_value = self.eval_expr(rhs)?; - let cmp = self.eval_binary(subject_value.clone(), op, rhs_value, *span)?; + let cmp = + self.eval_binary(subject_value.clone(), op, rhs_value, *span)?; cmp.is_truthy() } }; @@ -781,11 +894,17 @@ impl Executor { return self.eval_block_expr(&arm.body); } } - Err(self.err("No match arm matched and wildcard arm was not reached", *span)) - } - Expr::ParallelFor { pattern, iterable, body, .. } => { - self.eval_parallel_for_expr(pattern, iterable, body) + Err(self.err( + "No match arm matched and wildcard arm was not reached", + *span, + )) } + Expr::ParallelFor { + pattern, + iterable, + body, + .. + } => self.eval_parallel_for_expr(pattern, iterable, body), Expr::TaskBlock { body, .. } => { let body = body.clone(); let captured = self.flatten_scopes(); @@ -885,7 +1004,12 @@ impl Executor { _ => Err(self.err("await expects task", *span)), } } - Expr::Lambda { params, return_type, body, .. } => { + Expr::Lambda { + params, + return_type, + body, + .. + } => { let captured = self.flatten_scopes(); let func = Function { params: params.clone(), @@ -925,7 +1049,12 @@ impl Executor { } } - fn bind_pattern(&mut self, pattern: &Pattern, value: Value, span: Span) -> Result<(), RuntimeError> { + fn bind_pattern( + &mut self, + pattern: &Pattern, + value: Value, + span: Span, + ) -> Result<(), RuntimeError> { match pattern { Pattern::Ignore => Ok(()), Pattern::Name(name) => { @@ -934,8 +1063,12 @@ impl Executor { } Pattern::Tuple(parts) => { let values = match value { - Value::Array(arr) => arr.lock().map(|v| v.clone()).unwrap_or_else(|_| Vec::new()), - Value::Tuple(tup) => tup.lock().map(|v| v.clone()).unwrap_or_else(|_| Vec::new()), + Value::Array(arr) => { + arr.lock().map(|v| v.clone()).unwrap_or_else(|_| Vec::new()) + } + Value::Tuple(tup) => { + tup.lock().map(|v| v.clone()).unwrap_or_else(|_| Vec::new()) + } other => vec![other], }; for (idx, pat) in parts.iter().enumerate() { @@ -954,19 +1087,17 @@ impl Executor { for stmt in stmts { match stmt { Stmt::Expr { expr, .. } => last = self.eval_expr(expr)?, - _ => { - match self.exec_stmt(stmt) { - Ok(ExecResult::Normal) => {} - Ok(other) => { - pending = Ok(other); - break; - } - Err(e) => { - pending = Err(e); - break; - } + _ => match self.exec_stmt(stmt) { + Ok(ExecResult::Normal) => {} + Ok(other) => { + pending = Ok(other); + break; } - } + Err(e) => { + pending = Err(e); + break; + } + }, } } match self.exit_scope(pending)? { @@ -974,11 +1105,19 @@ impl Executor { ExecResult::Return(v) => Ok(v), ExecResult::Continue => Ok(last), ExecResult::Break => Ok(last), - ExecResult::Throw(v) => Err(self.err(&format!("Uncaught throw: {}", v.as_string()), self.last_span)), + ExecResult::Throw(v) => Err(self.err( + &format!("Uncaught throw: {}", v.as_string()), + self.last_span, + )), } } - fn eval_parallel_for_expr(&mut self, pattern: &Pattern, iterable: &Expr, body: &[Stmt]) -> Result { + fn eval_parallel_for_expr( + &mut self, + pattern: &Pattern, + iterable: &Expr, + body: &[Stmt], + ) -> Result { let it = self.eval_expr(iterable)?; let items = self.iterate_value(it, iterable.span())?; if items.is_empty() { @@ -1105,7 +1244,10 @@ impl Executor { return Err(err); } - let values = ordered.into_iter().map(|v| v.unwrap_or(Value::Null)).collect(); + let values = ordered + .into_iter() + .map(|v| v.unwrap_or(Value::Null)) + .collect(); Ok(Value::array(values)) } @@ -1211,7 +1353,11 @@ impl Executor { Value::Generator(Arc::new(handle)) } - pub fn next_generator(&self, generator: &Arc, span: Span) -> Result { + pub fn next_generator( + &self, + generator: &Arc, + span: Span, + ) -> Result { match self.generator_next_step(generator, span)? { GeneratorStep::Yield(v) => Ok(generator_step_dict(false, v, Value::Null)), GeneratorStep::Done(v) => Ok(generator_step_dict(true, Value::Null, v)), @@ -1254,7 +1400,12 @@ impl Executor { } } - fn call(&mut self, callee: Value, args: Vec, span: Span) -> Result { + fn call( + &mut self, + callee: Value, + args: Vec, + span: Span, + ) -> Result { match callee { Value::NativeFunction(f) => { let positional = self.positional_only_args(args, span)?; @@ -1308,7 +1459,11 @@ impl Executor { } } - pub fn call_value(&mut self, callee: Value, args: Vec) -> Result { + pub fn call_value( + &mut self, + callee: Value, + args: Vec, + ) -> Result { self.call(callee, args, Span { line: 0, col: 0 }) } @@ -1316,7 +1471,11 @@ impl Executor { self.iterate_value(value, span) } - fn positional_only_args(&self, args: Vec, span: Span) -> Result, RuntimeError> { + fn positional_only_args( + &self, + args: Vec, + span: Span, + ) -> Result, RuntimeError> { let mut out = Vec::new(); for arg in args { match arg { @@ -1325,14 +1484,22 @@ impl Executor { out.extend(self.expand_spread_value(v, span)?); } RuntimeCallArg::Named { .. } => { - return Err(self.err("Named arguments are only supported for user-defined functions in v1", span)); + return Err(self.err( + "Named arguments are only supported for user-defined functions in v1", + span, + )); } } } Ok(out) } - fn call_user_function(&mut self, func: &Function, args: Vec, span: Span) -> Result { + fn call_user_function( + &mut self, + func: &Function, + args: Vec, + span: Span, + ) -> Result { let mut positional = Vec::new(); let mut named: HashMap = HashMap::new(); for arg in args { @@ -1343,7 +1510,10 @@ impl Executor { } RuntimeCallArg::Named { name, value } => { if named.insert(name.clone(), value).is_some() { - return Err(self.err(&format!("Duplicate named argument '{name}' for {}", func.name), span)); + return Err(self.err( + &format!("Duplicate named argument '{name}' for {}", func.name), + span, + )); } } } @@ -1356,7 +1526,10 @@ impl Executor { if let Some(vidx) = variadic_param { if vidx != func.params.len() - 1 { return Err(self.err( - &format!("Variadic parameter for {} must be the final parameter", func.name), + &format!( + "Variadic parameter for {} must be the final parameter", + func.name + ), span, )); } @@ -1385,7 +1558,10 @@ impl Executor { if param.variadic { if named.contains_key(¶m.name) { return Err(self.err( - &format!("Variadic parameter '{}' for {} cannot be passed by name", param.name, func.name), + &format!( + "Variadic parameter '{}' for {} cannot be passed by name", + param.name, func.name + ), span, )); } @@ -1419,7 +1595,10 @@ impl Executor { let value = if positional_index < positional.len() { if named.contains_key(¶m.name) { return Err(self.err( - &format!("Argument '{}' for {} was provided both positionally and by name", param.name, func.name), + &format!( + "Argument '{}' for {} was provided both positionally and by name", + param.name, func.name + ), span, )); } @@ -1432,7 +1611,10 @@ impl Executor { self.eval_expr(default_expr)? } else { return Err(self.err( - &format!("Missing required argument '{}' for {}", param.name, func.name), + &format!( + "Missing required argument '{}' for {}", + param.name, func.name + ), span, )); }; @@ -1483,7 +1665,7 @@ impl Executor { ExecResult::Continue => return Err(self.err("'continue' used outside loop", span)), ExecResult::Break => return Err(self.err("'break' used outside loop", span)), ExecResult::Throw(v) => { - return Err(self.err(&format!("Uncaught throw: {}", v.as_string()), span)) + return Err(self.err(&format!("Uncaught throw: {}", v.as_string()), span)); } }; if let Some(t) = &func.return_type { @@ -1526,7 +1708,12 @@ impl Executor { } } - fn construct_record(&mut self, record_name: &str, args: Vec, span: Span) -> Result { + fn construct_record( + &mut self, + record_name: &str, + args: Vec, + span: Span, + ) -> Result { let def = self .record_defs .get(record_name) @@ -1543,7 +1730,10 @@ impl Executor { } RuntimeCallArg::Positional(_) | RuntimeCallArg::Spread(_) => { return Err(self.err( - &format!("Record constructor {}(...) expects named arguments only", record_name), + &format!( + "Record constructor {}(...) expects named arguments only", + record_name + ), span, )); } @@ -1562,7 +1752,10 @@ impl Executor { self.eval_expr(default)? } else { pending = Err(self.err( - &format!("Record '{}' missing required field '{}'", record_name, field.name), + &format!( + "Record '{}' missing required field '{}'", + record_name, field.name + ), span, )); break; @@ -1609,7 +1802,12 @@ impl Executor { Ok(scope_out) } - fn construct_nominal(&mut self, type_name: &str, args: Vec, span: Span) -> Result { + fn construct_nominal( + &mut self, + type_name: &str, + args: Vec, + span: Span, + ) -> Result { let base = self .type_aliases .get(type_name) @@ -1618,7 +1816,10 @@ impl Executor { if args.len() != 1 { return Err(self.err( - &format!("Type constructor {}(...) expects exactly 1 positional argument", type_name), + &format!( + "Type constructor {}(...) expects exactly 1 positional argument", + type_name + ), span, )); } @@ -1626,7 +1827,10 @@ impl Executor { RuntimeCallArg::Positional(v) => v, RuntimeCallArg::Spread(_) | RuntimeCallArg::Named { .. } => { return Err(self.err( - &format!("Type constructor {}(...) expects exactly 1 positional argument", type_name), + &format!( + "Type constructor {}(...) expects exactly 1 positional argument", + type_name + ), span, )); } @@ -1654,7 +1858,12 @@ impl Executor { type_str(ty) } - fn type_matches_checked(&self, ty: &TypeExpr, value: &Value, span: Span) -> Result { + fn type_matches_checked( + &self, + ty: &TypeExpr, + value: &Value, + span: Span, + ) -> Result { type_matches_with_resolver(ty, value, &self.type_aliases, &self.record_defs) .map_err(|e| self.err(&e, span)) } @@ -1701,11 +1910,12 @@ impl Executor { fn namespace_access(&self, obj: Value, name: &str, span: Span) -> Result { match obj { - Value::Module(module) => module - .exports - .get(name) - .cloned() - .ok_or_else(|| self.err(&format!("Module '{}' has no export '{}'", module.name, name), span)), + Value::Module(module) => module.exports.get(name).cloned().ok_or_else(|| { + self.err( + &format!("Module '{}' has no export '{}'", module.name, name), + span, + ) + }), _ => Err(self.err("'::' requires module namespace", span)), } } @@ -1722,94 +1932,125 @@ impl Executor { return Ok(Value::Number(guard.len() as f64)); } } - Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::Dict(map)) }) - } - Value::Array(arr) => { - match name { - "add" => { - let arr_ref = arr.clone(); - let func = move |args: Vec| { - let mut list = arr_ref.lock().map_err(|_| "Array lock poisoned".to_string())?; - if let Some(v) = args.get(0) { - list.push(v.clone()); - Ok(Value::Null) - } else { - Err("add expects 1 argument".to_string()) - } - }; - Ok(Value::NativeFunction(Arc::new(func))) - } - "count" => Ok(Value::Number(arr.lock().map(|v| v.len() as f64).unwrap_or(0.0))), - _ => Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::Array(arr)) }), - } + Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::Dict(map)), + }) } + Value::Array(arr) => match name { + "add" => { + let arr_ref = arr.clone(); + let func = move |args: Vec| { + let mut list = arr_ref + .lock() + .map_err(|_| "Array lock poisoned".to_string())?; + if let Some(v) = args.get(0) { + list.push(v.clone()); + Ok(Value::Null) + } else { + Err("add expects 1 argument".to_string()) + } + }; + Ok(Value::NativeFunction(Arc::new(func))) + } + "count" => Ok(Value::Number( + arr.lock().map(|v| v.len() as f64).unwrap_or(0.0), + )), + _ => Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::Array(arr)), + }), + }, Value::Tuple(tup) => match name { - "count" => Ok(Value::Number(tup.lock().map(|v| v.len() as f64).unwrap_or(0.0))), - _ => Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::Tuple(tup)) }), + "count" => Ok(Value::Number( + tup.lock().map(|v| v.len() as f64).unwrap_or(0.0), + )), + _ => Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::Tuple(tup)), + }), }, Value::String(s) => { if let Some(method) = strings::get_method(name, &s) { return Ok(method); } - Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::String(s)) }) + Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::String(s)), + }) } Value::Task(task) => match name { "join" => { let task_ref = task.clone(); - Ok(Value::NativeFunctionExec(Arc::new(move |exec, _args, span| { - exec.task_join(&task_ref, span) - }))) + Ok(Value::NativeFunctionExec(Arc::new( + move |exec, _args, span| exec.task_join(&task_ref, span), + ))) } "done" => { let task_ref = task.clone(); - Ok(Value::NativeFunctionExec(Arc::new(move |exec, _args, span| { - let done = task_ref - .state - .lock() - .map_err(|_| exec.err("Task state lock poisoned", span))? - .status - != TaskStatus::Running; - Ok(Value::Bool(done)) - }))) + Ok(Value::NativeFunctionExec(Arc::new( + move |exec, _args, span| { + let done = task_ref + .state + .lock() + .map_err(|_| exec.err("Task state lock poisoned", span))? + .status + != TaskStatus::Running; + Ok(Value::Bool(done)) + }, + ))) } "cancel" => { let task_ref = task.clone(); - Ok(Value::NativeFunctionExec(Arc::new(move |exec, _args, span| { - let mut state = task_ref - .state - .lock() - .map_err(|_| exec.err("Task state lock poisoned", span))?; - if state.status == TaskStatus::Running { - state.status = TaskStatus::Cancelled; - state.error = Some(exec.err("Task cancelled", span)); - return Ok(Value::Bool(true)); - } - Ok(Value::Bool(false)) - }))) + Ok(Value::NativeFunctionExec(Arc::new( + move |exec, _args, span| { + let mut state = task_ref + .state + .lock() + .map_err(|_| exec.err("Task state lock poisoned", span))?; + if state.status == TaskStatus::Running { + state.status = TaskStatus::Cancelled; + state.error = Some(exec.err("Task cancelled", span)); + return Ok(Value::Bool(true)); + } + Ok(Value::Bool(false)) + }, + ))) } "error" => { let task_ref = task.clone(); - Ok(Value::NativeFunctionExec(Arc::new(move |exec, _args, span| { - let state = task_ref - .state - .lock() - .map_err(|_| exec.err("Task state lock poisoned", span))?; - if let Some(err) = &state.error { - Ok(Value::String(err.message.clone())) - } else { - Ok(Value::Null) - } - }))) + Ok(Value::NativeFunctionExec(Arc::new( + move |exec, _args, span| { + let state = task_ref + .state + .lock() + .map_err(|_| exec.err("Task state lock poisoned", span))?; + if let Some(err) = &state.error { + Ok(Value::String(err.message.clone())) + } else { + Ok(Value::Null) + } + }, + ))) } - _ => Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::Task(task)) }), + _ => Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::Task(task)), + }), }, Value::Number(n) => { if let Some(method) = numbers::get_method(name, n) { return Ok(method); } - Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(Value::Number(n)) }) + Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(Value::Number(n)), + }) } - _ => Ok(Value::Ufcs { name: name.to_string(), receiver: Box::new(obj) }), + _ => Ok(Value::Ufcs { + name: name.to_string(), + receiver: Box::new(obj), + }), } } @@ -1817,15 +2058,24 @@ impl Executor { match obj { Value::Array(arr) => { let i = to_i64(idx)? as usize; - arr.lock().ok().and_then(|v| v.get(i).cloned()).ok_or_else(|| self.err("Index out of range", span)) + arr.lock() + .ok() + .and_then(|v| v.get(i).cloned()) + .ok_or_else(|| self.err("Index out of range", span)) } Value::Tuple(tup) => { let i = to_i64(idx)? as usize; - tup.lock().ok().and_then(|v| v.get(i).cloned()).ok_or_else(|| self.err("Index out of range", span)) + tup.lock() + .ok() + .and_then(|v| v.get(i).cloned()) + .ok_or_else(|| self.err("Index out of range", span)) } Value::Dict(map) => { let key = idx.as_string(); - map.lock().ok().and_then(|v| v.get(&key).cloned()).ok_or_else(|| self.err("Missing key", span)) + map.lock() + .ok() + .and_then(|v| v.get(&key).cloned()) + .ok_or_else(|| self.err("Missing key", span)) } _ => Err(self.err("Indexing not supported", span)), } @@ -1836,16 +2086,25 @@ impl Executor { Value::Array(arr) => { let i = to_i64(idx)? as usize; { - let mut list = arr.lock().map_err(|_| self.err("Array lock poisoned", Span { line: 0, col: 0 }))?; - if i >= list.len() { return Err(self.err("Index out of range", Span { line: 0, col: 0 })); } + let mut list = arr + .lock() + .map_err(|_| self.err("Array lock poisoned", Span { line: 0, col: 0 }))?; + if i >= list.len() { + return Err(self.err("Index out of range", Span { line: 0, col: 0 })); + } list[i] = val; } Ok(Value::Array(arr)) } - Value::Tuple(_) => Err(self.err("Cannot assign to tuple index (tuples are immutable)", Span { line: 0, col: 0 })), + Value::Tuple(_) => Err(self.err( + "Cannot assign to tuple index (tuples are immutable)", + Span { line: 0, col: 0 }, + )), Value::Dict(map) => { let key = idx.as_string(); - map.lock().map_err(|_| self.err("Dict lock poisoned", Span { line: 0, col: 0 }))?.insert(key, val); + map.lock() + .map_err(|_| self.err("Dict lock poisoned", Span { line: 0, col: 0 }))? + .insert(key, val); Ok(Value::Dict(map)) } _ => Err(self.err("Index assignment not supported", Span { line: 0, col: 0 })), @@ -1859,9 +2118,13 @@ impl Executor { Value::Range(a, b) => { let mut out = Vec::new(); if a <= b { - for i in a..=b { out.push(Value::Number(i as f64)); } + for i in a..=b { + out.push(Value::Number(i as f64)); + } } else { - for i in (b..=a).rev() { out.push(Value::Number(i as f64)); } + for i in (b..=a).rev() { + out.push(Value::Number(i as f64)); + } } Ok(out) } @@ -1888,7 +2151,12 @@ impl Executor { } } - fn exec_use(&mut self, module: &str, alias: Option<&str>, span: Span) -> Result<(), RuntimeError> { + fn exec_use( + &mut self, + module: &str, + alias: Option<&str>, + span: Span, + ) -> Result<(), RuntimeError> { let module_value = self.load_builtin_module(module, span)?; if let Some(alias_name) = alias { self.ensure_name_free(alias_name, span)?; @@ -1904,7 +2172,12 @@ impl Executor { if let Some(existing) = self.lookup(module) { match existing { Value::Module(existing_module) if Arc::ptr_eq(&existing_module, &module_value) => {} - _ => return Err(self.err(&format!("Name conflict for import/use binding '{}'", module), span)), + _ => { + return Err(self.err( + &format!("Name conflict for import/use binding '{}'", module), + span, + )); + } } } @@ -1927,17 +2200,23 @@ impl Executor { for item in items { let bind_name = item.alias.as_deref().unwrap_or(&item.name); self.ensure_name_free(bind_name, span)?; - let value = module - .exports - .get(&item.name) - .cloned() - .ok_or_else(|| self.err(&format!("Module '{}' has no export '{}'", module.name, item.name), span))?; + let value = module.exports.get(&item.name).cloned().ok_or_else(|| { + self.err( + &format!("Module '{}' has no export '{}'", module.name, item.name), + span, + ) + })?; self.define(bind_name, value); } Ok(()) } - fn exec_import_namespace(&mut self, alias: &str, source: &ImportSource, span: Span) -> Result<(), RuntimeError> { + fn exec_import_namespace( + &mut self, + alias: &str, + source: &ImportSource, + span: Span, + ) -> Result<(), RuntimeError> { self.ensure_name_free(alias, span)?; let module = self.load_module_from_source(source, span)?; self.define(alias, Value::Module(module)); @@ -1946,26 +2225,41 @@ impl Executor { fn ensure_name_free(&self, name: &str, span: Span) -> Result<(), RuntimeError> { if self.lookup(name).is_some() { - return Err(self.err(&format!("Name conflict for import/use binding '{}'", name), span)); + return Err(self.err( + &format!("Name conflict for import/use binding '{}'", name), + span, + )); } Ok(()) } - fn load_module_from_source(&mut self, source: &ImportSource, span: Span) -> Result, RuntimeError> { + fn load_module_from_source( + &mut self, + source: &ImportSource, + span: Span, + ) -> Result, RuntimeError> { match source { ImportSource::Builtin(name) => self.load_builtin_module(name, span), ImportSource::Path(path) => self.load_script_module(path, span), } } - fn load_builtin_module(&self, name: &str, span: Span) -> Result, RuntimeError> { + fn load_builtin_module( + &self, + name: &str, + span: Span, + ) -> Result, RuntimeError> { self.builtin_modules .get(name) .cloned() .ok_or_else(|| self.err(&format!("Unknown builtin module '{name}'"), span)) } - fn load_script_module(&mut self, raw_path: &str, span: Span) -> Result, RuntimeError> { + fn load_script_module( + &mut self, + raw_path: &str, + span: Span, + ) -> Result, RuntimeError> { let full = self.resolve_script_path(raw_path, span)?; let key = format!("script:{}", full.display()); @@ -2006,16 +2300,18 @@ impl Executor { self.loading_stack.push(key.clone()); let result = (|| -> Result, RuntimeError> { - let source = fs::read_to_string(&full) - .map_err(|e| self.err(&format!("Failed to read module {}: {e}", full.display()), span))?; + let source = fs::read_to_string(&full).map_err(|e| { + self.err( + &format!("Failed to read module {}: {e}", full.display()), + span, + ) + })?; let mut scanner = Scanner::new(&source); let tokens = scanner .scan_tokens() .map_err(|e| self.err(&format!("{}: {e}", full.display()), span))?; let mut parser = Parser::new(tokens, &source, full.to_string_lossy().to_string()); - let program = parser - .parse_program() - .map_err(|e| self.err(&e, span))?; + let program = parser.parse_program().map_err(|e| self.err(&e, span))?; let mut child = Executor { filename: full.to_string_lossy().to_string(), @@ -2070,7 +2366,8 @@ impl Executor { path.to_path_buf() } else { let base = if self.filename == "" || self.filename == "" { - std::env::current_dir().map_err(|e| self.err(&format!("Failed to resolve cwd: {e}"), span))? + std::env::current_dir() + .map_err(|e| self.err(&format!("Failed to resolve cwd: {e}"), span))? } else { Path::new(&self.filename) .parent() @@ -2081,7 +2378,10 @@ impl Executor { }; std::fs::canonicalize(&resolved).map_err(|e| { self.err( - &format!("Failed to resolve import path '{}': {e}", resolved.display()), + &format!( + "Failed to resolve import path '{}': {e}", + resolved.display() + ), span, ) }) @@ -2130,7 +2430,9 @@ impl Executor { fn lookup(&self, name: &str) -> Option { for scope in self.scopes.iter().rev() { - if let Some(v) = scope.get(name) { return Some(v.clone()); } + if let Some(v) = scope.get(name) { + return Some(v.clone()); + } } None } @@ -2145,7 +2447,10 @@ impl Executor { self.deferred_scopes.push(Vec::new()); } - fn exit_scope(&mut self, mut pending: Result) -> Result { + fn exit_scope( + &mut self, + mut pending: Result, + ) -> Result { let deferred_blocks = self.deferred_scopes.pop().unwrap_or_default(); for block in deferred_blocks.into_iter().rev() { match self.exec_block(&block) { @@ -2171,7 +2476,11 @@ impl Executor { } fn push_frame(&mut self, name: String, span: Span) { - self.stack.push(Frame { name, line: span.line, col: span.col }); + self.stack.push(Frame { + name, + line: span.line, + col: span.col, + }); } fn pop_frame(&mut self) { @@ -2189,7 +2498,14 @@ impl Executor { fn err(&self, message: &str, span: Span) -> RuntimeError { let span = if span.line == 0 { self.last_span } else { span }; let snippet = self.render_snippet(span); - RuntimeError::new(message.to_string(), self.filename.clone(), span.line, span.col, snippet, self.stack.clone()) + RuntimeError::new( + message.to_string(), + self.filename.clone(), + span.line, + span.col, + snippet, + self.stack.clone(), + ) } pub fn make_error(&self, message: &str, span: Span) -> RuntimeError { @@ -2198,7 +2514,9 @@ impl Executor { fn render_snippet(&self, span: Span) -> Option { let lines: Vec<&str> = self.source.lines().collect(); - if span.line == 0 || span.line > lines.len() { return None; } + if span.line == 0 || span.line > lines.len() { + return None; + } let line = lines[span.line - 1]; let col = if span.col == 0 { 1 } else { span.col }; let mut caret = String::new(); @@ -2236,7 +2554,12 @@ impl Executor { self.err(&msg, span) } - fn unknown_method_or_function_error(&self, name: &str, receiver: &Value, span: Span) -> RuntimeError { + fn unknown_method_or_function_error( + &self, + name: &str, + receiver: &Value, + span: Span, + ) -> RuntimeError { let ty = value_type_str(receiver); let mut candidates = self.known_method_names_for(receiver); candidates.extend(self.known_global_function_names()); @@ -2255,7 +2578,9 @@ impl Executor { let mut names: Vec = self.scopes[0] .iter() .filter_map(|(k, v)| match v { - Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) => Some(k.clone()), + Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) => { + Some(k.clone()) + } _ => None, }) .collect(); @@ -2270,7 +2595,10 @@ impl Executor { .into_iter() .map(str::to_string) .collect(), - Value::Array(_) => vec!["add", "count"].into_iter().map(str::to_string).collect(), + Value::Array(_) => vec!["add", "count"] + .into_iter() + .map(str::to_string) + .collect(), Value::Tuple(_) => vec!["count"].into_iter().map(str::to_string).collect(), Value::Dict(_) => vec!["count"].into_iter().map(str::to_string).collect(), Value::String(_) => vec![ @@ -2297,8 +2625,8 @@ impl Executor { .map(str::to_string) .collect(), Value::Number(_) => vec![ - "abs", "floor", "ceil", "round", "min", "max", "clamp", "pow", "sqrt", "toInt", "toFloat", - "isInt", "toString", + "abs", "floor", "ceil", "round", "min", "max", "clamp", "pow", "sqrt", "toInt", + "toFloat", "isInt", "toString", ] .into_iter() .map(str::to_string) @@ -2324,20 +2652,36 @@ enum GeneratorStep { fn to_f64(v: Value) -> Result { match v { Value::Number(n) => Ok(n), - _ => Err(RuntimeError::new("Expected number".to_string(), "".to_string(), 0, 0, None, Vec::new())), + _ => Err(RuntimeError::new( + "Expected number".to_string(), + "".to_string(), + 0, + 0, + None, + Vec::new(), + )), } } fn to_i64(v: Value) -> Result { match v { Value::Number(n) => Ok(n as i64), - _ => Err(RuntimeError::new("Expected number".to_string(), "".to_string(), 0, 0, None, Vec::new())), + _ => Err(RuntimeError::new( + "Expected number".to_string(), + "".to_string(), + 0, + 0, + None, + Vec::new(), + )), } } fn now_hms() -> String { use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); let secs = now.as_secs() % 86400; let h = secs / 3600; let m = (secs % 3600) / 60; @@ -2361,7 +2705,9 @@ fn parallelism_cap() -> usize { } } } - std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4) + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(4) } fn type_str(ty: &crate::parser::ast::TypeExpr) -> String { @@ -2401,7 +2747,10 @@ fn type_matches_with_resolver( "range" => Ok(matches!(value, Value::Range(_, _))), "task" => Ok(matches!(value, Value::Task(_))), "generator" => Ok(matches!(value, Value::Generator(_))), - "fn" => Ok(matches!(value, Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_))), + "fn" => Ok(matches!( + value, + Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) + )), other => { if record_defs.contains_key(other) { return Ok(record_type_name(value).as_deref() == Some(other)); @@ -2444,7 +2793,10 @@ fn type_matches_with_resolver( }, T::Map(k, v) => match value { Value::Dict(map) => { - let items = map.lock().map(|m| m.clone()).unwrap_or_else(|_| HashMap::new()); + let items = map + .lock() + .map(|m| m.clone()) + .unwrap_or_else(|_| HashMap::new()); for (key, val) in items { let key_val = Value::String(key); if !type_matches_with_resolver(k, &key_val, type_aliases, record_defs)? { @@ -2459,10 +2811,15 @@ fn type_matches_with_resolver( _ => Ok(false), }, T::Range(inner) => match value { - Value::Range(_, _) => type_matches_with_resolver(inner, &Value::Number(0.0), type_aliases, record_defs), + Value::Range(_, _) => { + type_matches_with_resolver(inner, &Value::Number(0.0), type_aliases, record_defs) + } _ => Ok(false), }, - T::Fn(_) => Ok(matches!(value, Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_))), + T::Fn(_) => Ok(matches!( + value, + Value::Function(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) + )), T::Union(items) => { for ty in items { if type_matches_with_resolver(ty, value, type_aliases, record_defs)? { @@ -2552,10 +2909,11 @@ fn stmt_contains_yield(stmt: &Stmt) -> bool { | Stmt::Throw { value: expr, .. } | Stmt::Invoke { expr, .. } => expr_contains_yield(expr), Stmt::IndexAssign { - target, index, expr, .. - } => { - expr_contains_yield(target) || expr_contains_yield(index) || expr_contains_yield(expr) - } + target, + index, + expr, + .. + } => expr_contains_yield(target) || expr_contains_yield(index) || expr_contains_yield(expr), Stmt::Return { value: None, .. } | Stmt::Continue { .. } | Stmt::Break { .. } @@ -2579,8 +2937,12 @@ fn expr_contains_yield(expr: &Expr) -> bool { CallArg::Named { value, .. } => expr_contains_yield(value), }) } - Expr::Member { object, .. } | Expr::NamespaceMember { object, .. } => expr_contains_yield(object), - Expr::Index { object, index, .. } => expr_contains_yield(object) || expr_contains_yield(index), + Expr::Member { object, .. } | Expr::NamespaceMember { object, .. } => { + expr_contains_yield(object) + } + Expr::Index { object, index, .. } => { + expr_contains_yield(object) || expr_contains_yield(index) + } Expr::Array { items, .. } | Expr::Tuple { items, .. } => { items.iter().any(expr_contains_yield) } @@ -2606,11 +2968,17 @@ fn expr_contains_yield(expr: &Expr) -> bool { then_branch, else_branch, .. - } => expr_contains_yield(cond) || block_contains_yield(then_branch) || block_contains_yield(else_branch), + } => { + expr_contains_yield(cond) + || block_contains_yield(then_branch) + || block_contains_yield(else_branch) + } Expr::Match { subject, arms, .. } => { expr_contains_yield(subject) || arms.iter().any(|arm| match &arm.kind { - MatchArmKind::Value(e) => expr_contains_yield(e) || block_contains_yield(&arm.body), + MatchArmKind::Value(e) => { + expr_contains_yield(e) || block_contains_yield(&arm.body) + } MatchArmKind::Compare { rhs, .. } => { expr_contains_yield(rhs) || block_contains_yield(&arm.body) } @@ -2635,7 +3003,9 @@ fn expr_contains_yield(expr: &Expr) -> bool { InterpPart::Expr(e) => expr_contains_yield(e), }), Expr::Sh { command, .. } => expr_contains_yield(command), - Expr::Ssh { host, command, .. } => expr_contains_yield(host) || expr_contains_yield(command), + Expr::Ssh { host, command, .. } => { + expr_contains_yield(host) || expr_contains_yield(command) + } Expr::Literal { .. } | Expr::Var { .. } => false, } } @@ -2654,7 +3024,11 @@ fn suggest_names(target: &str, candidates: &[String]) -> Vec { 2 } else { let d = levenshtein(&target_l, &c_l); - if d <= 2 { 3 + d } else { return None; } + if d <= 2 { + 3 + d + } else { + return None; + } }; Some((score, c.clone())) }) diff --git a/src/runtime/shell/result.rs b/src/runtime/shell/result.rs index 48c792d..d0bd17a 100644 --- a/src/runtime/shell/result.rs +++ b/src/runtime/shell/result.rs @@ -16,8 +16,14 @@ impl ShellResult { let mut map = HashMap::new(); map.insert("stdout".to_string(), Value::String(self.stdout.clone())); map.insert("stderr".to_string(), Value::String(self.stderr.clone())); - map.insert("exit_code".to_string(), Value::Number(self.exit_code as f64)); - map.insert("duration_ms".to_string(), Value::Number(self.duration.as_millis() as f64)); + map.insert( + "exit_code".to_string(), + Value::Number(self.exit_code as f64), + ); + map.insert( + "duration_ms".to_string(), + Value::Number(self.duration.as_millis() as f64), + ); Value::Dict(Arc::new(Mutex::new(map))) } } diff --git a/src/runtime/shell/sh.rs b/src/runtime/shell/sh.rs index 1db4657..cc9d583 100644 --- a/src/runtime/shell/sh.rs +++ b/src/runtime/shell/sh.rs @@ -12,9 +12,18 @@ pub fn run_sh(command: &str) -> Result { .map_err(|e| format!("Failed to run shell: {e}"))?; let duration = start.elapsed(); - let stdout = String::from_utf8_lossy(&output.stdout).trim_end().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim_end().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout) + .trim_end() + .to_string(); + let stderr = String::from_utf8_lossy(&output.stderr) + .trim_end() + .to_string(); let exit_code = output.status.code().unwrap_or(-1); - Ok(ShellResult { stdout, stderr, exit_code, duration }) + Ok(ShellResult { + stdout, + stderr, + exit_code, + duration, + }) } diff --git a/src/runtime/shell/ssh.rs b/src/runtime/shell/ssh.rs index 0d1ed3d..2f2d423 100644 --- a/src/runtime/shell/ssh.rs +++ b/src/runtime/shell/ssh.rs @@ -12,9 +12,18 @@ pub fn run_ssh(host: &str, command: &str) -> Result { .map_err(|e| format!("Failed to run ssh: {e}"))?; let duration = start.elapsed(); - let stdout = String::from_utf8_lossy(&output.stdout).trim_end().to_string(); - let stderr = String::from_utf8_lossy(&output.stderr).trim_end().to_string(); + let stdout = String::from_utf8_lossy(&output.stdout) + .trim_end() + .to_string(); + let stderr = String::from_utf8_lossy(&output.stderr) + .trim_end() + .to_string(); let exit_code = output.status.code().unwrap_or(-1); - Ok(ShellResult { stdout, stderr, exit_code, duration }) + Ok(ShellResult { + stdout, + stderr, + exit_code, + duration, + }) } diff --git a/src/runtime/value.rs b/src/runtime/value.rs index 612690f..c47c66b 100644 --- a/src/runtime/value.rs +++ b/src/runtime/value.rs @@ -29,14 +29,30 @@ pub enum Value { Dict(Arc>>), Range(i64, i64), Duration(Duration), - Nominal { name: String, inner: Box }, + Nominal { + name: String, + inner: Box, + }, Task(Arc), Generator(Arc), Function(Arc), Module(Arc), NativeFunction(Arc) -> Result + Send + Sync>), - NativeFunctionExec(Arc, crate::parser::ast::Span) -> Result + Send + Sync>), - Ufcs { name: String, receiver: Box }, + NativeFunctionExec( + Arc< + dyn Fn( + &mut crate::runtime::executor::Executor, + Vec, + crate::parser::ast::Span, + ) -> Result + + Send + + Sync, + >, + ), + Ufcs { + name: String, + receiver: Box, + }, } impl Value { @@ -49,12 +65,18 @@ impl Value { pub fn parse_duration(text: &str) -> Result { let mut idx = 0; for c in text.chars() { - if c.is_ascii_digit() { idx += 1; } else { break; } + if c.is_ascii_digit() { + idx += 1; + } else { + break; + } } if idx == 0 || idx >= text.len() { return Err("Invalid duration literal".to_string()); } - let value: u64 = text[..idx].parse().map_err(|_| "Invalid duration value".to_string())?; + let value: u64 = text[..idx] + .parse() + .map_err(|_| "Invalid duration value".to_string())?; let unit = &text[idx..]; let dur = match unit { "ms" => Duration::from_millis(value), @@ -81,7 +103,11 @@ impl Value { Value::Nominal { inner, .. } => inner.is_truthy(), Value::Task(_) => true, Value::Generator(_) => true, - Value::Function(_) | Value::Module(_) | Value::NativeFunction(_) | Value::NativeFunctionExec(_) | Value::Ufcs { .. } => true, + Value::Function(_) + | Value::Module(_) + | Value::NativeFunction(_) + | Value::NativeFunctionExec(_) + | Value::Ufcs { .. } => true, } } @@ -90,15 +116,25 @@ impl Value { Value::Null => "null".to_string(), Value::Bool(b) => b.to_string(), Value::Number(n) => { - if n.fract() == 0.0 { format!("{}", *n as i64) } else { n.to_string() } + if n.fract() == 0.0 { + format!("{}", *n as i64) + } else { + n.to_string() + } } Value::String(s) => s.clone(), Value::Array(a) => { - let items: Vec = a.lock().map(|v| v.iter().map(|x| x.as_string()).collect()).unwrap_or_else(|_| Vec::new()); + let items: Vec = a + .lock() + .map(|v| v.iter().map(|x| x.as_string()).collect()) + .unwrap_or_else(|_| Vec::new()); format!("[{}]", items.join(", ")) } Value::Tuple(t) => { - let items: Vec = t.lock().map(|v| v.iter().map(|x| x.as_string()).collect()).unwrap_or_else(|_| Vec::new()); + let items: Vec = t + .lock() + .map(|v| v.iter().map(|x| x.as_string()).collect()) + .unwrap_or_else(|_| Vec::new()); format!("({})", items.join(", ")) } Value::Dict(d) => { @@ -134,3 +170,60 @@ impl fmt::Debug for Value { write!(f, "{}", self.as_string()) } } + +#[cfg(test)] +mod tests { + use super::Value; + + #[test] + fn value_is_truthy() { + assert!(!Value::Null.is_truthy()); + assert!(!Value::Bool(false).is_truthy()); + assert!(Value::Bool(true).is_truthy()); + assert!(!Value::Number(0.0).is_truthy()); + assert!(Value::Number(1.0).is_truthy()); + assert!(!Value::String(String::new()).is_truthy()); + assert!(Value::String("x".to_string()).is_truthy()); + } + + #[test] + fn value_as_string() { + assert_eq!(Value::Null.as_string(), "null"); + assert_eq!(Value::Bool(true).as_string(), "true"); + assert_eq!(Value::Number(42.0).as_string(), "42"); + assert_eq!(Value::Number(3.14).as_string(), "3.14"); + assert_eq!(Value::String("hi".to_string()).as_string(), "hi"); + assert_eq!(Value::Range(1, 5).as_string(), "1..5"); + } + + #[test] + fn value_parse_duration() { + use std::time::Duration; + assert_eq!( + Value::parse_duration("100ms").unwrap(), + Duration::from_millis(100) + ); + assert_eq!(Value::parse_duration("2s").unwrap(), Duration::from_secs(2)); + assert!(Value::parse_duration("x").is_err()); + } + + #[test] + fn value_array_as_string() { + let arr = Value::array(vec![Value::Number(1.0), Value::Number(2.0)]); + assert_eq!(arr.as_string(), "[1, 2]"); + } + + #[test] + fn value_tuple_as_string() { + let tup = Value::tuple(vec![Value::Bool(true), Value::String("x".to_string())]); + assert_eq!(tup.as_string(), "(true, x)"); + } + + #[test] + fn value_empty_array_truthy() { + let empty = Value::array(vec![]); + assert!(!empty.is_truthy()); + let non_empty = Value::array(vec![Value::Null]); + assert!(non_empty.is_truthy()); + } +} diff --git a/tests/e2e.rs b/tests/e2e.rs new file mode 100644 index 0000000..823c389 --- /dev/null +++ b/tests/e2e.rs @@ -0,0 +1,72 @@ +//! End-to-end tests: run full scripts and assert on captured output. + +use v2r::run_script_capture; + +fn assert_output_contains(captured: &[String], needle: &str) { + let found = captured.iter().any(|line| line.contains(needle)); + assert!( + found, + "Expected output to contain {:?}, got: {:?}", + needle, captured + ); +} + +#[test] +fn e2e_log_literal() { + let out = run_script_capture("log 42").unwrap(); + assert_output_contains(&out, "42"); +} + +#[test] +fn e2e_log_string() { + let out = run_script_capture(r#"log "hello""#).unwrap(); + assert_output_contains(&out, "hello"); +} + +#[test] +fn e2e_arithmetic() { + let out = run_script_capture("log 1 + 2 * 3").unwrap(); + assert_output_contains(&out, "7"); // 1 + 6 +} + +#[test] +fn e2e_sort_builtin() { + let out = run_script_capture("log sort([3, 1, 2])").unwrap(); + assert_output_contains(&out, "[1, 2, 3]"); +} + +#[test] +fn e2e_filter_find() { + let out = run_script_capture("let nums = [5, 1, 2, 4, 3]; log find(nums, n -> n > 3)").unwrap(); + assert_output_contains(&out, "5"); +} + +#[test] +fn e2e_function_and_named_args() { + let src = r#" +fn greet(name, suffix = "!") { return "hello, #{name}#{suffix}" } +log greet("world") +log greet(name = "team", suffix = " :)") +"#; + let out = run_script_capture(src).unwrap(); + assert_output_contains(&out, "hello, world!"); + assert_output_contains(&out, "hello, team :)"); +} + +#[test] +fn e2e_interpolated_string() { + let out = run_script_capture(r#"log "result: #{1 + 2}""#).unwrap(); + assert_output_contains(&out, "result: 3"); +} + +#[test] +fn e2e_parse_error() { + let err = run_script_capture("let x = ").unwrap_err(); + assert!(err.contains("Expected") || err.to_lowercase().contains("parse") || err.len() > 0); +} + +#[test] +fn e2e_runtime_error_undefined_var() { + let err = run_script_capture("log nonexistent").unwrap_err(); + assert!(!err.is_empty()); +}