From 602d214b9648964c032cdd2b414b0c40e22c6414 Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:48:05 +0200 Subject: [PATCH] feat: expression static analysis (#449) * feat: expression static analysis * fix: cargo fmt * fix: python workflow * fix: python workflow * fix: python workflow * fix: ignore miri test bound by clock * fix: memory leaks --- .github/workflows/python.yaml | 9 +- .gitignore | 1 - Cargo.lock | 5039 +++++++++++++++++ core/engine/src/decision_graph/walker.rs | 5 +- core/engine/src/nodes/decision_table/mod.rs | 2 +- core/expression/Cargo.toml | 1 + core/expression/benches/lexer.rs | 4 +- core/expression/benches/standard.rs | 5 +- core/expression/benches/unary.rs | 5 +- core/expression/src/arena.rs | 44 - core/expression/src/functions/date_method.rs | 4 +- core/expression/src/functions/internal.rs | 2 +- .../expression/src/intellisense/completion.rs | 500 ++ .../expression/src/intellisense/dependency.rs | 846 +++ .../expression/src/intellisense/diagnostic.rs | 115 + .../src/intellisense/discriminant.rs | 734 +++ .../src/intellisense/entity_flow.rs | 205 + .../expression/src/intellisense/inspection.rs | 33 + core/expression/src/intellisense/mod.rs | 441 +- .../{types/provider.rs => type_provider.rs} | 552 +- core/expression/src/intellisense/types/mod.rs | 2 - .../src/intellisense/types/type_info.rs | 51 - core/expression/src/isolate.rs | 47 +- core/expression/src/lexer/lexer.rs | 44 +- core/expression/src/lib.rs | 3 +- core/expression/src/parser/constants.rs | 16 +- core/expression/src/parser/mod.rs | 1 + core/expression/src/vm/vm.rs | 6 + core/expression/tests/analysis.rs | 650 +++ .../tests/data/analysis/closures.toml | 262 + .../tests/data/analysis/completions.toml | 203 + .../tests/data/analysis/dependencies.toml | 343 ++ .../tests/data/analysis/diagnostics.toml | 315 ++ .../tests/data/analysis/edge_cases.toml | 81 + .../expression/tests/data/analysis/enums.toml | 327 ++ .../tests/data/analysis/functions.toml | 291 + .../tests/data/analysis/inspect.toml | 54 + .../tests/data/analysis/nullable.toml | 1053 ++++ .../tests/data/analysis/read_spans.toml | 116 + .../tests/data/analysis/references.toml | 154 + .../expression/tests/data/analysis/types.toml | 456 ++ .../expression/tests/data/analysis/unary.toml | 311 + core/expression/tests/isolate.rs | 28 + core/expression/tests/lexer.rs | 5 +- core/expression/tests/standard.rs | 23 +- core/expression/tests/unary.rs | 14 +- core/expression_repl/src/main.rs | 2 + core/macros/src/lib.rs | 2 + core/template/src/interpreter.rs | 2 +- core/template/src/lib.rs | 2 + core/types/src/decision/mod.rs | 9 +- core/types/src/lib.rs | 2 + core/types/src/variable/mod.rs | 13 + core/types/src/variable/ref_deser.rs | 2 +- core/types/src/variable/ref_ser.rs | 2 +- core/types/src/variable_type/conv.rs | 4 +- core/types/src/variable_type/mod.rs | 6 + core/types/src/variable_type/util.rs | 71 +- 58 files changed, 13225 insertions(+), 295 deletions(-) create mode 100644 Cargo.lock delete mode 100644 core/expression/src/arena.rs create mode 100644 core/expression/src/intellisense/completion.rs create mode 100644 core/expression/src/intellisense/dependency.rs create mode 100644 core/expression/src/intellisense/diagnostic.rs create mode 100644 core/expression/src/intellisense/discriminant.rs create mode 100644 core/expression/src/intellisense/entity_flow.rs create mode 100644 core/expression/src/intellisense/inspection.rs rename core/expression/src/intellisense/{types/provider.rs => type_provider.rs} (50%) delete mode 100644 core/expression/src/intellisense/types/mod.rs delete mode 100644 core/expression/src/intellisense/types/type_info.rs create mode 100644 core/expression/tests/analysis.rs create mode 100644 core/expression/tests/data/analysis/closures.toml create mode 100644 core/expression/tests/data/analysis/completions.toml create mode 100644 core/expression/tests/data/analysis/dependencies.toml create mode 100644 core/expression/tests/data/analysis/diagnostics.toml create mode 100644 core/expression/tests/data/analysis/edge_cases.toml create mode 100644 core/expression/tests/data/analysis/enums.toml create mode 100644 core/expression/tests/data/analysis/functions.toml create mode 100644 core/expression/tests/data/analysis/inspect.toml create mode 100644 core/expression/tests/data/analysis/nullable.toml create mode 100644 core/expression/tests/data/analysis/read_spans.toml create mode 100644 core/expression/tests/data/analysis/references.toml create mode 100644 core/expression/tests/data/analysis/types.toml create mode 100644 core/expression/tests/data/analysis/unary.toml diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml index d6fa1ee2..19ce47e6 100644 --- a/.github/workflows/python.yaml +++ b/.github/workflows/python.yaml @@ -39,14 +39,17 @@ jobs: python-version: '3.13' - name: Build wheels - uses: stefan-gorules/maturin-action@main + uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} args: --release --out dist --find-interpreter -m bindings/python/Cargo.toml manylinux: 2_28 before-script-linux: | - apt update - apt install -y patch + if command -v dnf > /dev/null; then + dnf install -y patch + elif command -v apt-get > /dev/null; then + apt-get update && apt-get install -y patch + fi - name: Upload wheels uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 71c7c0ed..c7f6e292 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .temp /target -/Cargo.lock .idea .env.docker diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..5fbb8b9b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5039 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[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 = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ar_archive_writer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.117", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", +] + +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "clap", + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.117", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[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 = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[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 = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "expression_repl" +version = "0.55.1" +dependencies = [ + "colored", + "rustyline", + "serde_json", + "zen-expression", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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 = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[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-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[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 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +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 = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "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 = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "pest", + "pest_derive", + "serde", + "similar", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json_dotpath" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a" +dependencies = [ + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonschema" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5" +dependencies = [ + "ahash 0.8.12", + "base64", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "reqwest 0.12.28", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "napi" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9" +dependencies = [ + "anyhow", + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "3.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42" +dependencies = [ + "convert_case 0.11.0", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "napi-derive-backend" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "semver", + "syn 2.0.117", +] + +[[package]] +name = "napi-sys" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" +dependencies = [ + "libloading 0.9.0", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[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 = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher 1.0.3", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2 0.10.9", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.6.4", + "spki", +] + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +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 = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[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 = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "anyhow", + "either", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "serde", + "unindent", +] + +[[package]] +name = "pyo3-async-runtimes" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73cc6b1b7d8b3cef02101d37390dbdfe7e450dfea14921cae80a9534ba59ef2" +dependencies = [ + "futures", + "once_cell", + "pin-project-lite", + "pyo3", + "pyo3-async-runtimes-macros", + "tokio", +] + +[[package]] +name = "pyo3-async-runtimes-macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca31e43a0f205f2960208938135e37e579e61e10b36b4e7f49b0e8f60fab5b83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pythonize" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597907139a488b22573158793aa7539df36ae863eba300c75f3a0d65fc475e27" +dependencies = [ + "pyo3", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", + "serde", +] + +[[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 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[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 = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "referencing" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d" +dependencies = [ + "ahash 0.8.12", + "fluent-uri", + "once_cell", + "parking_lot", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +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-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqsign" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "124e8be679553a5d8c52faa4e5169a8f6727ea8c8e4ecb20902a45d25e3b40ae" +dependencies = [ + "reqsign-aws-v4", + "reqsign-azure-storage", + "reqsign-command-execute-tokio", + "reqsign-core", + "reqsign-file-read-tokio", + "reqsign-google", + "reqsign-http-send-reqwest", +] + +[[package]] +name = "reqsign-aws-v4" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75624bd8a466e37ddc0a7b6c33ac859a85347c153a916e1dd9d0b68338f74a" +dependencies = [ + "anyhow", + "bytes", + "form_urlencoded", + "hex", + "http", + "log", + "percent-encoding", + "quick-xml", + "reqsign-core", + "rust-ini", + "serde", + "serde_json", + "serde_urlencoded", + "sha1", +] + +[[package]] +name = "reqsign-azure-storage" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b96928e73ad984de1d99e382749d09e5dab7dd707b767974f7e40aa926b82f" +dependencies = [ + "anyhow", + "base64", + "bytes", + "form_urlencoded", + "http", + "log", + "pem", + "percent-encoding", + "reqsign-core", + "rsa", + "serde", + "serde_json", + "sha1", +] + +[[package]] +name = "reqsign-command-execute-tokio" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13460892332fd1ae7bc344ca063379beae3f0db50e7066af72467c938c3eea9f" +dependencies = [ + "reqsign-core", + "tokio", +] + +[[package]] +name = "reqsign-core" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5fa5cb48808693614d1701fcd3db0b30fa292e0f18e122ae068b6d32eaeed3f" +dependencies = [ + "anyhow", + "base64", + "bytes", + "form_urlencoded", + "futures", + "hex", + "hmac 0.13.0", + "http", + "jiff", + "log", + "percent-encoding", + "rsa", + "serde", + "serde_json", + "sha1", + "sha2 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "reqsign-file-read-tokio" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a4b6f3a3fd29ffcc99a90aec585a65217783badfd73acddf847b63ae683bda9" +dependencies = [ + "anyhow", + "reqsign-core", + "tokio", +] + +[[package]] +name = "reqsign-google" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb215d0876a18b6bd9cdd380b589e5292aaa638ca15266de794b1122d898b6b2" +dependencies = [ + "form_urlencoded", + "http", + "log", + "percent-encoding", + "reqsign-aws-v4", + "reqsign-core", + "rsa", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "reqsign-http-send-reqwest" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6089b23d9ba77aa6a8e5cf38d11550da23458d1c455645ae59e52294ffe7d26f" +dependencies = [ + "anyhow", + "bytes", + "futures-channel", + "http", + "http-body-util", + "reqsign-core", + "reqwest 0.13.4", + "wasm-bindgen-futures", +] + +[[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 = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rquickjs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a135375fbac5ba723bb6a48f432a72f81539cedde422f0121a86c7c4e96d8e0d" +dependencies = [ + "either", + "rquickjs-core", + "rquickjs-macro", +] + +[[package]] +name = "rquickjs-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bccb7121a123865c8ace4dea42e7ed84d78b90cbaf4ca32c59849d8d210c9672" +dependencies = [ + "async-lock", + "either", + "hashbrown 0.16.1", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-macro" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f93602cc3112c7f30bf5f29e722784232138692c7df4c52ebbac7e035d900d" +dependencies = [ + "convert_case 0.8.0", + "fnv", + "ident_case", + "indexmap", + "proc-macro-crate", + "proc-macro2", + "quote", + "rquickjs-core", + "syn 2.0.117", +] + +[[package]] +name = "rquickjs-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b1b6528590d4d65dc86b5159eae2d0219709546644c66408b2441696d1d725" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a5a6f027e892c7a035c6fddb50435a1fbf5a734ffc0c2a9fed4d0221440519" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +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 = "rustyline" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_json5" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34d03f54462862f2a42918391c9526337f53171eaa4d8894562be7f252edd3" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[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 = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[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 = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[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.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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 2.0.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 2.0.117", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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 = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[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.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.5.11", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025a05cba02ee22b6624ac3d257e59c7395319ea8fe1aae33a7cdb4e2a3016cc" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "serde", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[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 = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zen-engine" +version = "0.55.1" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "async-trait", + "criterion", + "downcast-rs", + "fixedbitset", + "http", + "insta", + "json_dotpath", + "jsonschema", + "nohash-hasher", + "once_cell", + "petgraph", + "reqsign", + "reqwest 0.12.28", + "rquickjs", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.9", + "strum", + "thiserror 1.0.69", + "tokio", + "zen-expression", + "zen-tmpl", + "zen-types", +] + +[[package]] +name = "zen-expression" +version = "0.55.1" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "bumpalo", + "chrono", + "chrono-tz", + "criterion", + "csv", + "fastrand", + "humantime", + "iana-time-zone", + "nohash-hasher", + "once_cell", + "recursive", + "regex", + "regex-lite", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "serde_json5", + "strsim", + "strum", + "strum_macros", + "thiserror 1.0.69", + "toml 0.8.23", + "zen-macros", + "zen-types", +] + +[[package]] +name = "zen-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "cbindgen", + "libc", + "serde", + "serde_json", + "strum", + "tokio", + "zen-engine", + "zen-expression", + "zen-tmpl", +] + +[[package]] +name = "zen-macros" +version = "0.55.1" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "zen-nodejs" +version = "0.1.0" +dependencies = [ + "ahash 0.8.12", + "iana-time-zone", + "json_dotpath", + "napi", + "napi-build", + "napi-derive", + "napi-sys", + "serde", + "serde_json", + "tokio-util", + "zen-engine", + "zen-expression", + "zen-tmpl", +] + +[[package]] +name = "zen-python" +version = "0.53.0" +dependencies = [ + "anyhow", + "either", + "json_dotpath", + "pyo3", + "pyo3-async-runtimes", + "pythonize", + "rust_decimal", + "serde", + "serde_json", + "tokio-util", + "zen-engine", + "zen-expression", + "zen-tmpl", +] + +[[package]] +name = "zen-tmpl" +version = "0.55.1" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "zen-expression", +] + +[[package]] +name = "zen-types" +version = "0.55.1" +dependencies = [ + "ahash 0.8.12", + "nohash-hasher", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "zen-uniffi" +version = "0.7.2" +dependencies = [ + "async-trait", + "serde", + "serde_json", + "tokio", + "uniffi", + "zen-engine", + "zen-expression", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/core/engine/src/decision_graph/walker.rs b/core/engine/src/decision_graph/walker.rs index 9d8d88f0..48e21acd 100644 --- a/core/engine/src/decision_graph/walker.rs +++ b/core/engine/src/decision_graph/walker.rs @@ -252,10 +252,7 @@ impl GraphWalker { } } -fn switch_statement_evaluate<'a>( - isolate: &mut Isolate<'a>, - switch_statement: &'a SwitchStatement, -) -> bool { +fn switch_statement_evaluate(isolate: &mut Isolate, switch_statement: &SwitchStatement) -> bool { if switch_statement.condition.is_empty() { return true; } diff --git a/core/engine/src/nodes/decision_table/mod.rs b/core/engine/src/nodes/decision_table/mod.rs index 3ead1abc..d2b9f37b 100644 --- a/core/engine/src/nodes/decision_table/mod.rs +++ b/core/engine/src/nodes/decision_table/mod.rs @@ -109,7 +109,7 @@ impl DecisionTableNodeHandler { &self, ctx: &'a DecisionTableContext, rule: &'a HashMap, Arc>, - isolate: &mut Isolate<'a>, + isolate: &mut Isolate, ) -> Option { let content = &ctx.node; for input in content.inputs.iter() { diff --git a/core/expression/Cargo.toml b/core/expression/Cargo.toml index 5bf3aff3..6895bc28 100644 --- a/core/expression/Cargo.toml +++ b/core/expression/Cargo.toml @@ -36,6 +36,7 @@ zen-types = { path = "../types", version = "0.55.1" } criterion = { workspace = true } csv = "1" serde_json5 = "0.2" +toml = "0.8" [target.'cfg(not(target_family = "wasm"))'.dependencies] recursive = { workspace = true } diff --git a/core/expression/benches/lexer.rs b/core/expression/benches/lexer.rs index 1931081d..d64eea38 100644 --- a/core/expression/benches/lexer.rs +++ b/core/expression/benches/lexer.rs @@ -4,9 +4,11 @@ use zen_expression::lexer::Lexer; fn bench_source(b: &mut Bencher, source: &'static str) { let mut lexer = Lexer::new(); + let mut bump = bumpalo::Bump::new(); b.iter(|| { - criterion::black_box(lexer.tokenize(source).unwrap()); + bump.reset(); + criterion::black_box(lexer.tokenize(&bump, source).unwrap()); }) } diff --git a/core/expression/benches/standard.rs b/core/expression/benches/standard.rs index 9cdf15d9..980d6c56 100644 --- a/core/expression/benches/standard.rs +++ b/core/expression/benches/standard.rs @@ -6,12 +6,13 @@ use zen_expression::parser::Parser; fn bench_source(b: &mut Bencher, src: &'static str) { let mut lexer = Lexer::new(); + let token_bump = Bump::new(); let mut bump = Bump::new(); - let tokens = lexer.tokenize(src).unwrap(); + let tokens = lexer.tokenize(&token_bump, src).unwrap(); b.iter(|| { - let std_parser = Parser::try_new(tokens, &bump).unwrap().standard(); + let std_parser = Parser::try_new(&tokens, &bump).unwrap().standard(); criterion::black_box(std_parser.parse()); bump.reset(); diff --git a/core/expression/benches/unary.rs b/core/expression/benches/unary.rs index a6d4e5c4..fc8cd900 100644 --- a/core/expression/benches/unary.rs +++ b/core/expression/benches/unary.rs @@ -6,11 +6,12 @@ use zen_expression::parser::Parser; fn bench_source(b: &mut Bencher, src: &'static str) { let mut lexer = Lexer::new(); + let token_bump = Bump::new(); let mut bump = Bump::new(); - let tokens = lexer.tokenize(src).unwrap(); + let tokens = lexer.tokenize(&token_bump, src).unwrap(); b.iter(|| { - let unary_parser = Parser::try_new(tokens, &bump).unwrap().unary(); + let unary_parser = Parser::try_new(&tokens, &bump).unwrap().unary(); criterion::black_box(unary_parser.parse()); bump.reset(); diff --git a/core/expression/src/arena.rs b/core/expression/src/arena.rs deleted file mode 100644 index b12cc077..00000000 --- a/core/expression/src/arena.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::marker::PhantomData; - -use bumpalo::Bump; - -/// Structure used for self-referential arena (Bump). -/// Compared to regular reference, this prevents issues with retag caused by misaligned lifetimes -/// when trying to transmute. Bump is safely dropped once the owner struct is dropped. -#[derive(Debug)] -pub(crate) struct UnsafeArena<'arena> { - arena: *mut Bump, - _marker: PhantomData<&'arena Bump>, -} - -impl<'arena> UnsafeArena<'arena> { - pub fn new() -> Self { - let boxed = Box::new(Bump::new()); - let leaked = Box::leak(boxed); - - Self { - arena: leaked, - _marker: Default::default(), - } - } - - pub fn get(&self) -> &'arena Bump { - unsafe { &*self.arena } - } - - pub fn with_mut(&mut self, callback: F) - where - F: FnOnce(&mut Bump), - { - let reference = unsafe { &mut *self.arena }; - callback(reference); - } -} - -impl<'arena> Drop for UnsafeArena<'arena> { - fn drop(&mut self) { - unsafe { - let _ = Box::from_raw(self.arena); - } - } -} diff --git a/core/expression/src/functions/date_method.rs b/core/expression/src/functions/date_method.rs index a69a373d..d6fb950e 100644 --- a/core/expression/src/functions/date_method.rs +++ b/core/expression/src/functions/date_method.rs @@ -314,11 +314,11 @@ mod imp { signatures: vec![ FunctionSignature { parameters: vec![VT::Date, VT::Date], - return_type: VT::Date, + return_type: VT::Bool, }, FunctionSignature { parameters: vec![VT::Date, VT::Date, DurationUnit::variable_type()], - return_type: VT::Date, + return_type: VT::Bool, }, ], implementation: Rc::new(move |args: Arguments| -> anyhow::Result { diff --git a/core/expression/src/functions/internal.rs b/core/expression/src/functions/internal.rs index 6acb1d29..786bc0ed 100644 --- a/core/expression/src/functions/internal.rs +++ b/core/expression/src/functions/internal.rs @@ -491,7 +491,7 @@ pub(crate) mod imp { .sum(); match first { - V::Array(_) | V::Null => { + V::Array(_) => { let mut merged = Vec::with_capacity(capacity); for item in arr.iter() { diff --git a/core/expression/src/intellisense/completion.rs b/core/expression/src/intellisense/completion.rs new file mode 100644 index 00000000..9fd68bb3 --- /dev/null +++ b/core/expression/src/intellisense/completion.rs @@ -0,0 +1,500 @@ +use crate::functions::registry::FunctionRegistry; +use crate::functions::DateMethod; +use crate::functions::{ + ClosureFunction, DeprecatedFunction, FunctionKind, InternalFunction, MethodKind, MethodRegistry, +}; +use crate::intellisense::IntelliSenseToken; +use crate::variable::VariableType; +use serde::Serialize; +use strum::IntoEnumIterator; + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum CompletionKind { + Variable, + Function, + Method, + Property, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Completion { + pub label: String, + pub kind: CompletionKind, + pub detail: String, + pub info: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub boost: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub method_for: Option, +} + +pub struct Completions; + +impl Completions { + pub fn build( + source: &str, + pos: u32, + data: &VariableType, + tokens: &[IntelliSenseToken], + ) -> Vec { + let before = source.get(..pos as usize).unwrap_or(source); + let prefix = Self::extract_prefix(before); + + let completions = match Self::find_property_dot(before) { + Some(dot) => { + let target_type = tokens + .iter() + .filter(|t| t.span.1 <= dot as u32 && t.span.1 > 0) + .max_by(|a, b| { + a.span + .1 + .cmp(&b.span.1) + .then_with(|| (a.span.1 - a.span.0).cmp(&(b.span.1 - b.span.0))) + }) + .map(|t| t.kind.clone()) + .unwrap_or(VariableType::Any); + + Self::build_property(&target_type) + } + None => Self::build_scope(data), + }; + + Self::filter(completions, prefix) + } + + pub fn build_property(vt: &VariableType) -> Vec { + let mut completions = Vec::new(); + let resolved = match vt { + VariableType::Nullable(inner) => inner.as_ref(), + other => other, + }; + + if let VariableType::Object(obj) = resolved { + let obj = obj.borrow(); + for (key, val) in obj.iter() { + completions.push(Completion { + label: key.to_string(), + kind: CompletionKind::Property, + detail: val.to_string(), + info: String::new(), + boost: Some(10), + method_for: None, + }); + } + } + + for mk in DateMethod::iter().map(MethodKind::DateMethod) { + let def = MethodRegistry::get_definition(&mk); + let applies = def + .as_ref() + .and_then(|d| d.param_type(0)) + .map(|pt| vt.satisfies(&pt)) + .unwrap_or(false); + + if applies || matches!(vt, VariableType::Any) { + completions.push(Self::method(mk)); + } + } + + completions + } + + pub fn build_scope(data: &VariableType) -> Vec { + let mut completions = Vec::new(); + + let resolved_data = match data { + VariableType::Nullable(inner) => inner.as_ref(), + other => other, + }; + + if let VariableType::Object(obj) = resolved_data { + let obj = obj.borrow(); + for (key, val) in obj.iter() { + completions.push(Completion { + label: key.to_string(), + kind: CompletionKind::Variable, + detail: val.to_string(), + info: String::new(), + boost: Some(20), + method_for: None, + }); + } + } + + completions.push(Completion { + label: "$root".to_string(), + kind: CompletionKind::Variable, + detail: "Root variable".to_string(), + info: String::new(), + boost: Some(-10), + method_for: None, + }); + + completions.extend( + InternalFunction::iter() + .map(FunctionKind::Internal) + .chain(ClosureFunction::iter().map(FunctionKind::Closure)) + .map(|fk| Self::function(fk, None)), + ); + + completions + } + + fn function(fk: FunctionKind, boost_override: Option) -> Completion { + let label = fk.to_string(); + let info = function_info(&fk); + let detail = function_signature(&fk); + let boost = boost_override.or(match &fk { + FunctionKind::Internal(_) => Some(10), + FunctionKind::Closure(_) => None, + FunctionKind::Deprecated(_) => Some(-20), + }); + + Completion { + label, + kind: CompletionKind::Function, + detail, + info, + boost, + method_for: None, + } + } + + fn method(mk: MethodKind) -> Completion { + let label = mk.to_string(); + let info = method_info(&mk); + let (detail, method_for) = method_signature(&mk); + + Completion { + label, + kind: CompletionKind::Method, + detail, + info, + boost: None, + method_for, + } + } + + fn extract_prefix(before_cursor: &str) -> &str { + let boundary = before_cursor + .rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '$' && c != '#') + .map(|i| i + 1) + .unwrap_or(0); + + &before_cursor[boundary..] + } + + fn filter(completions: Vec, prefix: &str) -> Vec { + if prefix.is_empty() { + return completions; + } + + let prefix_lower = prefix.to_lowercase(); + completions + .into_iter() + .filter(|c| c.label.to_lowercase().starts_with(&prefix_lower)) + .collect() + } + + fn find_property_dot(before_cursor: &str) -> Option { + let trimmed = before_cursor.trim_end(); + if trimmed.ends_with('.') { + return Some(trimmed.len() - 1); + } + + let word_start = trimmed.rfind(|c: char| !c.is_alphanumeric() && c != '_' && c != '#'); + match word_start { + Some(i) if trimmed.as_bytes().get(i) == Some(&b'.') => Some(i), + _ => None, + } + } +} + +fn function_info(fk: &FunctionKind) -> String { + let s = match fk { + FunctionKind::Internal(i) => match i { + InternalFunction::Len => "Returns the length of variable", + InternalFunction::Contains => "Checks if variable contains a needle", + InternalFunction::Flatten => "Flattens an array", + InternalFunction::Upper => "Converts all characters in a string to uppercase", + InternalFunction::Lower => "Converts all characters in a string to lowercase", + InternalFunction::Trim => { + "Returns the string with leading and trailing whitespace removed" + } + InternalFunction::StartsWith => { + "Returns true if the string starts with the specified prefix" + } + InternalFunction::EndsWith => { + "Returns true if the string ends with the specified suffix" + } + InternalFunction::Matches => "Returns true if the string matches the specified pattern", + InternalFunction::Extract => "Extracts matching substrings according to a pattern", + InternalFunction::FuzzyMatch => "Performs a fuzzy search of the needle in the haystack", + InternalFunction::Split => { + "Splits a string into an array of substrings using the specified delimiter" + } + InternalFunction::Abs => "Returns the absolute value of a number", + InternalFunction::Sum => "Returns the sum of all elements in the input array", + InternalFunction::Avg => "Calculates the average of all elements in the input array", + InternalFunction::Min => "Returns the smallest of the elements in the input array", + InternalFunction::Max => "Returns the largest of the elements in the input array", + InternalFunction::Rand => { + "Generates a random number between 0 (inclusive) and max (inclusive)" + } + InternalFunction::Median => { + "Calculates the median value of all elements in the input array" + } + InternalFunction::Mode => "Finds the mode(s) of the input array", + InternalFunction::Floor => "Rounds a number down to the nearest integer", + InternalFunction::Ceil => "Rounds a number up to the nearest integer", + InternalFunction::Round => "Rounds a number to a specified number of decimal places", + InternalFunction::Trunc => "Truncates a number to a specified number of decimal places", + InternalFunction::IsNumeric => "Checks if the given value is of a numeric type", + InternalFunction::String => "Converts the given value to a string", + InternalFunction::Number => "Converts the given value to a number", + InternalFunction::Bool => "Converts the given value to a boolean", + InternalFunction::Type => "Returns a string representing the data type of the value", + InternalFunction::Keys => { + "Returns an array of a given object's own enumerable property names" + } + InternalFunction::Values => { + "Returns an array of a given object's own enumerable property values" + } + InternalFunction::Date => "Returns a new date time instance", + InternalFunction::Merge => "Merges multiple objects into one", + InternalFunction::MergeDeep => "Deeply merges multiple objects into one", + }, + FunctionKind::Deprecated(d) => match d { + DeprecatedFunction::Date => "Converts a numeric timestamp to a unix timestamp", + DeprecatedFunction::Time => "Extracts the time from a numeric timestamp", + DeprecatedFunction::Duration => "Parses a duration string (e.g. 1h30min)", + DeprecatedFunction::Year => "Extracts the year from a given timestamp", + DeprecatedFunction::DayOfWeek => "Gets the day of the week from a given timestamp", + DeprecatedFunction::DayOfMonth => { + "Extracts the day of the month from a given timestamp" + } + DeprecatedFunction::DayOfYear => "Gets the day of the year from a given timestamp", + DeprecatedFunction::WeekOfYear => { + "Calculates the week of the year from a given timestamp" + } + DeprecatedFunction::MonthOfYear => "Extracts the month from a given timestamp", + DeprecatedFunction::MonthString => { + "Converts the month from a given timestamp into its string representation" + } + DeprecatedFunction::DateString => { + "Converts a timestamp to a human-readable date string" + } + DeprecatedFunction::WeekdayString => { + "Converts the day of the week into its string representation" + } + DeprecatedFunction::StartOf => { + "Returns the timestamp representing the start of a specified unit" + } + DeprecatedFunction::EndOf => { + "Returns the timestamp representing the end of a specified unit" + } + }, + FunctionKind::Closure(c) => match c { + ClosureFunction::All => "Checks if all elements in the array satisfy the condition", + ClosureFunction::None => "Checks if no elements in the array satisfy the condition", + ClosureFunction::Some => "Checks if at least one element satisfies the condition", + ClosureFunction::One => "Checks if exactly one element satisfies the condition", + ClosureFunction::Filter => { + "Creates a new array with elements that satisfy the condition" + } + ClosureFunction::Map => "Creates a new array by transforming each element", + ClosureFunction::FlatMap => "Maps each element then flattens the result", + ClosureFunction::Count => "Counts elements that satisfy the condition", + }, + }; + s.to_string() +} + +fn function_param_names(fk: &FunctionKind) -> Vec<&'static str> { + match fk { + FunctionKind::Internal(i) => match i { + InternalFunction::Len => vec!["var"], + InternalFunction::Contains => vec!["haystack", "needle"], + InternalFunction::Flatten => vec!["arr"], + InternalFunction::Upper | InternalFunction::Lower | InternalFunction::Trim => { + vec!["str"] + } + InternalFunction::StartsWith => vec!["str", "prefix"], + InternalFunction::EndsWith => vec!["str", "suffix"], + InternalFunction::Matches | InternalFunction::Extract => vec!["str", "pattern"], + InternalFunction::FuzzyMatch => vec!["haystack", "needle"], + InternalFunction::Split => vec!["str", "delimiter"], + InternalFunction::Abs | InternalFunction::Floor | InternalFunction::Ceil => { + vec!["num"] + } + InternalFunction::Sum + | InternalFunction::Avg + | InternalFunction::Min + | InternalFunction::Max + | InternalFunction::Median + | InternalFunction::Mode => vec!["arr"], + InternalFunction::Rand => vec!["max"], + InternalFunction::Round | InternalFunction::Trunc => vec!["num", "digits"], + InternalFunction::IsNumeric + | InternalFunction::String + | InternalFunction::Number + | InternalFunction::Bool + | InternalFunction::Type => vec!["value"], + InternalFunction::Keys | InternalFunction::Values => vec!["obj"], + InternalFunction::Date => vec!["dateOrTimezone", "timezone"], + InternalFunction::Merge | InternalFunction::MergeDeep => vec!["objects"], + }, + FunctionKind::Deprecated(d) => match d { + DeprecatedFunction::Date + | DeprecatedFunction::Time + | DeprecatedFunction::Year + | DeprecatedFunction::DayOfWeek + | DeprecatedFunction::DayOfMonth + | DeprecatedFunction::DayOfYear + | DeprecatedFunction::WeekOfYear + | DeprecatedFunction::MonthOfYear + | DeprecatedFunction::MonthString + | DeprecatedFunction::DateString + | DeprecatedFunction::WeekdayString => vec!["timestamp"], + DeprecatedFunction::Duration => vec!["duration"], + DeprecatedFunction::StartOf | DeprecatedFunction::EndOf => vec!["timestamp", "unit"], + }, + FunctionKind::Closure(_) => vec![], + } +} + +fn function_signature(fk: &FunctionKind) -> String { + match fk { + FunctionKind::Internal(_) | FunctionKind::Deprecated(_) => { + let param_names = function_param_names(fk); + let Some(definition) = FunctionRegistry::get_definition(fk) else { + return String::new(); + }; + + let required = definition.required_parameters(); + let total = required + definition.optional_parameters(); + let params: Vec = (0..total) + .map(|i| { + let name = param_names.get(i).copied().unwrap_or("var"); + let optional = if i >= required { "?" } else { "" }; + let typ = definition.param_type_str(i); + format!("{name}{optional}: {typ}") + }) + .collect(); + + format!( + "({}) -> {}", + params.join(", "), + definition.return_type_str() + ) + } + FunctionKind::Closure(c) => match c { + ClosureFunction::All + | ClosureFunction::None + | ClosureFunction::Some + | ClosureFunction::One => { + "(array: T[], callback: Callback) -> boolean".to_string() + } + ClosureFunction::Filter => { + "(array: T[], callback: Callback) -> T[]".to_string() + } + ClosureFunction::Map => { + "(array: T[], callback: Callback) -> U[]".to_string() + } + ClosureFunction::FlatMap => { + "(array: T[], callback: Callback) -> U[]".to_string() + } + ClosureFunction::Count => { + "(array: T[], callback: Callback) -> number".to_string() + } + }, + } +} + +fn method_info(mk: &MethodKind) -> String { + let s = match mk { + MethodKind::DateMethod(dm) => match dm { + DateMethod::Add => "Adds time to a date", + DateMethod::Sub => "Subtracts time from a date", + DateMethod::Set => "Sets a specific unit of time on a date", + DateMethod::Format => "Formats a date into a string representation", + DateMethod::StartOf => "Returns the start of a specified time unit", + DateMethod::EndOf => "Returns the end of a specified time unit", + DateMethod::Diff => "Calculates the difference between two dates", + DateMethod::Tz => "Converts a date to a different timezone", + DateMethod::IsSame => "Checks if two dates are the same", + DateMethod::IsBefore => "Checks if a date is before another date", + DateMethod::IsAfter => "Checks if a date is after another date", + DateMethod::IsSameOrBefore => "Checks if a date is the same as or before another", + DateMethod::IsSameOrAfter => "Checks if a date is the same as or after another", + DateMethod::Second => "Gets the seconds of a date", + DateMethod::Minute => "Gets the minutes of a date", + DateMethod::Hour => "Gets the hours of a date", + DateMethod::Day => "Gets the day of the month", + DateMethod::DayOfYear => "Gets the day of the year", + DateMethod::Week => "Gets the week of the year", + DateMethod::Weekday => "Gets the day of the week", + DateMethod::Month => "Gets the month", + DateMethod::Quarter => "Gets the quarter", + DateMethod::Year => "Gets the year", + DateMethod::Timestamp => "Gets the Unix timestamp", + DateMethod::OffsetName => "Gets the timezone offset name", + DateMethod::IsValid => "Checks if a date is valid", + DateMethod::IsYesterday => "Checks if a date is yesterday", + DateMethod::IsToday => "Checks if a date is today", + DateMethod::IsTomorrow => "Checks if a date is tomorrow", + DateMethod::IsLeapYear => "Checks if the year is a leap year", + }, + }; + s.to_string() +} + +fn method_param_names(mk: &MethodKind) -> Vec<&'static str> { + match mk { + MethodKind::DateMethod(dm) => match dm { + DateMethod::Add | DateMethod::Sub => vec!["amount", "unit"], + DateMethod::Set => vec!["value", "unit"], + DateMethod::Format => vec!["format"], + DateMethod::StartOf | DateMethod::EndOf => vec!["unit"], + DateMethod::Diff => vec!["otherDate", "unit"], + DateMethod::Tz => vec!["timezone"], + DateMethod::IsSame + | DateMethod::IsBefore + | DateMethod::IsAfter + | DateMethod::IsSameOrBefore + | DateMethod::IsSameOrAfter => vec!["otherDate", "unit"], + _ => vec![], + }, + } +} + +fn method_signature(mk: &MethodKind) -> (String, Option) { + let Some(definition) = MethodRegistry::get_definition(mk) else { + return (String::new(), None); + }; + + let param_names = method_param_names(mk); + let method_for = definition.param_type(0); + let required = definition.required_parameters(); + let total = required + definition.optional_parameters(); + + let params: Vec = (1..total) + .map(|i| { + let name = param_names.get(i - 1).copied().unwrap_or("var"); + let optional = if i >= required { "?" } else { "" }; + let typ = definition.param_type_str(i); + format!("{name}{optional}: {typ}") + }) + .collect(); + + ( + format!( + "({}) -> {}", + params.join(", "), + definition.return_type_str() + ), + method_for, + ) +} diff --git a/core/expression/src/intellisense/dependency.rs b/core/expression/src/intellisense/dependency.rs new file mode 100644 index 00000000..df39a64d --- /dev/null +++ b/core/expression/src/intellisense/dependency.rs @@ -0,0 +1,846 @@ +use crate::functions::{ClosureFunction, FunctionKind}; +use crate::lexer::{LogicalOperator, Operator}; +use crate::parser::{Node, NodeMetadata}; +use crate::variable::Variable; +use ahash::HashSet; +use nohash_hasher::BuildNoHashHasher; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ReadDependency { + Direct { + path: Vec>, + #[serde(default)] + span: (u32, u32), + #[serde(default, skip_serializing_if = "is_false")] + via_index: bool, + }, + Iteration { + collection: Vec>, + #[serde(default)] + span: (u32, u32), + #[serde(skip_serializing_if = "Option::is_none")] + alias: Option>, + reads: Vec, + }, + Unresolved { + path: Vec>, + #[serde(default)] + span: (u32, u32), + }, +} + +fn is_false(b: &bool) -> bool { + !*b +} + +impl ReadDependency { + pub fn without_spans(&self) -> Self { + match self { + ReadDependency::Direct { + path, via_index, .. + } => ReadDependency::Direct { + path: path.clone(), + span: (0, 0), + via_index: *via_index, + }, + ReadDependency::Iteration { + collection, + alias, + reads, + .. + } => ReadDependency::Iteration { + collection: collection.clone(), + span: (0, 0), + alias: alias.clone(), + reads: reads.iter().map(|r| r.without_spans()).collect(), + }, + ReadDependency::Unresolved { path, .. } => ReadDependency::Unresolved { + path: path.clone(), + span: (0, 0), + }, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Reference { + pub path: Vec>, + pub spans: Vec<(u32, u32)>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub via_alias: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub via_index: Option>>, +} + +impl Reference { + pub fn without_via_alias(&self) -> Self { + Self { + path: self.path.clone(), + spans: self.spans.clone(), + via_alias: None, + via_index: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AliasBinding { + pub alias: Rc, + pub collection: Vec>, +} + +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct DependencyResult { + pub reads: Vec, + pub references: Vec, +} + +type MetadataMap = HashMap>; + +enum ChainSegment<'a> { + Field { name: Rc, prop: &'a Node<'a> }, + Dynamic { prop: &'a Node<'a> }, +} + +struct FlatChain<'a> { + root: &'a Node<'a>, + segments: Vec>, +} + +pub(crate) struct DependencyResolutionWalker<'a> { + reads: Vec, + references: Vec, + metadata: &'a MetadataMap, +} + +#[derive(Debug, Clone, Default)] +struct Scope { + locals: HashSet>, + bindings: Vec>>, + aliases: HashMap, Vec>>, + unresolved_aliases: HashSet>, + pointer_collection: Option>>, +} + +impl Scope { + fn is_local(&self, path: &[Rc]) -> bool { + if path.is_empty() { + return false; + } + + if self.locals.contains(&path[0]) { + return true; + } + + self.bindings + .iter() + .any(|b| path.len() >= b.len() && path.iter().zip(b.iter()).all(|(a, b)| a == b)) + } + + fn alias_binding_for(&self, path: &[Rc]) -> Option { + let root = path.first()?; + let collection = self.aliases.get(root)?; + + Some(AliasBinding { + alias: root.clone(), + collection: collection.clone(), + }) + } + + fn expand_alias_root(&self, path: &[Rc]) -> Vec> { + match path.first().and_then(|root| self.aliases.get(root)) { + Some(prefix) => prefix.iter().chain(path.iter().skip(1)).cloned().collect(), + None => path.to_vec(), + } + } +} + +fn node_address(node: &Node) -> usize { + node as *const Node as usize +} + +impl<'a> DependencyResolutionWalker<'a> { + pub fn walk(root: &Node, metadata: &'a MetadataMap) -> DependencyResult { + Self::walk_with_locals(root, metadata, &[]) + } + + pub fn walk_with_locals( + root: &Node, + metadata: &'a MetadataMap, + locals: &[&str], + ) -> DependencyResult { + let mut walker = Self { + reads: Vec::new(), + references: Vec::new(), + metadata, + }; + + let mut scope = Scope::default(); + scope.locals.extend(locals.iter().map(|l| Rc::from(*l))); + walker.resolve(root, &mut scope); + + DependencyResult { + reads: walker.reads, + references: walker.references, + } + } + + pub fn field_dependencies( + root: &Node, + metadata: &'a MetadataMap, + field_path: &[&str], + ) -> Option> { + let mut bindings: Vec<(Rc, Vec>)> = Vec::new(); + let mut values: Vec<&Node> = Vec::new(); + Self::navigate_field(root, field_path, &mut bindings, &mut values); + if values.is_empty() { + return None; + } + + let mut reads: Vec = Vec::new(); + for value in values { + let (r, _refs) = Self::walk_inner(value, &mut Scope::default(), metadata); + reads.extend(r); + } + + let mut wrapped = reads; + for (alias, collection) in bindings.into_iter().rev() { + wrapped = vec![ReadDependency::Iteration { + collection, + span: (0, 0), + alias: Some(alias), + reads: wrapped, + }]; + } + Some(wrapped) + } + + fn navigate_field<'n>( + node: &'n Node<'n>, + field_path: &[&str], + bindings: &mut Vec<(Rc, Vec>)>, + out: &mut Vec<&'n Node<'n>>, + ) { + match node { + Node::Parenthesized(inner) => Self::navigate_field(inner, field_path, bindings, out), + Node::Assignments { + output: Some(output), + .. + } => Self::navigate_field(output, field_path, bindings, out), + Node::Conditional { + on_true, on_false, .. + } => { + Self::navigate_field(on_true, field_path, bindings, out); + Self::navigate_field(on_false, field_path, bindings, out); + } + Node::FunctionCall { + kind: FunctionKind::Closure(ClosureFunction::Map | ClosureFunction::FlatMap), + arguments, + } if arguments.len() >= 2 => { + if let Node::Closure { body, alias } = arguments[1] { + if let (Some(a), Some(src)) = + (alias, Self::collection_source_path(arguments[0])) + { + bindings.push((Rc::from(*a), src)); + } + Self::navigate_field(body, field_path, bindings, out); + } + } + Node::Object(pairs) => { + let Some((first, rest)) = field_path.split_first() else { + return; + }; + let value = pairs.iter().find_map(|(k, v)| match k { + Node::String(s) if s == first => Some(*v), + _ => None, + }); + if let Some(value) = value { + if rest.is_empty() { + out.push(value); + } else { + Self::navigate_field(value, rest, bindings, out); + } + } + } + _ => {} + } + } + + fn walk_inner( + node: &Node, + scope: &mut Scope, + metadata: &'a MetadataMap, + ) -> (Vec, Vec) { + let mut walker = Self { + reads: Vec::new(), + references: Vec::new(), + metadata, + }; + walker.resolve(node, scope); + (walker.reads, walker.references) + } + + fn node_span(&self, node: &Node) -> (u32, u32) { + node.span() + .or_else(|| self.metadata.get(&node_address(node)).map(|m| m.span)) + .unwrap_or_default() + } + + fn extract_path_with_spans(&self, node: &Node) -> Option<(Vec>, Vec<(u32, u32)>)> { + match node { + Node::Identifier(name) => Some((vec![Rc::from(*name)], vec![self.node_span(node)])), + Node::Root => Some((vec![Variable::root_key()], vec![self.node_span(node)])), + Node::Member { node: n, property } => { + let (mut path, mut spans) = self.extract_path_with_spans(n)?; + match property { + Node::String(key) => { + path.push(Rc::from(*key)); + spans.push(self.node_span(property)); + Some((path, spans)) + } + _ => None, + } + } + _ => None, + } + } + + fn collection_source_path(node: &Node) -> Option>> { + match node { + Node::Identifier(name) => Some(vec![Rc::from(*name)]), + Node::Root => Some(vec![Variable::root_key()]), + Node::Member { .. } => Self::extract_read_path(node), + Node::Parenthesized(inner) => Self::collection_source_path(inner), + Node::Binary { + left, + operator: Operator::Logical(LogicalOperator::NullishCoalescing), + .. + } => Self::collection_source_path(left), + Node::FunctionCall { + kind: FunctionKind::Closure(ClosureFunction::Filter), + arguments, + } if !arguments.is_empty() => Self::collection_source_path(arguments[0]), + _ => None, + } + } + + fn flatten_member_chain<'n>(node: &'n Node<'n>) -> Option> { + let mut segments: Vec> = Vec::new(); + let mut current = node; + loop { + match current { + Node::Member { node: n, property } => { + segments.push(match property { + Node::String(key) => ChainSegment::Field { + name: Rc::from(*key), + prop: property, + }, + _ => ChainSegment::Dynamic { prop: property }, + }); + current = n; + } + Node::Identifier(_) | Node::Root | Node::Pointer => { + segments.reverse(); + return Some(FlatChain { + root: current, + segments, + }); + } + _ => return None, + } + } + } + + fn resolve_member_chain(&mut self, node: &Node, scope: &mut Scope) { + let Some(chain) = Self::flatten_member_chain(node) else { + if let Node::Member { node: n, property } = node { + self.resolve(n, scope); + self.resolve(property, scope); + } + + return; + }; + + let root_name: Rc = match chain.root { + Node::Identifier(name) if *name == "$" => { + self.resolve_pointer_chain(&chain, scope); + return; + } + Node::Identifier(name) => Rc::from(*name), + Node::Root => Variable::root_key(), + Node::Pointer => { + self.resolve_pointer_chain(&chain, scope); + return; + } + _ => return, + }; + let root_span = self.node_span(chain.root); + + if scope.unresolved_aliases.contains(&root_name) { + let mut path = vec![root_name.clone()]; + let mut spans = vec![root_span]; + for segment in &chain.segments { + match segment { + ChainSegment::Field { name, prop } => { + path.push(name.clone()); + spans.push(self.node_span(prop)); + } + ChainSegment::Dynamic { prop } => self.resolve(prop, scope), + } + } + let span = match (spans.first(), spans.last()) { + (Some(first), Some(last)) => (first.0, last.1), + _ => Default::default(), + }; + self.reads.push(ReadDependency::Unresolved { + path: path.clone(), + span, + }); + self.references.push(Reference { + path, + spans, + via_alias: None, + via_index: None, + }); + + return; + } + + let via_alias_first = + scope + .aliases + .get(&root_name) + .cloned() + .map(|collection| AliasBinding { + alias: root_name.clone(), + collection, + }); + + if via_alias_first.is_none() && scope.is_local(std::slice::from_ref(&root_name)) { + for segment in &chain.segments { + if let ChainSegment::Dynamic { prop } = segment { + self.resolve(prop, scope); + } + } + return; + } + + let mut group_path: Vec> = vec![root_name.clone()]; + let mut group_spans: Vec<(u32, u32)> = vec![root_span]; + let mut cumulative: Vec> = vec![root_name]; + let mut via_index_for_group: Option>> = None; + let mut is_first_group = true; + + for segment in &chain.segments { + match segment { + ChainSegment::Field { name, prop } => { + group_path.push(name.clone()); + group_spans.push(self.node_span(prop)); + cumulative.push(name.clone()); + } + ChainSegment::Dynamic { prop } => { + let emittable = is_first_group || !group_path.is_empty(); + if emittable && !group_path.is_empty() { + let via_alias = if is_first_group { + via_alias_first.clone() + } else { + None + }; + self.emit_group( + std::mem::take(&mut group_path), + std::mem::take(&mut group_spans), + via_alias, + via_index_for_group.clone(), + scope, + ); + } + group_path.clear(); + group_spans.clear(); + self.resolve(prop, scope); + via_index_for_group = Some(cumulative.clone()); + is_first_group = false; + } + } + } + + if !group_path.is_empty() { + let via_alias = if is_first_group { + via_alias_first + } else { + None + }; + self.emit_group( + group_path, + group_spans, + via_alias, + via_index_for_group, + scope, + ); + } + } + + fn resolve_pointer_chain(&mut self, chain: &FlatChain, scope: &mut Scope) { + let Some(collection) = scope.pointer_collection.clone() else { + for segment in &chain.segments { + if let ChainSegment::Dynamic { prop } = segment { + self.resolve(prop, scope); + } + } + return; + }; + + let mut group_path: Vec> = Vec::new(); + let mut group_spans: Vec<(u32, u32)> = Vec::new(); + let mut cumulative: Vec> = collection.clone(); + let mut via_index_for_group = collection; + + for segment in &chain.segments { + match segment { + ChainSegment::Field { name, prop } => { + group_path.push(name.clone()); + group_spans.push(self.node_span(prop)); + cumulative.push(name.clone()); + } + ChainSegment::Dynamic { prop } => { + if !group_path.is_empty() { + self.references.push(Reference { + path: std::mem::take(&mut group_path), + spans: std::mem::take(&mut group_spans), + via_alias: None, + via_index: Some(via_index_for_group.clone()), + }); + } + self.resolve(prop, scope); + via_index_for_group = cumulative.clone(); + } + } + } + + if !group_path.is_empty() { + self.references.push(Reference { + path: group_path, + spans: group_spans, + via_alias: None, + via_index: Some(via_index_for_group), + }); + } + } + + fn emit_group( + &mut self, + path: Vec>, + spans: Vec<(u32, u32)>, + via_alias: Option, + via_index: Option>>, + scope: &Scope, + ) { + if path.is_empty() { + return; + } + if via_alias.is_none() && scope.is_local(&path) { + return; + } + let span = match (spans.first(), spans.last()) { + (Some(first), Some(last)) => (first.0, last.1), + _ => Default::default(), + }; + let read_path = match &via_index { + Some(prefix) => prefix.iter().chain(path.iter()).cloned().collect(), + None => path.clone(), + }; + self.reads.push(ReadDependency::Direct { + path: read_path, + span, + via_index: via_index.is_some(), + }); + self.references.push(Reference { + path, + spans, + via_alias, + via_index, + }); + } + + #[cfg_attr(not(target_family = "wasm"), recursive::recursive)] + fn resolve(&mut self, node: &Node, scope: &mut Scope) { + match node { + Node::Identifier(name) => { + let path = vec![Rc::from(*name)]; + let span = self.node_span(node); + if scope.unresolved_aliases.contains(&path[0]) { + self.reads.push(ReadDependency::Unresolved { + path: path.clone(), + span, + }); + self.references.push(Reference { + path, + spans: vec![span], + via_alias: None, + via_index: None, + }); + return; + } + let via_alias = + scope + .aliases + .get(&path[0]) + .cloned() + .map(|collection| AliasBinding { + alias: path[0].clone(), + collection, + }); + if via_alias.is_some() || !scope.is_local(&path) { + self.reads.push(ReadDependency::Direct { + path: path.clone(), + span, + via_index: false, + }); + self.references.push(Reference { + path, + spans: vec![span], + via_alias, + via_index: None, + }); + } + } + + Node::Member { .. } => self.resolve_member_chain(node, scope), + + Node::Assignments { list, output } => { + for (key, value) in list.iter() { + self.resolve(value, scope); + + if let Some(path) = Self::extract_binding_path(key) { + if path.len() == 1 { + scope.locals.insert(path[0].clone()); + } else if path.len() > 1 { + scope.bindings.push(path); + } + } + } + + if let Some(output) = output { + self.resolve(output, scope); + } + } + + Node::FunctionCall { kind, arguments } => { + if let FunctionKind::Closure(_) = kind { + if arguments.len() >= 2 { + let collection_node = arguments[0]; + let closure_node = arguments[1]; + + let collection_info = self.extract_path_with_spans(collection_node); + let collection_source = collection_info + .as_ref() + .map(|(p, _)| p.clone()) + .or_else(|| Self::collection_source_path(collection_node)); + + let (alias, inner_reads, inner_refs) = match closure_node { + Node::Closure { body, alias } => { + let mut inner_scope = scope.clone(); + inner_scope.locals.insert(Variable::dollar_key()); + inner_scope.pointer_collection = collection_source + .as_ref() + .filter(|source| !scope.is_local(source)) + .map(|source| scope.expand_alias_root(source)); + + match (alias, collection_source.as_ref()) { + (Some(alias_name), Some(source)) => { + inner_scope.aliases.insert( + Rc::from(*alias_name), + scope.expand_alias_root(source), + ); + } + (Some(alias_name), None) => { + inner_scope + .unresolved_aliases + .insert(Rc::from(*alias_name)); + } + _ => {} + } + let (reads, refs) = + Self::walk_inner(body, &mut inner_scope, self.metadata); + (alias.map(|a| Rc::from(a)), reads, refs) + } + _ => { + self.resolve(closure_node, scope); + return; + } + }; + + match collection_info { + Some((collection, spans)) if !scope.is_local(&collection) => { + let span = self.node_span(collection_node); + self.references.push(Reference { + path: collection.clone(), + spans, + via_alias: scope.alias_binding_for(&collection), + via_index: None, + }); + self.references.extend(inner_refs); + self.reads.push(ReadDependency::Iteration { + collection, + span, + alias, + reads: inner_reads, + }); + } + _ => { + self.resolve(collection_node, scope); + match collection_source { + Some(source) + if !source.is_empty() && !scope.is_local(&source) => + { + let span = self.node_span(collection_node); + self.references.extend(inner_refs); + self.reads.push(ReadDependency::Iteration { + collection: source, + span, + alias, + reads: inner_reads, + }); + } + _ => { + self.reads.extend(inner_reads); + self.references.extend(inner_refs); + } + } + } + } + } else { + for arg in arguments.iter() { + self.resolve(arg, scope); + } + } + } else { + for arg in arguments.iter() { + self.resolve(arg, scope); + } + } + } + + Node::Closure { body, alias: _ } => { + let mut inner = scope.clone(); + inner.locals.insert(Variable::dollar_key()); + inner.pointer_collection = None; + self.resolve(body, &mut inner); + } + + Node::MethodCall { + this, arguments, .. + } => { + self.resolve(this, scope); + for arg in arguments.iter() { + self.resolve(arg, scope); + } + } + + Node::Binary { left, right, .. } => { + self.resolve(left, scope); + self.resolve(right, scope); + } + + Node::Unary { node, .. } => self.resolve(node, scope), + + Node::Conditional { + condition, + on_true, + on_false, + } => { + self.resolve(condition, scope); + self.resolve(on_true, scope); + self.resolve(on_false, scope); + } + + Node::Parenthesized(n) => self.resolve(n, scope), + + Node::Array(items) => { + for item in items.iter() { + self.resolve(item, scope); + } + } + + Node::Object(pairs) => { + for (k, v) in pairs.iter() { + self.resolve(k, scope); + self.resolve(v, scope); + } + } + + Node::TemplateString(parts) => { + for part in parts.iter() { + self.resolve(part, scope); + } + } + + Node::Slice { node, from, to } => { + self.resolve(node, scope); + if let Some(f) = from { + self.resolve(f, scope); + } + if let Some(t) = to { + self.resolve(t, scope); + } + } + + Node::Interval { left, right, .. } => { + self.resolve(left, scope); + self.resolve(right, scope); + } + + Node::Error { node, .. } => { + if let Some(n) = node { + self.resolve(n, scope); + } + } + + Node::Root => { + let path: Vec> = vec![Variable::root_key()]; + let span = self.node_span(node); + self.reads.push(ReadDependency::Direct { + path: path.clone(), + span, + via_index: false, + }); + self.references.push(Reference { + path, + spans: vec![span], + via_alias: None, + via_index: None, + }); + } + + Node::Null | Node::Bool(_) | Node::Number(_) | Node::String(_) | Node::Pointer => {} + } + } + + fn extract_binding_path(node: &Node) -> Option>> { + match node { + Node::String(s) => Some(s.split('.').map(Rc::from).collect()), + _ => Self::extract_read_path(node), + } + } + + fn extract_read_path(node: &Node) -> Option>> { + match node { + Node::Identifier(name) => Some(vec![Rc::from(*name)]), + Node::Root => Some(vec![Variable::root_key()]), + Node::Member { node, property } => { + let mut path = Self::extract_read_path(node)?; + match property { + Node::String(key) => { + path.push(Rc::from(*key)); + Some(path) + } + _ => None, + } + } + _ => None, + } + } +} diff --git a/core/expression/src/intellisense/diagnostic.rs b/core/expression/src/intellisense/diagnostic.rs new file mode 100644 index 00000000..59cb2be7 --- /dev/null +++ b/core/expression/src/intellisense/diagnostic.rs @@ -0,0 +1,115 @@ +use crate::compiler::CompilerError; +use crate::intellisense::type_provider::TypesProvider; +use crate::lexer::LexerError; +use crate::parser::Node; +use nohash_hasher::BuildNoHashHasher; +use serde::Serialize; +use std::cell::RefCell; +use std::collections::HashMap; + +use crate::parser::NodeMetadata; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Diagnostic { + pub span: (u32, u32), + pub message: String, + pub severity: Severity, + pub source: DiagnosticSource, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Severity { + Error, + Warning, + Hint, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum DiagnosticSource { + Lexer, + Parser, + TypeCheck, + Compiler, +} + +pub(crate) fn lexer_error_to_diagnostic(err: &LexerError) -> Diagnostic { + let span = match err { + LexerError::UnexpectedSymbol { span, .. } => *span, + LexerError::UnmatchedSymbol { position, .. } => (*position, *position), + LexerError::UnexpectedEof { position, .. } => (*position, *position), + }; + + Diagnostic { + span, + message: err.to_string(), + severity: Severity::Error, + source: DiagnosticSource::Lexer, + } +} + +pub(crate) fn collect_parser_diagnostics(ast: &Node, diagnostics: &mut Vec) { + let collected = RefCell::new(Vec::new()); + ast.walk(|node| { + if let Node::Error { error, .. } = node { + collected.borrow_mut().push(Diagnostic { + span: node.span().unwrap_or_default(), + message: error.to_string(), + severity: Severity::Error, + source: DiagnosticSource::Parser, + }); + } + }); + diagnostics.extend(collected.into_inner()); +} + +pub(crate) fn collect_type_diagnostics( + ast: &Node, + type_data: &TypesProvider, + metadata: &HashMap>, + diagnostics: &mut Vec, +) { + let collected = RefCell::new(Vec::new()); + ast.walk(|node| { + let Some(type_info) = type_data.get_type(node) else { + return; + }; + + let Some(error) = &type_info.error else { + return; + }; + + let addr = node as *const Node as usize; + let span = node + .span() + .or_else(|| metadata.get(&addr).map(|m| m.span)) + .unwrap_or_default(); + + let (severity, message) = if let Some(hint) = error.strip_prefix("Hint:") { + (Severity::Warning, hint.trim().to_string()) + } else if let Some(lint) = error.strip_prefix("Lint:") { + (Severity::Hint, lint.trim().to_string()) + } else { + (Severity::Error, error.clone()) + }; + + collected.borrow_mut().push(Diagnostic { + span, + message, + severity, + source: DiagnosticSource::TypeCheck, + }); + }); + diagnostics.extend(collected.into_inner()); +} + +pub(crate) fn compiler_error_to_diagnostic(err: &CompilerError) -> Diagnostic { + Diagnostic { + span: (0, 0), + message: err.to_string(), + severity: Severity::Error, + source: DiagnosticSource::Compiler, + } +} diff --git a/core/expression/src/intellisense/discriminant.rs b/core/expression/src/intellisense/discriminant.rs new file mode 100644 index 00000000..0ff5c4f5 --- /dev/null +++ b/core/expression/src/intellisense/discriminant.rs @@ -0,0 +1,734 @@ +use std::rc::Rc; + +use rust_decimal::Decimal; + +use crate::lexer::{ArithmeticOperator, Bracket, ComparisonOperator, LogicalOperator, Operator}; +use crate::parser::Node; + +#[derive(Debug, Clone, PartialEq)] +pub enum ArmTest { + Enum { + path: Vec>, + values: Vec>, + }, + Bool { + path: Vec>, + values: Vec, + }, + Number { + path: Vec>, + cover: NumberCover, + }, + Default, + Unrecognized, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct NumberCover { + segments: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct NumberSegment { + lo: Bound, + hi: Bound, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Bound { + Unbounded, + Inclusive(Decimal), + Exclusive(Decimal), +} + +enum Operand { + Path(Vec>), + Num(Decimal), + Str(Rc), + Bool(bool), + Other, +} + +impl ArmTest { + pub(crate) fn from_node(node: &Node) -> ArmTest { + match node { + Node::Parenthesized(inner) => Self::from_node(inner), + Node::Binary { + left, + operator, + right, + } => Self::binary(left, *operator, right), + _ => ArmTest::Unrecognized, + } + } + + fn binary(left: &Node, operator: Operator, right: &Node) -> ArmTest { + match operator { + Operator::Comparison(ComparisonOperator::Equal) => Self::equality(left, right), + Operator::Comparison(ComparisonOperator::In) => Self::in_set(left, right), + Operator::Comparison( + op @ (ComparisonOperator::LessThan + | ComparisonOperator::LessThanOrEqual + | ComparisonOperator::GreaterThan + | ComparisonOperator::GreaterThanOrEqual), + ) => Self::numeric(left, op, right), + Operator::Logical(LogicalOperator::And) => { + Self::and(Self::from_node(left), Self::from_node(right)) + } + Operator::Logical(LogicalOperator::Or) => { + Self::or(Self::from_node(left), Self::from_node(right)) + } + _ => ArmTest::Unrecognized, + } + } + + fn equality(left: &Node, right: &Node) -> ArmTest { + let (path, literal) = match (Operand::classify(left), Operand::classify(right)) { + (Operand::Path(p), literal) => (p, literal), + (literal, Operand::Path(p)) => (p, literal), + _ => return ArmTest::Unrecognized, + }; + match literal { + Operand::Str(s) => ArmTest::Enum { + path, + values: vec![s], + }, + Operand::Bool(b) => ArmTest::Bool { + path, + values: vec![b], + }, + Operand::Num(n) => ArmTest::Number { + path, + cover: NumberCover::point(n), + }, + _ => ArmTest::Unrecognized, + } + } + + fn in_set(left: &Node, right: &Node) -> ArmTest { + let Operand::Path(path) = Operand::classify(left) else { + return ArmTest::Unrecognized; + }; + if let Node::Interval { + left: lo, + right: hi, + left_bracket, + right_bracket, + } = Operand::unwrap(right) + { + let (Operand::Num(lo), Operand::Num(hi)) = + (Operand::classify(lo), Operand::classify(hi)) + else { + return ArmTest::Unrecognized; + }; + return match NumberCover::interval(lo, *left_bracket, hi, *right_bracket) { + Some(cover) => ArmTest::Number { path, cover }, + None => ArmTest::Unrecognized, + }; + } + let Node::Array(items) = Operand::unwrap(right) else { + return ArmTest::Unrecognized; + }; + if items.is_empty() { + return ArmTest::Unrecognized; + } + if let Some(values) = items + .iter() + .map(|n| match Operand::unwrap(n) { + Node::String(s) => Some(Rc::from(*s)), + _ => None, + }) + .collect::>>>() + { + return ArmTest::Enum { path, values }; + } + if let Some(values) = items + .iter() + .map(|n| match Operand::unwrap(n) { + Node::Bool(b) => Some(*b), + _ => None, + }) + .collect::>>() + { + return ArmTest::Bool { path, values }; + } + if let Some(values) = items + .iter() + .map(|n| match Operand::classify(n) { + Operand::Num(d) => Some(d), + _ => None, + }) + .collect::>>() + { + let mut numbers = values.into_iter(); + let Some(first) = numbers.next() else { + return ArmTest::Unrecognized; + }; + let cover = numbers.fold(NumberCover::point(first), |mut cover, n| { + cover.merged_with(&NumberCover::point(n)); + cover + }); + return ArmTest::Number { path, cover }; + } + ArmTest::Unrecognized + } + + fn numeric(left: &Node, op: ComparisonOperator, right: &Node) -> ArmTest { + let (path, num, op) = match (Operand::classify(left), Operand::classify(right)) { + (Operand::Path(p), Operand::Num(n)) => (p, n, op), + (Operand::Num(n), Operand::Path(p)) => (p, n, Self::flip(op)), + _ => return ArmTest::Unrecognized, + }; + match NumberCover::single(op, num) { + Some(cover) => ArmTest::Number { path, cover }, + None => ArmTest::Unrecognized, + } + } + + fn and(left: ArmTest, right: ArmTest) -> ArmTest { + match (left, right) { + ( + ArmTest::Number { + path: pa, + cover: ca, + }, + ArmTest::Number { + path: pb, + cover: cb, + }, + ) if pa == pb => match ca.intersect(&cb) { + Some(cover) => ArmTest::Number { path: pa, cover }, + None => ArmTest::Unrecognized, + }, + _ => ArmTest::Unrecognized, + } + } + + fn or(left: ArmTest, right: ArmTest) -> ArmTest { + match (left, right) { + ( + ArmTest::Enum { + path: pa, + values: mut va, + }, + ArmTest::Enum { + path: pb, + values: vb, + }, + ) if pa == pb => { + va.extend(vb); + ArmTest::Enum { + path: pa, + values: va, + } + } + ( + ArmTest::Bool { + path: pa, + values: mut va, + }, + ArmTest::Bool { + path: pb, + values: vb, + }, + ) if pa == pb => { + va.extend(vb); + ArmTest::Bool { + path: pa, + values: va, + } + } + ( + ArmTest::Number { + path: pa, + cover: mut ca, + }, + ArmTest::Number { + path: pb, + cover: cb, + }, + ) if pa == pb => { + ca.merged_with(&cb); + ArmTest::Number { + path: pa, + cover: ca, + } + } + _ => ArmTest::Unrecognized, + } + } + + fn flip(op: ComparisonOperator) -> ComparisonOperator { + match op { + ComparisonOperator::LessThan => ComparisonOperator::GreaterThan, + ComparisonOperator::LessThanOrEqual => ComparisonOperator::GreaterThanOrEqual, + ComparisonOperator::GreaterThan => ComparisonOperator::LessThan, + ComparisonOperator::GreaterThanOrEqual => ComparisonOperator::LessThanOrEqual, + other => other, + } + } +} + +impl Operand { + fn classify(node: &Node) -> Operand { + match Self::unwrap(node) { + Node::Number(n) => Operand::Num(*n), + Node::String(s) => Operand::Str(Rc::from(*s)), + Node::Bool(b) => Operand::Bool(*b), + Node::Unary { + operator: Operator::Arithmetic(ArithmeticOperator::Subtract), + node, + } => match Self::unwrap(node) { + Node::Number(n) => Operand::Num(-*n), + _ => Operand::Other, + }, + other => match Self::extract_path(other) { + Some(path) => Operand::Path(path), + None => Operand::Other, + }, + } + } + + fn unwrap<'a, 'n>(node: &'a Node<'n>) -> &'a Node<'n> { + match node { + Node::Parenthesized(inner) => Self::unwrap(inner), + other => other, + } + } + + fn extract_path(node: &Node) -> Option>> { + match node { + Node::Identifier(name) => Some(vec![Rc::from(*name)]), + Node::Member { node, property } => { + let mut path = Self::extract_path(node)?; + match property { + Node::String(key) => { + path.push(Rc::from(*key)); + Some(path) + } + _ => None, + } + } + _ => None, + } + } +} + +impl NumberCover { + fn point(n: Decimal) -> Self { + Self { + segments: vec![NumberSegment { + lo: Bound::Inclusive(n), + hi: Bound::Inclusive(n), + }], + } + } + + fn interval(lo: Decimal, left: Bracket, hi: Decimal, right: Bracket) -> Option { + let lo = match left { + Bracket::LeftSquareBracket => Bound::Inclusive(lo), + Bracket::LeftParenthesis => Bound::Exclusive(lo), + _ => return None, + }; + let hi = match right { + Bracket::RightSquareBracket => Bound::Inclusive(hi), + Bracket::RightParenthesis => Bound::Exclusive(hi), + _ => return None, + }; + Some(Self { + segments: vec![NumberSegment { lo, hi }], + }) + } + + fn single(op: ComparisonOperator, n: Decimal) -> Option { + let segment = match op { + ComparisonOperator::LessThan => NumberSegment { + lo: Bound::Unbounded, + hi: Bound::Exclusive(n), + }, + ComparisonOperator::LessThanOrEqual => NumberSegment { + lo: Bound::Unbounded, + hi: Bound::Inclusive(n), + }, + ComparisonOperator::GreaterThan => NumberSegment { + lo: Bound::Exclusive(n), + hi: Bound::Unbounded, + }, + ComparisonOperator::GreaterThanOrEqual => NumberSegment { + lo: Bound::Inclusive(n), + hi: Bound::Unbounded, + }, + _ => return None, + }; + Some(Self { + segments: vec![segment], + }) + } + + pub fn points(&self) -> Option> { + self.segments + .iter() + .map(|s| match (s.lo, s.hi) { + (Bound::Inclusive(lo), Bound::Inclusive(hi)) if lo == hi => Some(lo), + _ => None, + }) + .collect() + } + + pub fn merged_with(&mut self, other: &NumberCover) { + self.segments.extend(other.segments.iter().copied()); + } + + fn intersect(&self, other: &NumberCover) -> Option { + let ([a], [b]) = (self.segments.as_slice(), other.segments.as_slice()) else { + return None; + }; + Some(NumberCover { + segments: vec![NumberSegment { + lo: Bound::tighter_lo(a.lo, b.lo), + hi: Bound::tighter_hi(a.hi, b.hi), + }], + }) + } + + pub fn is_total(&self) -> bool { + let mut segments = self.segments.clone(); + segments.sort_by(|a, b| Bound::lo_rank(a.lo).cmp(&Bound::lo_rank(b.lo))); + let Some((first, rest)) = segments.split_first() else { + return false; + }; + if first.lo != Bound::Unbounded { + return false; + } + let mut frontier = first.hi; + for segment in rest { + if frontier == Bound::Unbounded { + return true; + } + if !Bound::connects(frontier, segment.lo) { + return false; + } + frontier = Bound::wider_hi(frontier, segment.hi); + } + frontier == Bound::Unbounded + } +} + +impl Bound { + fn lo_rank(self) -> (u8, Decimal, u8) { + match self { + Bound::Unbounded => (0, Decimal::ZERO, 0), + Bound::Inclusive(n) => (1, n, 0), + Bound::Exclusive(n) => (1, n, 1), + } + } + + fn hi_rank(self) -> (Decimal, u8) { + match self { + Bound::Exclusive(n) => (n, 0), + Bound::Inclusive(n) => (n, 1), + Bound::Unbounded => (Decimal::MAX, 2), + } + } + + fn tighter_lo(a: Bound, b: Bound) -> Bound { + match (a, b) { + (Bound::Unbounded, other) | (other, Bound::Unbounded) => other, + _ if a.lo_rank() >= b.lo_rank() => a, + _ => b, + } + } + + fn tighter_hi(a: Bound, b: Bound) -> Bound { + match (a, b) { + (Bound::Unbounded, other) | (other, Bound::Unbounded) => other, + _ if a.hi_rank() <= b.hi_rank() => a, + _ => b, + } + } + + fn wider_hi(a: Bound, b: Bound) -> Bound { + match (a, b) { + (Bound::Unbounded, _) | (_, Bound::Unbounded) => Bound::Unbounded, + _ if a.hi_rank() >= b.hi_rank() => a, + _ => b, + } + } + + fn connects(frontier_hi: Bound, next_lo: Bound) -> bool { + let (Bound::Inclusive(f) | Bound::Exclusive(f), Bound::Inclusive(l) | Bound::Exclusive(l)) = + (frontier_hi, next_lo) + else { + return true; + }; + if l < f { + return true; + } + if l > f { + return false; + } + !(matches!(frontier_hi, Bound::Exclusive(_)) && matches!(next_lo, Bound::Exclusive(_))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::intellisense::IntelliSense; + + fn test_of(source: &str) -> ArmTest { + IntelliSense::new().arm_test(source) + } + + fn path(segments: &[&str]) -> Vec> { + segments.iter().map(|s| Rc::from(*s)).collect() + } + + fn number_cover(source: &str) -> NumberCover { + match test_of(source) { + ArmTest::Number { cover, .. } => cover, + other => panic!("expected number cover, got {other:?}"), + } + } + + #[test] + fn equality_string_is_enum() { + assert_eq!( + test_of("customer.segment == \"retail\""), + ArmTest::Enum { + path: path(&["customer", "segment"]), + values: vec![Rc::from("retail")], + } + ); + } + + #[test] + fn equality_literal_left_is_flipped_into_enum() { + assert_eq!( + test_of("\"retail\" == customer.segment"), + ArmTest::Enum { + path: path(&["customer", "segment"]), + values: vec![Rc::from("retail")], + } + ); + } + + #[test] + fn in_string_set_is_enum() { + assert_eq!( + test_of("customer.segment in [\"retail\", \"corporate\"]"), + ArmTest::Enum { + path: path(&["customer", "segment"]), + values: vec![Rc::from("retail"), Rc::from("corporate")], + } + ); + } + + #[test] + fn equality_bool_is_bool() { + assert_eq!( + test_of("customer.active == true"), + ArmTest::Bool { + path: path(&["customer", "active"]), + values: vec![true], + } + ); + } + + #[test] + fn empty_condition_is_default() { + assert_eq!(test_of(""), ArmTest::Default); + } + + #[test] + fn not_equal_is_unrecognized() { + assert_eq!( + test_of("customer.segment != \"retail\""), + ArmTest::Unrecognized + ); + } + + #[test] + fn not_in_is_unrecognized() { + assert_eq!(test_of("customer.n not in [1, 2]"), ArmTest::Unrecognized); + } + + #[test] + fn garbage_is_unrecognized() { + assert_eq!(test_of("customer."), ArmTest::Unrecognized); + } + + #[test] + fn same_property_and_is_interval_intersection() { + let cover = number_cover("customer.p >= 10 and customer.p < 20"); + assert!(!cover.is_total()); + } + + #[test] + fn different_property_and_is_guard() { + assert_eq!( + test_of("customer.p < 10 and customer.region == \"EU\""), + ArmTest::Unrecognized + ); + } + + #[test] + fn same_property_or_is_enum_union() { + assert_eq!( + test_of("customer.segment == \"retail\" or customer.segment == \"corporate\""), + ArmTest::Enum { + path: path(&["customer", "segment"]), + values: vec![Rc::from("retail"), Rc::from("corporate")], + } + ); + } + + #[test] + fn number_tiling_no_gap_is_total() { + let mut cover = number_cover("customer.age < 18"); + cover.merged_with(&number_cover("customer.age >= 18")); + assert!(cover.is_total()); + } + + #[test] + fn number_tiling_with_gap_is_not_total() { + let mut cover = number_cover("customer.age < 18"); + cover.merged_with(&number_cover("customer.age > 18")); + assert!(!cover.is_total()); + } + + #[test] + fn number_tiling_inclusive_overlap_is_total() { + let mut cover = number_cover("customer.age <= 18"); + cover.merged_with(&number_cover("customer.age >= 18")); + assert!(cover.is_total()); + } + + #[test] + fn number_point_fills_seam() { + let mut cover = number_cover("customer.age < 18"); + cover.merged_with(&NumberCover::point(Decimal::from(18))); + cover.merged_with(&number_cover("customer.age > 18")); + assert!(cover.is_total()); + } + + #[test] + fn lone_point_is_not_total() { + assert!(!NumberCover::point(Decimal::from(5)).is_total()); + } + + #[test] + fn disjoint_union_with_gap_is_not_total() { + let mut cover = number_cover("customer.age < 10"); + cover.merged_with(&number_cover("customer.age >= 20")); + assert!(!cover.is_total()); + } + + fn cell_of(source: &str) -> ArmTest { + IntelliSense::new().cell_test(source) + } + + fn cell_cover(source: &str) -> NumberCover { + match cell_of(source) { + ArmTest::Number { cover, .. } => cover, + other => panic!("expected number cover, got {other:?}"), + } + } + + #[test] + fn cell_string_literal_is_enum() { + assert_eq!( + cell_of("\"US\""), + ArmTest::Enum { + path: path(&["$"]), + values: vec![Rc::from("US")], + } + ); + } + + #[test] + fn cell_comma_list_is_enum_union() { + assert_eq!( + cell_of("\"US\", \"CA\""), + ArmTest::Enum { + path: path(&["$"]), + values: vec![Rc::from("US"), Rc::from("CA")], + } + ); + } + + #[test] + fn cell_in_array_is_enum() { + assert_eq!( + cell_of("in [\"US\", \"CA\"]"), + ArmTest::Enum { + path: path(&["$"]), + values: vec![Rc::from("US"), Rc::from("CA")], + } + ); + } + + #[test] + fn cell_bool_literal() { + assert_eq!( + cell_of("true"), + ArmTest::Bool { + path: path(&["$"]), + values: vec![true], + } + ); + } + + #[test] + fn cell_comparison_tiles() { + let mut cover = cell_cover("< 18"); + cover.merged_with(&cell_cover(">= 18")); + assert!(cover.is_total()); + } + + #[test] + fn cell_closed_interval() { + let mut cover = cell_cover("[0..18]"); + assert!(!cover.is_total()); + cover.merged_with(&cell_cover("> 18")); + cover.merged_with(&cell_cover("< 0")); + assert!(cover.is_total()); + } + + #[test] + fn cell_open_interval_leaves_seam() { + let mut cover = cell_cover("(0..18)"); + cover.merged_with(&cell_cover(">= 18")); + cover.merged_with(&cell_cover("<= 0")); + assert!(cover.is_total()); + } + + #[test] + fn cell_negative_bounds() { + let mut cover = cell_cover("[-10..10]"); + cover.merged_with(&cell_cover("> 10")); + cover.merged_with(&cell_cover("< -10")); + assert!(cover.is_total()); + } + + #[test] + fn cell_and_intersects() { + let cover = cell_cover(">= 10 and < 20"); + assert!(!cover.is_total()); + } + + #[test] + fn cell_subpath_test_is_unrecognized() { + assert_eq!(cell_of("$.foo == 1"), ArmTest::Unrecognized); + } + + #[test] + fn cell_function_condition_is_unrecognized() { + assert_eq!(cell_of("startsWith($, \"a\")"), ArmTest::Unrecognized); + } + + #[test] + fn cell_empty_is_default() { + assert_eq!(cell_of(""), ArmTest::Default); + } +} diff --git a/core/expression/src/intellisense/entity_flow.rs b/core/expression/src/intellisense/entity_flow.rs new file mode 100644 index 00000000..0ee229e2 --- /dev/null +++ b/core/expression/src/intellisense/entity_flow.rs @@ -0,0 +1,205 @@ +use std::rc::Rc; + +use crate::functions::{ClosureFunction, FunctionKind}; +use crate::lexer::{LogicalOperator, Operator}; +use crate::parser::Node; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FlowSource { + pub path: Vec>, + pub element: bool, +} + +impl FlowSource { + pub(crate) fn from_node(node: &Node) -> Option { + match node { + Node::Parenthesized(inner) => Self::from_node(inner), + Node::Identifier(_) | Node::Root => Self::path_of(node).map(|path| FlowSource { + path, + element: false, + }), + Node::Member { + node: base, + property, + } => match property { + Node::String(_) => Self::path_of(node).map(|path| FlowSource { + path, + element: false, + }), + Node::Number(_) => { + let source = Self::from_node(base)?; + (!source.element).then_some(FlowSource { + path: source.path, + element: true, + }) + } + _ => None, + }, + Node::Slice { node: base, .. } => { + let source = Self::from_node(base)?; + (!source.element).then_some(source) + } + Node::FunctionCall { + kind: FunctionKind::Closure(ClosureFunction::Filter), + arguments, + } => { + let source = Self::from_node(arguments.first()?)?; + (!source.element).then_some(source) + } + Node::Binary { + left, + operator: Operator::Logical(LogicalOperator::NullishCoalescing), + right, + } => Self::agreeing(left, right), + Node::Conditional { + on_true, on_false, .. + } => Self::agreeing(on_true, on_false), + _ => None, + } + } + + fn agreeing(a: &Node, b: &Node) -> Option { + let left = Self::from_node(a)?; + let right = Self::from_node(b)?; + (left == right).then_some(left) + } + + fn path_of(node: &Node) -> Option>> { + match node { + Node::Identifier(name) => Some(vec![Rc::from(*name)]), + Node::Member { + node: base, + property, + } => { + let mut path = Self::path_of(base)?; + match property { + Node::String(key) => { + path.push(Rc::from(*key)); + Some(path) + } + _ => None, + } + } + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::intellisense::IntelliSense; + + fn flow(source: &str) -> Option { + IntelliSense::new().flow_source(source) + } + + fn path(segments: &[&str]) -> Vec> { + segments.iter().map(|s| Rc::from(*s)).collect() + } + + #[test] + fn bare_path_is_identity() { + assert_eq!( + flow("customer.companies"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: false, + }) + ); + } + + #[test] + fn filter_preserves_identity() { + assert_eq!( + flow("filter(customer.companies, $.revenue > 0)"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: false, + }) + ); + } + + #[test] + fn index_yields_element() { + assert_eq!( + flow("customer.companies[0]"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: true, + }) + ); + } + + #[test] + fn slice_preserves_array() { + assert_eq!( + flow("customer.companies[1:3]"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: false, + }) + ); + } + + #[test] + fn filter_of_index_is_rejected() { + assert_eq!(flow("filter(customer.companies[0], true)"), None); + } + + #[test] + fn index_of_index_is_rejected() { + assert_eq!(flow("customer.companies[0][1]"), None); + } + + #[test] + fn nullish_with_agreeing_sides() { + assert_eq!( + flow("customer.companies ?? customer.companies"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: false, + }) + ); + } + + #[test] + fn conditional_with_disagreeing_branches_is_rejected() { + assert_eq!( + flow("customer.age > 10 ? customer.companies : customer.orders"), + None + ); + } + + #[test] + fn conditional_with_agreeing_branches() { + assert_eq!( + flow("customer.age > 10 ? customer.companies[0] : customer.companies[0]"), + Some(FlowSource { + path: path(&["customer", "companies"]), + element: true, + }) + ); + } + + #[test] + fn map_erases_identity() { + assert_eq!(flow("map(customer.companies as c, { name: c.name })"), None); + } + + #[test] + fn arithmetic_erases_identity() { + assert_eq!(flow("customer.age * 2"), None); + } + + #[test] + fn filter_chain_through_member_path() { + assert_eq!( + flow("filter(customer.profitableCompanies, $.revenue > 100)[0]"), + Some(FlowSource { + path: path(&["customer", "profitableCompanies"]), + element: true, + }) + ); + } +} diff --git a/core/expression/src/intellisense/inspection.rs b/core/expression/src/intellisense/inspection.rs new file mode 100644 index 00000000..55e638f6 --- /dev/null +++ b/core/expression/src/intellisense/inspection.rs @@ -0,0 +1,33 @@ +use crate::intellisense::IntelliSenseToken; +use crate::variable::VariableType; +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InspectionResult { + pub span: (u32, u32), + pub kind: VariableType, + pub label: String, +} + +pub(crate) fn inspect_at( + source: &str, + pos: u32, + tokens: &[IntelliSenseToken], +) -> Option { + let token = tokens + .iter() + .filter(|t| t.span.0 <= pos && pos <= t.span.1 && t.span.0 < t.span.1) + .min_by_key(|t| t.span.1 - t.span.0)?; + + let label = source + .get(token.span.0 as usize..token.span.1 as usize) + .unwrap_or("") + .to_string(); + + Some(InspectionResult { + span: token.span, + kind: token.kind.clone(), + label, + }) +} diff --git a/core/expression/src/intellisense/mod.rs b/core/expression/src/intellisense/mod.rs index 1913287a..922267be 100644 --- a/core/expression/src/intellisense/mod.rs +++ b/core/expression/src/intellisense/mod.rs @@ -1,14 +1,37 @@ -use crate::arena::UnsafeArena; +use crate::compiler::Compiler; +use crate::intellisense::completion::Completions; +use crate::intellisense::dependency::DependencyResolutionWalker; +use crate::intellisense::diagnostic::{ + collect_parser_diagnostics, collect_type_diagnostics, compiler_error_to_diagnostic, + lexer_error_to_diagnostic, Diagnostic, DiagnosticSource, Severity, +}; +use crate::intellisense::inspection::{inspect_at, InspectionResult}; use crate::intellisense::scope::IntelliSenseScope; -use crate::intellisense::types::provider::TypesProvider; +use crate::intellisense::type_provider::TypesProvider; use crate::lexer::Lexer; -use crate::parser::{Node, Parser}; +use crate::parser::{Node, NodeMetadata, Parser}; use crate::variable::VariableType; +use bumpalo::Bump; +use nohash_hasher::BuildNoHashHasher; use serde::Serialize; use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +pub mod completion; +pub mod dependency; +pub mod diagnostic; +mod discriminant; +mod entity_flow; +mod inspection; mod scope; -mod types; +pub(crate) mod type_provider; + +pub use dependency::{DependencyResult, ReadDependency, Reference}; +pub use discriminant::{ArmTest, NumberCover}; +pub use entity_flow::FlowSource; + +pub type AstMetadata = HashMap>; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -19,28 +42,376 @@ pub struct IntelliSenseToken { pub error: Option, } -pub struct IntelliSense<'arena> { - arena: UnsafeArena<'arena>, - lexer: Lexer<'arena>, +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExpressionAnalysis { + pub return_type: VariableType, + pub reads: Vec, + pub references: Vec, + pub diagnostics: Vec, } -impl<'arena> IntelliSense<'arena> { +pub struct IntelliSense { + arena: Bump, + lexer: Lexer, + strict: bool, +} + +impl IntelliSense { pub fn new() -> Self { Self { - arena: UnsafeArena::new(), + arena: Bump::new(), lexer: Lexer::new(), + strict: false, + } + } + + pub fn with_strict(mut self, strict: bool) -> Self { + self.strict = strict; + self + } + + pub fn completions( + &mut self, + source: &str, + pos: u32, + data: &VariableType, + ) -> Vec { + let tokens = match self.type_check(source, data) { + Some(t) => t, + None => return Completions::build_scope(data), + }; + + Completions::build(source, pos, data, &tokens) + } + + pub fn inspect( + &mut self, + source: &str, + pos: u32, + data: &VariableType, + ) -> Option { + let tokens = self.type_check(source, data)?; + inspect_at(source, pos, &tokens) + } + + pub fn analyze(&mut self, source: &str, data: &VariableType) -> Rc { + Rc::new(self.analyze_standard_inner(source, data)) + } + + fn analyze_standard_inner(&mut self, source: &str, data: &VariableType) -> ExpressionAnalysis { + self.arena.reset(); + let arena = &self.arena; + let mut diagnostics = Vec::new(); + + let tokens = match self.lexer.tokenize(arena, source) { + Ok(tokens) => tokens, + Err(err) => { + diagnostics.push(lexer_error_to_diagnostic(&err)); + return ExpressionAnalysis { + return_type: VariableType::Any, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + } + }; + + let Ok(parser) = Parser::try_new(&tokens, arena) else { + return ExpressionAnalysis { + return_type: VariableType::Any, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + }; + + let parser = parser.standard().with_metadata(); + let parser_result = parser.parse(); + let ast = parser_result.root; + + if !parser_result.is_complete || ast.has_error() { + if !parser_result.is_complete { + diagnostics.push(Diagnostic { + span: (0, 0), + message: "Incomplete expression".to_string(), + severity: Severity::Error, + source: DiagnosticSource::Parser, + }); + } + collect_parser_diagnostics(ast, &mut diagnostics); + return ExpressionAnalysis { + return_type: VariableType::Any, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + } + + let metadata = parser_result.metadata.unwrap_or_default(); + + let scope = IntelliSenseScope { + pointer_data: data.shallow_clone(), + root_data: data.shallow_clone(), + current_data: data.shallow_clone(), + ..Default::default() + }; + + let type_data = TypesProvider::generate(ast, scope, self.strict); + + let return_type = type_data + .get_type(ast) + .map(|t| t.kind.clone()) + .unwrap_or(VariableType::Any); + + collect_type_diagnostics(ast, &type_data, &metadata, &mut diagnostics); + + let dep_result = DependencyResolutionWalker::walk(ast, &metadata); + + let mut compiler = Compiler::new(); + if let Err(err) = compiler.compile(ast) { + diagnostics.push(compiler_error_to_diagnostic(&err)); + } + + ExpressionAnalysis { + return_type, + reads: dep_result.reads, + references: dep_result.references, + diagnostics, + } + } + + pub fn with_ast( + &mut self, + source: &str, + unary: bool, + f: impl for<'arena> FnOnce(&'arena Node<'arena>, &AstMetadata) -> T, + ) -> Option { + self.arena.reset(); + let arena = &self.arena; + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).ok()?; + let parser_result = if unary { + parser.unary().with_metadata().parse() + } else { + parser.standard().with_metadata().parse() + }; + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + let metadata = parser_result.metadata.unwrap_or_default(); + Some(f(ast, &metadata)) + } + + pub fn field_reads( + &mut self, + source: &str, + field_path: &[&str], + ) -> Option> { + self.arena.reset(); + let arena = &self.arena; + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena) + .ok()? + .standard() + .with_metadata(); + let parser_result = parser.parse(); + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + let metadata = parser_result.metadata.unwrap_or_default(); + DependencyResolutionWalker::field_dependencies(ast, &metadata, field_path) + } + + pub fn arm_test(&mut self, source: &str) -> ArmTest { + if source.trim().is_empty() { + return ArmTest::Default; + } + self.arena.reset(); + let arena = &self.arena; + let result = (|| { + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).ok()?; + let parser_result = parser.standard().with_metadata().parse(); + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + Some(ArmTest::from_node(ast)) + })(); + result.unwrap_or(ArmTest::Unrecognized) + } + + pub fn cell_test(&mut self, source: &str) -> ArmTest { + if source.trim().is_empty() { + return ArmTest::Default; + } + self.arena.reset(); + let arena = &self.arena; + let result = (|| { + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).ok()?; + let parser_result = parser.unary().with_metadata().parse(); + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + let test = ArmTest::from_node(ast); + let on_reference = match &test { + ArmTest::Enum { path, .. } + | ArmTest::Bool { path, .. } + | ArmTest::Number { path, .. } => { + matches!(path.as_slice(), [p] if p.as_ref() == "$") + } + _ => true, + }; + on_reference.then_some(test) + })(); + result.unwrap_or(ArmTest::Unrecognized) + } + + pub fn flow_source(&mut self, source: &str) -> Option { + if source.trim().is_empty() { + return None; + } + self.arena.reset(); + let arena = &self.arena; + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).ok()?; + let parser_result = parser.standard().parse(); + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + FlowSource::from_node(ast) + } + + pub fn reads(&mut self, source: &str) -> Vec { + self.reads_inner(source, false) + } + + pub fn reads_unary(&mut self, source: &str) -> Vec { + self.reads_inner(source, true) + } + + fn reads_inner(&mut self, source: &str, unary: bool) -> Vec { + self.arena.reset(); + let arena = &self.arena; + let result = (|| { + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).ok()?; + let parser_result = if unary { + parser.unary().with_metadata().parse() + } else { + parser.standard().with_metadata().parse() + }; + let ast = parser_result.root; + if !parser_result.is_complete || ast.has_error() { + return None; + } + let metadata = parser_result.metadata.unwrap_or_default(); + let dep = if unary { + DependencyResolutionWalker::walk_with_locals(ast, &metadata, &["$"]) + } else { + DependencyResolutionWalker::walk(ast, &metadata) + }; + Some(dep.reads) + })(); + result.unwrap_or_default() + } + + pub fn analyze_unary(&mut self, source: &str, data: &VariableType) -> Rc { + Rc::new(self.analyze_unary_inner(source, data)) + } + + fn analyze_unary_inner(&mut self, source: &str, data: &VariableType) -> ExpressionAnalysis { + self.arena.reset(); + let arena = &self.arena; + let mut diagnostics = Vec::new(); + + let tokens = match self.lexer.tokenize(arena, source) { + Ok(tokens) => tokens, + Err(err) => { + diagnostics.push(lexer_error_to_diagnostic(&err)); + return ExpressionAnalysis { + return_type: VariableType::Bool, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + } + }; + + let Ok(parser) = Parser::try_new(&tokens, arena) else { + return ExpressionAnalysis { + return_type: VariableType::Bool, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + }; + + let parser = parser.unary().with_metadata(); + let parser_result = parser.parse(); + let ast = parser_result.root; + + if !parser_result.is_complete || ast.has_error() { + if !parser_result.is_complete { + diagnostics.push(Diagnostic { + span: (0, 0), + message: "Incomplete expression".to_string(), + severity: Severity::Error, + source: DiagnosticSource::Parser, + }); + } + collect_parser_diagnostics(ast, &mut diagnostics); + return ExpressionAnalysis { + return_type: VariableType::Bool, + reads: Vec::new(), + references: Vec::new(), + diagnostics, + }; + } + + let metadata = parser_result.metadata.unwrap_or_default(); + + let scope = IntelliSenseScope { + pointer_data: data.shallow_clone(), + root_data: data.shallow_clone(), + current_data: data.shallow_clone(), + ..Default::default() + }; + + let type_data = TypesProvider::generate(ast, scope, self.strict); + collect_type_diagnostics(ast, &type_data, &metadata, &mut diagnostics); + + let dep_result = DependencyResolutionWalker::walk_with_locals(ast, &metadata, &["$"]); + + let mut compiler = Compiler::new(); + if let Err(err) = compiler.compile(ast) { + diagnostics.push(compiler_error_to_diagnostic(&err)); + } + + ExpressionAnalysis { + return_type: VariableType::Bool, + reads: dep_result.reads, + references: dep_result.references, + diagnostics, } } pub fn type_check( &mut self, - source: &'arena str, + source: &str, data: &VariableType, ) -> Option> { - let arena = self.arena.get(); + self.arena.reset(); + let arena = &self.arena; - let tokens = self.lexer.tokenize(source).ok()?; - let parser = Parser::try_new(tokens, &arena).map(|p| p.standard()).ok()?; + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).map(|p| p.standard()).ok()?; let parser_result = parser.with_metadata().parse(); let ast = parser_result.root; @@ -54,6 +425,7 @@ impl<'arena> IntelliSense<'arena> { current_data: data.shallow_clone(), ..Default::default() }, + self.strict, ); let results = RefCell::new(Vec::new()); @@ -74,20 +446,19 @@ impl<'arena> IntelliSense<'arena> { .unwrap_or_else(|| VariableType::Any), }); }); - - self.arena.with_mut(|a| a.reset()); Some(results.into_inner()) } pub fn type_check_unary( &mut self, - source: &'arena str, + source: &str, data: &VariableType, ) -> Option> { - let arena = self.arena.get(); + self.arena.reset(); + let arena = &self.arena; - let tokens = self.lexer.tokenize(source).ok()?; - let parser = Parser::try_new(tokens, &arena).map(|p| p.unary()).ok()?; + let tokens = self.lexer.tokenize(arena, source).ok()?; + let parser = Parser::try_new(&tokens, arena).map(|p| p.unary()).ok()?; let parser_result = parser.with_metadata().parse(); let ast = parser_result.root; @@ -101,6 +472,7 @@ impl<'arena> IntelliSense<'arena> { current_data: data.shallow_clone(), ..Default::default() }, + self.strict, ); let results = RefCell::new(Vec::new()); @@ -118,37 +490,6 @@ impl<'arena> IntelliSense<'arena> { .unwrap_or_else(|| VariableType::Any), }); }); - - self.arena.with_mut(|a| a.reset()); Some(results.into_inner()) } } - -#[cfg(test)] -mod tests { - use crate::intellisense::IntelliSense; - use crate::variable::VariableType; - use serde_json::json; - - #[test] - fn sample_test() { - let mut is = IntelliSense::new(); - - let data = json!({ "customer": { "firstName": "John", "lastName": "Doe", "array": [{"a": 5}, {"a": 6}] } }); - let data_type: VariableType = data.into(); - - let typ = is.type_check("customer.array[0]", &data_type); - println!("{:?}", typ); - } - - #[test] - fn sample_test_unary() { - let mut is = IntelliSense::new(); - - let data = json!({ "customer": { "firstName": "John", "lastName": "Doe" }, "$": 10}); - let data_type: VariableType = data.into(); - - let typ = is.type_check_unary("> 10", &data_type); - println!("{typ:?}"); - } -} diff --git a/core/expression/src/intellisense/types/provider.rs b/core/expression/src/intellisense/type_provider.rs similarity index 50% rename from core/expression/src/intellisense/types/provider.rs rename to core/expression/src/intellisense/type_provider.rs index 924fd2fb..cb53e052 100644 --- a/core/expression/src/intellisense/types/provider.rs +++ b/core/expression/src/intellisense/type_provider.rs @@ -1,25 +1,75 @@ +use crate::functions::internal::InternalFunction; use crate::functions::registry::FunctionRegistry; use crate::functions::{ClosureFunction, FunctionKind, MethodRegistry}; use crate::intellisense::scope::IntelliSenseScope; -use crate::intellisense::types::type_info::TypeInfo; use crate::lexer::{ArithmeticOperator, ComparisonOperator, LogicalOperator, Operator}; use crate::parser::Node; use crate::variable::VariableType; use ahash::{HashMap, HashMapExt}; use std::cell::RefCell; +use std::fmt::{Display, Formatter}; use std::iter::once; use std::ops::Deref; use std::rc::Rc; +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct TypeInfo { + pub(crate) kind: VariableType, + pub(crate) error: Option, +} + +impl Deref for TypeInfo { + type Target = VariableType; + + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl Display for TypeInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.kind) + } +} + +impl Default for TypeInfo { + fn default() -> Self { + Self { + kind: VariableType::Any, + error: None, + } + } +} + +impl From for TypeInfo { + fn from(value: VariableType) -> Self { + Self { + kind: value, + error: None, + } + } +} + +impl From> for TypeInfo { + fn from(value: Rc) -> Self { + Self { + kind: value.deref().clone(), + error: None, + } + } +} + #[derive(Debug)] pub struct TypesProvider { types: HashMap, + strict: bool, } impl TypesProvider { - pub fn generate(root: &Node, scope: IntelliSenseScope) -> Self { + pub fn generate(root: &Node, scope: IntelliSenseScope, strict: bool) -> Self { let mut s = Self { types: HashMap::new(), + strict, }; s.determine(root, scope); @@ -52,6 +102,19 @@ impl TypesProvider { }); } + fn maybe_nullable(kind: VariableType, nullable: bool) -> TypeInfo { + if nullable + && !matches!( + kind, + VariableType::Any | VariableType::Null | VariableType::Nullable(_) + ) + { + TypeInfo::from(VariableType::Nullable(Rc::new(kind))) + } else { + TypeInfo::from(kind) + } + } + #[cfg_attr(not(target_family = "wasm"), recursive::recursive)] fn determine(&mut self, node: &Node, mut scope: IntelliSenseScope) -> TypeInfo { #[allow(non_snake_case)] @@ -83,10 +146,12 @@ impl TypesProvider { Node::Root => V(scope.root_data.clone()), Node::Slice { node, from, to } => { + let node_type = self.determine(node, scope.clone()); + if let Some(f) = from { let from_type = self.determine(f, scope.clone()); if !from_type.satisfies(&VariableType::Number) { - self.set_error(node, format!("Invalid slice index: expected a `number`, but found `{from_type}`.")); + self.set_error(f, format!("Invalid slice index: expected a `number`, but found `{from_type}`.")); } } @@ -94,7 +159,7 @@ impl TypesProvider { let to_type = self.determine(t, scope.clone()); if !to_type.satisfies(&VariableType::Number) { self.set_error( - node, + t, format!( "Invalid slice index: expected a `number`, but found `{to_type}`." ), @@ -102,27 +167,21 @@ impl TypesProvider { } } - let node_type = self.determine(node, scope.clone()); match node_type.kind.widen() { VariableType::Any => V(VariableType::Any), - VariableType::Array(inner) => TypeInfo::from(inner.clone()), + VariableType::Array(inner) => V(VariableType::Array(inner.clone())), VariableType::String => V(VariableType::String), _ => Error("Slice operation is only allowed on `string | any[]`".to_string()), } } Node::Array(items) => { - let mut type_list: Vec = items + let element_type = items .iter() .map(|n| self.determine(n, scope.clone()).kind) - .collect(); - let first = type_list.pop(); - let all_same = type_list.iter().all(|t| Some(t) == first.as_ref()); - - match (first, all_same) { - (Some(typ), true) => V(VariableType::Array(Rc::new(typ))), - _ => V(VariableType::Array(Rc::new(VariableType::Any))), - } + .reduce(|acc, t| acc.merge(&t)) + .unwrap_or(VariableType::Any); + V(VariableType::Array(Rc::new(element_type))) } Node::Object(obj) => { @@ -141,6 +200,10 @@ impl TypesProvider { } Node::Assignments { list, output } => { + if matches!(scope.root_data, VariableType::Any | VariableType::Null) { + scope.root_data = VariableType::Object(Rc::new(RefCell::new(HashMap::new()))); + } + let obj_type: HashMap, VariableType> = list .iter() .filter_map(|(k, v)| { @@ -173,7 +236,14 @@ impl TypesProvider { let node_type = self.determine(node, scope.clone()); let property_type = self.determine(property, scope.clone()); - match &node_type.kind { + let node_nullable = + self.strict && matches!(node_type.kind, VariableType::Nullable(_)); + let resolved_kind = match &node_type.kind { + VariableType::Nullable(inner) => inner.as_ref(), + other => other, + }; + + match resolved_kind { VariableType::Any => V(VariableType::Any), VariableType::Null => V(VariableType::Null), VariableType::Array(inner) => { @@ -184,7 +254,17 @@ impl TypesProvider { ); } - TypeInfo::from(inner.clone()) + Self::maybe_nullable(inner.as_ref().clone(), self.strict) + } + VariableType::String | VariableType::Const(_) | VariableType::Enum(_, _) => { + if !property_type.satisfies(&VariableType::Number) { + self.set_error( + property, + format!("Expression of type `{property_type}` cannot be used to index `{node_type}`."), + ); + } + + Self::maybe_nullable(VariableType::String, self.strict) } VariableType::Object(obj) => { if !property_type.satisfies(&VariableType::String) { @@ -195,12 +275,21 @@ impl TypesProvider { } let obj = obj.borrow(); - match property_type.as_const_str() { - None => V(VariableType::Any), - Some(key) => TypeInfo::from( - obj.get(&key).cloned().unwrap_or(VariableType::Any), - ), - } + let field = match property_type.as_const_str() { + None => VariableType::Any, + Some(key) => match obj.get(&key) { + Some(t) => t.clone(), + None if !self.strict || obj.is_empty() => VariableType::Any, + None => { + self.set_error( + node, + format!("'{key}' is not a valid member of `{node_type}`."), + ); + VariableType::Any + } + }, + }; + Self::maybe_nullable(field, node_nullable) } _ => Error(format!("Expression of type `{property_type}` cannot be used to index `{node_type}`.")), } @@ -244,23 +333,62 @@ impl TypesProvider { )), } } - LogicalOperator::NullishCoalescing => TypeInfo::from(right_type.kind), + LogicalOperator::NullishCoalescing => { + match &left_type.kind { + VariableType::Nullable(inner) => { + V(inner.as_ref().merge(&right_type.kind)) + } + VariableType::Null => { + if self.strict { + on_fly_error.replace( + "Lint: Left-hand side of `??` is always null; the fallback is always used.".to_string(), + ); + } + TypeInfo::from(right_type.kind) + } + VariableType::Any => TypeInfo::from(left_type.kind), + _ => { + if self.strict { + on_fly_error.replace(format!( + "Lint: Left-hand side of `??` is never null (`{left_type}`); the fallback is redundant." + )); + } + TypeInfo::from(left_type.kind) + } + } + } }, Operator::Comparison(comp) => match comp { ComparisonOperator::Equal => { - if !left_type.satisfies(&right_type) && !left_type.is_null() && !right_type.is_null() { - on_fly_error.replace(format!( - "Hint: Expression will always evaluate to `false` because `{left_type}` != `{right_type}`." - )); + match check_enum_comparison(&left_type, &right_type) { + Some(Some(err)) => { on_fly_error.replace(err); } + Some(None) => {} + None => { + let always_false = Self::structured_comparison(&left_type, &right_type) + || (types_disjoint(&left_type, &right_type) && !left_type.is_nullable() && !right_type.is_nullable() && !left_type.is_null() && !right_type.is_null()); + if always_false { + on_fly_error.replace(format!( + "Hint: Expression will always evaluate to `false` because `{left_type}` != `{right_type}`." + )); + } + } } V(VariableType::Bool) }, ComparisonOperator::NotEqual => { - if !left_type.satisfies(&right_type) && !left_type.is_null() && !right_type.is_null() { - on_fly_error.replace(format!( - "Hint: Expression will always evaluate to `true` because `{left_type}` != `{right_type}`." - )); + match check_enum_comparison(&left_type, &right_type) { + Some(Some(err)) => { on_fly_error.replace(err); } + Some(None) => {} + None => { + let always_true = Self::structured_comparison(&left_type, &right_type) + || (types_disjoint(&left_type, &right_type) && !left_type.is_nullable() && !right_type.is_nullable() && !left_type.is_null() && !right_type.is_null()); + if always_true { + on_fly_error.replace(format!( + "Hint: Expression will always evaluate to `true` because `{left_type}` != `{right_type}`." + )); + } + } } V(VariableType::Bool) @@ -277,15 +405,21 @@ impl TypesProvider { }, ComparisonOperator::In | ComparisonOperator::NotIn => match (left_type.widen(), right_type.widen()) { (_, VariableType::Array(inner_type)) => { - if !left_type.satisfies(&inner_type) { - let expected = match comp { - ComparisonOperator::In => "false", - _ => "true" - }; + match check_enum_comparison(&left_type, &inner_type) { + Some(Some(err)) => { on_fly_error.replace(err); } + Some(None) => {} + None => { + if types_disjoint(&left_type, &inner_type) { + let expected = match comp { + ComparisonOperator::In => "false", + _ => "true" + }; - on_fly_error.replace(format!( - "Hint: Expression will always evaluate to `{expected}`. because array contains element of type `{inner_type}`, and `{left_type}` != `{inner_type}`." - )); + on_fly_error.replace(format!( + "Hint: Expression will always evaluate to `{expected}`. because array contains element of type `{inner_type}`, and `{left_type}` != `{inner_type}`." + )); + } + } } V(VariableType::Bool) @@ -385,13 +519,20 @@ impl TypesProvider { V(VariableType::Interval) } Node::FunctionCall { arguments, kind } => { + let is_closure_kind = matches!(kind, FunctionKind::Closure(_)); let mut type_list: Vec = arguments .iter() - .map(|n| self.determine(n, scope.clone()).kind) + .enumerate() + .map(|(i, n)| match is_closure_kind && i == 1 { + true => VariableType::Any, + false => self.determine(n, scope.clone()).kind, + }) .collect(); if let FunctionKind::Closure(_) = kind { - let ptr_type = type_list[0].iterator().unwrap_or_default(); + let ptr_type = type_list[0] + .iterator() + .unwrap_or_else(|| Rc::new(VariableType::Any)); let ptr_type_inner = ptr_type.deref().clone(); let alias = match arguments[1] { @@ -417,6 +558,18 @@ impl TypesProvider { } match kind { + FunctionKind::Internal(InternalFunction::Merge) => { + self.merge_typecheck(&type_list, arguments) + } + FunctionKind::Internal(InternalFunction::MergeDeep) => { + self.merge_deep_typecheck(&type_list, arguments) + } + FunctionKind::Internal(InternalFunction::Flatten) => { + self.flatten_typecheck(&type_list, arguments) + } + FunctionKind::Internal(InternalFunction::Values) => { + self.values_typecheck(&type_list, arguments) + } FunctionKind::Internal(_) | FunctionKind::Deprecated(_) => { let Some(def) = FunctionRegistry::get_definition(kind) else { return V(VariableType::Any); @@ -438,9 +591,16 @@ impl TypesProvider { arguments[0], format!("Argument of type `{}` is not `iterable`.", type_list[0]), ); + } else if self.strict && matches!(type_list[0], VariableType::Nullable(_)) { + self.set_error( + arguments[0], + format!( + "Argument of type `{}` may be `null`; use `?? []` to provide a fallback.", + type_list[0] + ), + ); } - // Boolean callbacks if matches!( c, ClosureFunction::All @@ -471,7 +631,14 @@ impl TypesProvider { ClosureFunction::Map => { V(VariableType::Array(Rc::new(type_list[1].clone()))) } - ClosureFunction::FlatMap => V(VariableType::Any), + ClosureFunction::FlatMap => { + let body_ty = &type_list[1]; + let element = match body_ty.iterator() { + Some(inner) => inner.deref().clone(), + None => body_ty.clone(), + }; + V(VariableType::Array(Rc::new(element))) + } } } } @@ -508,8 +675,8 @@ impl TypesProvider { error: typecheck.general, } } - Node::Closure { body, .. } => self.determine(body, scope.clone()), - Node::Parenthesized(c) => self.determine(c, scope.clone()), + Node::Closure { body, .. } => TypeInfo::from(self.determine(body, scope.clone()).kind), + Node::Parenthesized(c) => TypeInfo::from(self.determine(c, scope.clone()).kind), Node::Error { node, error } => match node { None => TypeInfo { kind: VariableType::Any, @@ -534,7 +701,300 @@ impl TypesProvider { } } +fn types_disjoint(left: &VariableType, right: &VariableType) -> bool { + let (left, _) = left.unwrap_nullable(); + let (right, _) = right.unwrap_nullable(); + match (value_set(left), value_set(right)) { + (Some(l), Some(r)) => !l.iter().any(|v| r.contains(v)), + _ => !left.satisfies(right), + } +} + +fn value_set(t: &VariableType) -> Option>> { + match t { + VariableType::Const(c) => Some(vec![c.clone()]), + VariableType::Enum(_, values) => Some(values.clone()), + _ => None, + } +} + +fn check_enum_comparison(left: &VariableType, right: &VariableType) -> Option> { + let (left, _) = left.unwrap_nullable(); + let (right, _) = right.unwrap_nullable(); + + match (left, right) { + (VariableType::Enum(_, values), VariableType::Const(c)) + | (VariableType::Const(c), VariableType::Enum(_, values)) => { + if values.iter().any(|v| v == c) { + Some(None) + } else { + let enum_type = match (left, right) { + (VariableType::Enum(_, _), _) => left, + _ => right, + }; + Some(Some(format!( + "Value `\"{c}\"` is not a valid member of `{enum_type}`." + ))) + } + } + (VariableType::Const(c1), VariableType::Const(c2)) => { + if c1 == c2 { + Some(None) + } else { + Some(Some(format!( + "Value `\"{c1}\"` will never equal `\"{c2}\"`." + ))) + } + } + _ => None, + } +} + #[allow(unused)] fn node_address(node: &Node) -> usize { node as *const Node as usize } + +impl TypesProvider { + fn merge_typecheck(&mut self, arg_types: &[VariableType], arg_nodes: &[&Node]) -> TypeInfo { + if arg_types.len() != 1 { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!( + "Expected `1` arguments, got `{}`.", + arg_types.len() + )), + }; + } + + let Some(element) = arg_types[0].iterator() else { + if let Some(node) = arg_nodes.first() { + self.set_error( + node, + format!("`merge` expects an array, got `{}`", arg_types[0]), + ); + } + return TypeInfo::from(VariableType::Any); + }; + + Self::merge_kind_from_types(std::slice::from_ref(&*element)) + } + + fn merge_deep_typecheck( + &mut self, + arg_types: &[VariableType], + arg_nodes: &[&Node], + ) -> TypeInfo { + if arg_types.len() != 1 { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!( + "Expected `1` arguments, got `{}`.", + arg_types.len() + )), + }; + } + + let Some(element) = arg_types[0].iterator() else { + if let Some(node) = arg_nodes.first() { + self.set_error( + node, + format!("`mergeDeep` expects an array, got `{}`", arg_types[0]), + ); + } + return TypeInfo::from(VariableType::Any); + }; + + Self::merge_deep_from_types(std::slice::from_ref(&*element)) + } + + fn flatten_typecheck(&mut self, arg_types: &[VariableType], arg_nodes: &[&Node]) -> TypeInfo { + if arg_types.len() != 1 { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!( + "Expected `1` arguments, got `{}`.", + arg_types.len() + )), + }; + } + let arg = &arg_types[0]; + let Some(outer_element) = arg.iterator() else { + if let Some(node) = arg_nodes.first() { + self.set_error(node, format!("`flatten` expects an array, got `{arg}`")); + } + return TypeInfo::from(VariableType::Any); + }; + + if self.strict && matches!(arg, VariableType::Nullable(_)) { + if let Some(node) = arg_nodes.first() { + self.set_error( + node, + format!("Argument of type `{arg}` may be `null`; use `?? []` to provide a fallback."), + ); + } + } + + let element = Self::flatten_contribution_for(&outer_element); + TypeInfo::from(VariableType::Array(Rc::new(element))) + } + + /// Typecheck `values(obj)` → `Array`. + /// Falls back to `Array` if the object has no known fields. + fn values_typecheck(&mut self, arg_types: &[VariableType], arg_nodes: &[&Node]) -> TypeInfo { + if arg_types.len() != 1 { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!( + "Expected `1` arguments, got `{}`.", + arg_types.len() + )), + }; + } + let arg = &arg_types[0]; + let (inner_ty, _) = arg.unwrap_nullable(); + let VariableType::Object(obj) = inner_ty else { + if !matches!(inner_ty, VariableType::Any) { + if let Some(node) = arg_nodes.first() { + self.set_error(node, format!("`values` expects an object, got `{arg}`")); + } + } + return TypeInfo::from(VariableType::Array(Rc::new(VariableType::Any))); + }; + let fields = obj.borrow(); + let mut union: Option = None; + for v in fields.values() { + union = Some(match union { + None => v.clone(), + Some(prev) => prev.merge(v), + }); + } + TypeInfo::from(VariableType::Array(Rc::new( + union.unwrap_or(VariableType::Any), + ))) + } + + fn structured_comparison(left: &VariableType, right: &VariableType) -> bool { + matches!( + (left, right), + (VariableType::Object(_), VariableType::Object(_)) + | (VariableType::Array(_), VariableType::Array(_)) + ) + } + + fn merge_kind_from_types(types: &[VariableType]) -> TypeInfo { + let mut saw_object = false; + let mut saw_array = false; + for ty in types { + match ty.unwrap_nullable().0 { + VariableType::Object(_) => saw_object = true, + VariableType::Array(_) => saw_array = true, + VariableType::Any | VariableType::Null => {} + _ => { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!("`merge` expects objects or arrays, got `{ty}`")), + }; + } + } + } + + if !saw_object && !saw_array { + return TypeInfo::from(VariableType::Any); + } + + if saw_object && !saw_array { + let mut fields: HashMap, VariableType> = HashMap::new(); + for ty in types { + if let VariableType::Object(obj) = ty.unwrap_nullable().0 { + for (k, v) in obj.borrow().iter() { + // Last write wins — matches the runtime. + fields.insert(k.clone(), v.clone()); + } + } + } + return TypeInfo::from(VariableType::Object(Rc::new(RefCell::new(fields)))); + } + + if saw_array && !saw_object { + let mut element: Option = None; + for ty in types { + if let Some(inner) = ty.iterator() { + element = Some(match element { + None => inner.deref().clone(), + Some(prev) => prev.merge(&inner), + }); + } + } + return TypeInfo::from(VariableType::Array(Rc::new( + element.unwrap_or(VariableType::Any), + ))); + } + + TypeInfo { + kind: VariableType::Any, + error: Some( + "`merge` expects all arguments to be objects, or all to be arrays".to_string(), + ), + } + } + + fn merge_deep_from_types(types: &[VariableType]) -> TypeInfo { + for ty in types { + if !matches!( + ty.unwrap_nullable().0, + VariableType::Object(_) | VariableType::Any | VariableType::Null + ) { + return TypeInfo { + kind: VariableType::Any, + error: Some(format!("`mergeDeep` expects objects, got `{ty}`")), + }; + } + } + let mut fields: HashMap, VariableType> = HashMap::new(); + for ty in types { + if let VariableType::Object(obj) = ty.unwrap_nullable().0 { + for (k, v) in obj.borrow().iter() { + let new_value = match fields.remove(k) { + Some(existing) => Self::deep_merge_types(&existing, v), + None => v.clone(), + }; + fields.insert(k.clone(), new_value); + } + } + } + TypeInfo::from(VariableType::Object(Rc::new(RefCell::new(fields)))) + } + + fn deep_merge_types(a: &VariableType, b: &VariableType) -> VariableType { + match (a, b) { + (VariableType::Object(oa), VariableType::Object(ob)) => { + let mut fields: HashMap, VariableType> = HashMap::new(); + for (k, v) in oa.borrow().iter() { + fields.insert(k.clone(), v.clone()); + } + for (k, v) in ob.borrow().iter() { + let merged = match fields.remove(k) { + Some(existing) => Self::deep_merge_types(&existing, v), + None => v.clone(), + }; + fields.insert(k.clone(), merged); + } + VariableType::Object(Rc::new(RefCell::new(fields))) + } + _ => b.clone(), + } + } + + fn flatten_contribution_for(t: &VariableType) -> VariableType { + match t { + VariableType::Nullable(inner) => { + let inner_contribution = Self::flatten_contribution_for(inner); + VariableType::Null.merge(&inner_contribution) + } + VariableType::Array(inner) => inner.deref().clone(), + VariableType::Interval => VariableType::Number, + scalar => scalar.clone(), + } + } +} diff --git a/core/expression/src/intellisense/types/mod.rs b/core/expression/src/intellisense/types/mod.rs deleted file mode 100644 index bf6a4364..00000000 --- a/core/expression/src/intellisense/types/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod provider; -mod type_info; diff --git a/core/expression/src/intellisense/types/type_info.rs b/core/expression/src/intellisense/types/type_info.rs deleted file mode 100644 index 19b35e7e..00000000 --- a/core/expression/src/intellisense/types/type_info.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::variable::VariableType; -use std::fmt::{Display, Formatter}; -use std::ops::Deref; -use std::rc::Rc; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct TypeInfo { - pub(crate) kind: VariableType, - pub(crate) error: Option, -} - -impl Deref for TypeInfo { - type Target = VariableType; - - fn deref(&self) -> &Self::Target { - &self.kind - } -} - -impl Display for TypeInfo { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.kind) - } -} - -impl Default for TypeInfo { - fn default() -> Self { - Self { - kind: VariableType::Any, - error: None, - } - } -} - -impl From for TypeInfo { - fn from(value: VariableType) -> Self { - Self { - kind: value, - error: None, - } - } -} - -impl From> for TypeInfo { - fn from(value: Rc) -> Self { - Self { - kind: value.deref().clone(), - error: None, - } - } -} diff --git a/core/expression/src/isolate.rs b/core/expression/src/isolate.rs index 2d8aeafb..6ddd6cdc 100644 --- a/core/expression/src/isolate.rs +++ b/core/expression/src/isolate.rs @@ -1,11 +1,9 @@ use ahash::HashMap; use serde::ser::SerializeMap; use serde::{Serialize, Serializer}; -use std::rc::Rc; use std::sync::Arc; use thiserror::Error; -use crate::arena::UnsafeArena; use crate::compiler::{Compiler, CompilerError, Opcode}; use crate::expression::{OpcodeCache, Standard, Unary}; use crate::lexer::{Lexer, LexerError}; @@ -13,6 +11,7 @@ use crate::parser::{Parser, ParserError}; use crate::variable::Variable; use crate::vm::{VMError, VM}; use crate::{Expression, ExpressionKind}; +use bumpalo::Bump; /// Isolate is a component that encapsulates an isolated environment for executing expressions. /// @@ -20,26 +19,26 @@ use crate::{Expression, ExpressionKind}; /// The arena allocator optimizes memory management by reusing memory blocks for subsequent evaluations, /// contributing to improved performance and resource utilization in scenarios where the Isolate is reused multiple times. #[derive(Debug)] -pub struct Isolate<'arena> { - lexer: Lexer<'arena>, +pub struct Isolate { + lexer: Lexer, compiler: Compiler, vm: VM, - bump: UnsafeArena<'arena>, + bump: Bump, environment: Option, references: HashMap, cache: Option>, } -impl<'a> Isolate<'a> { +impl Isolate { pub fn new() -> Self { Self { lexer: Lexer::new(), compiler: Compiler::new(), vm: VM::new(), - bump: UnsafeArena::new(), + bump: Bump::new(), environment: None, references: Default::default(), @@ -61,6 +60,7 @@ impl<'a> Isolate<'a> { pub fn set_environment(&mut self, variable: Variable) { self.environment.replace(variable); + self.references.clear(); } pub fn set_cache(&mut self, cache: Arc) { @@ -74,7 +74,7 @@ impl<'a> Isolate<'a> { updater(self.environment.as_mut()); } - pub fn set_reference(&mut self, reference: &'a str) -> Result<(), IsolateError> { + pub fn set_reference(&mut self, reference: &str) -> Result<(), IsolateError> { let reference_value = match self.references.get(reference) { Some(value) => value.clone(), None => { @@ -85,6 +85,10 @@ impl<'a> Isolate<'a> { } }; + self.set_reference_value(reference_value) + } + + pub fn set_reference_value(&mut self, value: Variable) -> Result<(), IsolateError> { if !matches!(&mut self.environment, Some(Variable::Object(_))) { self.environment.replace(Variable::empty_object()); } @@ -94,7 +98,7 @@ impl<'a> Isolate<'a> { }; let mut environment_object = environment_object_ref.borrow_mut(); - environment_object.insert(Rc::from("$"), reference_value); + environment_object.insert(Variable::dollar_key(), value); Ok(()) } @@ -103,17 +107,13 @@ impl<'a> Isolate<'a> { self.references.get(reference).cloned() } - pub fn clear_references(&mut self) { - self.references.clear(); - } + fn run_internal(&mut self, source: &str, kind: ExpressionKind) -> Result<(), IsolateError> { + self.bump.reset(); + let bump = &self.bump; - fn run_internal(&mut self, source: &'a str, kind: ExpressionKind) -> Result<(), IsolateError> { - self.bump.with_mut(|b| b.reset()); - let bump = self.bump.get(); + let tokens = self.lexer.tokenize(bump, source)?; - let tokens = self.lexer.tokenize(source)?; - - let base_parser = Parser::try_new(tokens, bump)?; + let base_parser = Parser::try_new(&tokens, bump)?; let parser_result = match kind { ExpressionKind::Unary => base_parser.unary().parse(), ExpressionKind::Standard => base_parser.standard().parse(), @@ -126,17 +126,14 @@ impl<'a> Isolate<'a> { Ok(()) } - pub fn compile_standard( - &mut self, - source: &'a str, - ) -> Result, IsolateError> { + pub fn compile_standard(&mut self, source: &str) -> Result, IsolateError> { self.run_internal(source, ExpressionKind::Standard)?; let bytecode = self.compiler.get_bytecode().to_vec(); Ok(Expression::new_standard(Arc::from(bytecode))) } - pub fn run_standard(&mut self, source: &'a str) -> Result { + pub fn run_standard(&mut self, source: &str) -> Result { let cached = self .cache .as_ref() @@ -162,14 +159,14 @@ impl<'a> Isolate<'a> { Ok(result) } - pub fn compile_unary(&mut self, source: &'a str) -> Result, IsolateError> { + pub fn compile_unary(&mut self, source: &str) -> Result, IsolateError> { self.run_internal(source, ExpressionKind::Unary)?; let bytecode = self.compiler.get_bytecode().to_vec(); Ok(Expression::new_unary(Arc::from(bytecode))) } - pub fn run_unary(&mut self, source: &'a str) -> Result { + pub fn run_unary(&mut self, source: &str) -> Result { let cached = self .cache .as_ref() diff --git a/core/expression/src/lexer/lexer.rs b/core/expression/src/lexer/lexer.rs index 2c2a9812..9638cff5 100644 --- a/core/expression/src/lexer/lexer.rs +++ b/core/expression/src/lexer/lexer.rs @@ -5,34 +5,41 @@ use crate::lexer::token::{ Bracket, ComparisonOperator, Identifier, LogicalOperator, Operator, Token, TokenKind, }; use crate::lexer::{LexerError, QuotationMark, TemplateString}; +use bumpalo::collections::Vec as BumpVec; +use bumpalo::Bump; use std::str::FromStr; #[derive(Debug, Default)] -pub struct Lexer<'arena> { - tokens: Vec>, +pub struct Lexer { + capacity_hint: usize, } -impl<'arena> Lexer<'arena> { +impl Lexer { pub fn new() -> Self { Self::default() } - pub fn tokenize(&mut self, source: &'arena str) -> LexerResult<&[Token<'arena>]> { - self.tokens.clear(); + pub fn tokenize<'arena>( + &mut self, + bump: &'arena Bump, + source: &'arena str, + ) -> LexerResult>> { + let mut tokens = BumpVec::with_capacity_in(self.capacity_hint, bump); - Scanner::new(source, &mut self.tokens).scan()?; - Ok(&self.tokens) + Scanner::new(source, &mut tokens).scan()?; + self.capacity_hint = self.capacity_hint.max(tokens.len()); + Ok(tokens) } } struct Scanner<'arena, 'self_ref> { cursor: Cursor<'arena>, - tokens: &'self_ref mut Vec>, + tokens: &'self_ref mut BumpVec<'arena, Token<'arena>>, source: &'arena str, } impl<'arena, 'self_ref> Scanner<'arena, 'self_ref> { - pub fn new(source: &'arena str, tokens: &'self_ref mut Vec>) -> Self { + pub fn new(source: &'arena str, tokens: &'self_ref mut BumpVec<'arena, Token<'arena>>) -> Self { Self { cursor: Cursor::from(source), source, @@ -102,7 +109,24 @@ impl<'arena, 'self_ref> Scanner<'arena, 'self_ref> { let mut in_expression = false; let mut str_start = start + 1; loop { - let (e, c) = self.next()?; + let Some((e, c)) = self.cursor.next() else { + let end = self.source.len(); + if !in_expression && str_start < end { + self.tokens.push(Token { + kind: TokenKind::Literal, + span: (str_start as u32, end as u32), + value: &self.source[str_start..end], + }); + } + + self.tokens.push(Token { + kind: TokenKind::QuotationMark(QuotationMark::Backtick), + span: (end as u32, end as u32), + value: QuotationMark::Backtick.into(), + }); + + break; + }; match (c, in_expression) { ('`', _) => { diff --git a/core/expression/src/lib.rs b/core/expression/src/lib.rs index a0ff7614..f630ec5c 100644 --- a/core/expression/src/lib.rs +++ b/core/expression/src/lib.rs @@ -55,9 +55,10 @@ //! `regex-deprecated` | Uses standard `regex` crate | Yes //! `regex-lite` | Opts for usage of lightweight `regex-lite` crate. Useful for reducing build size, especially in WASM. | No +#![forbid(unsafe_code)] + mod isolate; -mod arena; pub mod compiler; mod exports; pub mod expression; diff --git a/core/expression/src/parser/constants.rs b/core/expression/src/parser/constants.rs index 19ceefb7..b1d8a0a2 100644 --- a/core/expression/src/parser/constants.rs +++ b/core/expression/src/parser/constants.rs @@ -7,18 +7,28 @@ use Associativity::{Left, Right}; type NoHasher = BuildNoHashHasher; -#[derive(Debug, PartialEq)] -pub(crate) enum Associativity { +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Associativity { Left, Right, } #[derive(Debug, PartialEq)] -pub(crate) struct ParserOperator { +pub struct ParserOperator { pub precedence: u8, pub associativity: Associativity, } +impl ParserOperator { + pub fn binary(operator: &Operator) -> Option<&'static ParserOperator> { + BINARY_OPERATORS.get(operator) + } + + pub fn unary(operator: &Operator) -> Option<&'static ParserOperator> { + UNARY_OPERATORS.get(operator) + } +} + macro_rules! hashmap { (@single $($x:tt)*) => (()); (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); diff --git a/core/expression/src/parser/mod.rs b/core/expression/src/parser/mod.rs index a8f21f7a..82ff37b6 100644 --- a/core/expression/src/parser/mod.rs +++ b/core/expression/src/parser/mod.rs @@ -14,6 +14,7 @@ mod standard; mod unary; pub use ast::Node; +pub use constants::{Associativity, ParserOperator}; pub use error::ParserError; pub use parser::Parser; pub use result::{NodeMetadata, ParserResult}; diff --git a/core/expression/src/vm/vm.rs b/core/expression/src/vm/vm.rs index ded87600..570e9e9e 100644 --- a/core/expression/src/vm/vm.rs +++ b/core/expression/src/vm/vm.rs @@ -153,6 +153,12 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> { let arr_ref = arr.borrow(); arr_ref.get(*num as usize).cloned().unwrap_or(Null) } + String(str) => { + let index = *num as usize; + str.get(index..index + 1) + .map(|slice| String(Rc::from(slice))) + .unwrap_or(Null) + } _ => Null, }, }); diff --git a/core/expression/tests/analysis.rs b/core/expression/tests/analysis.rs new file mode 100644 index 00000000..e458dab8 --- /dev/null +++ b/core/expression/tests/analysis.rs @@ -0,0 +1,650 @@ +use serde::Deserialize; +use serde_json::Value; +use zen_expression::intellisense::dependency::{ReadDependency, Reference}; + +use zen_expression::intellisense::diagnostic::{DiagnosticSource, Severity}; +use zen_expression::intellisense::{ExpressionAnalysis, IntelliSense}; +use zen_expression::variable::VariableType; + +#[derive(Debug, Deserialize)] +struct TestFile { + test: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestCase { + name: String, + expression: String, + input: Option, + return_type: Option, + reads: Option>, + reads_with_spans: Option>, + references: Option>, + diagnostics: Option>, + #[serde(default)] + unary: bool, + #[serde(default)] + strict: StrictField, + loose: Option, +} + +#[derive(Debug, Deserialize, Default)] +#[serde(untagged)] +enum StrictField { + #[default] + Absent, + Bool(bool), + Expectations(ModeExpectations), +} + +impl StrictField { + fn as_bool(&self) -> bool { + matches!(self, StrictField::Bool(true)) + } + + fn as_expectations(&self) -> Option<&ModeExpectations> { + match self { + StrictField::Expectations(e) => Some(e), + _ => None, + } + } +} + +#[derive(Debug, Deserialize, Default)] +struct ModeExpectations { + return_type: Option, + diagnostics: Option>, +} + +#[derive(Debug, Deserialize)] +struct ExpectedDiagnostic { + source: String, + severity: String, +} + +fn parse_data_type(test: &TestCase) -> VariableType { + match &test.input { + Some(input_str) => { + // Try parsing as VariableType directly (for enum types etc.), + // fall back to JSON Value conversion + serde_json::from_str::(input_str).unwrap_or_else(|_| { + let input: Value = serde_json5::from_str(input_str) + .unwrap_or_else(|e| panic!("[{}] Failed to parse input: {e}", test.name)); + VariableType::from(input) + }) + } + None => VariableType::Any, + } +} + +fn run_analysis_inner(test: &TestCase, strict: bool, unary: bool) -> ExpressionAnalysis { + let mut is = IntelliSense::new().with_strict(strict); + let data_type = parse_data_type(test); + let rc = if unary { + is.analyze_unary(&test.expression, &data_type) + } else { + is.analyze(&test.expression, &data_type) + }; + std::rc::Rc::unwrap_or_clone(rc) +} + +fn parse_diagnostic_source(s: &str) -> DiagnosticSource { + match s { + "lexer" => DiagnosticSource::Lexer, + "parser" => DiagnosticSource::Parser, + "type_check" => DiagnosticSource::TypeCheck, + "compiler" => DiagnosticSource::Compiler, + other => panic!("Unknown diagnostic source: {other}"), + } +} + +fn parse_severity(s: &str) -> Severity { + match s { + "error" => Severity::Error, + "warning" => Severity::Warning, + other => panic!("Unknown severity: {other}"), + } +} + +fn run_test_file(file_name: &str, toml_data: &str) { + run_test_file_inner(file_name, toml_data, false); +} + +fn run_test_file_unary(file_name: &str, toml_data: &str) { + run_test_file_inner(file_name, toml_data, true); +} + +fn run_test_file_inner(file_name: &str, toml_data: &str, force_unary: bool) { + let test_file: TestFile = + toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}")); + + for test in &test_file.test { + let unary = test.unary || force_unary; + let has_mode_sections = test.loose.is_some() || test.strict.as_expectations().is_some(); + + if has_mode_sections { + if let Some(loose) = &test.loose { + let result = run_analysis_inner(test, false, unary); + check_mode(file_name, test, &result, loose, "loose"); + } + if let Some(strict) = test.strict.as_expectations() { + let result = run_analysis_inner(test, true, unary); + check_mode(file_name, test, &result, strict, "strict"); + } + } else { + let result = run_analysis_inner(test, test.strict.as_bool(), unary); + check_result(file_name, test, &result); + } + } +} + +fn check_mode( + file_name: &str, + test: &TestCase, + result: &ExpressionAnalysis, + expected: &ModeExpectations, + mode: &str, +) { + if let Some(expected_rt) = &expected.return_type { + let expected_type: VariableType = serde_json::from_str(expected_rt).unwrap_or_else(|e| { + panic!( + "[{file_name}:{}:{mode}] Failed to parse return_type '{}': {e}", + test.name, expected_rt + ) + }); + assert_eq!( + result.return_type, expected_type, + "[{file_name}:{}:{mode}] return_type mismatch.\n Expression: {}\n Expected: {:?}\n Got: {:?}", + test.name, test.expression, expected_type, result.return_type + ); + } + + if let Some(expected_reads) = &test.reads { + let actual_stripped: Vec<_> = result.reads.iter().map(|r| r.without_spans()).collect(); + let expected_stripped: Vec<_> = expected_reads.iter().map(|r| r.without_spans()).collect(); + assert_eq!( + actual_stripped, + expected_stripped, + "[{file_name}:{}:{mode}] reads mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, + test.expression, + serde_json::to_string_pretty(expected_reads).unwrap(), + serde_json::to_string_pretty(&result.reads).unwrap() + ); + } + + if let Some(expected_reads) = &test.reads_with_spans { + assert_eq!( + &result.reads, expected_reads, + "[{file_name}:{}:{mode}] reads_with_spans mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, test.expression, + serde_json::to_string_pretty(expected_reads).unwrap(), + serde_json::to_string_pretty(&result.reads).unwrap() + ); + } + + if let Some(expected_refs) = &test.references { + let actual: Vec<_> = result + .references + .iter() + .map(|r| r.without_via_alias()) + .collect(); + let expected: Vec<_> = expected_refs + .iter() + .map(|r| r.without_via_alias()) + .collect(); + assert_eq!( + actual, expected, + "[{file_name}:{}:{mode}] references mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, test.expression, + serde_json::to_string_pretty(expected_refs).unwrap(), + serde_json::to_string_pretty(&result.references).unwrap() + ); + } + + if let Some(expected_diags) = &expected.diagnostics { + check_diagnostics(file_name, test, result, expected_diags, mode); + } else if expected.return_type.is_some() || test.reads.is_some() { + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .collect(); + assert!( + errors.is_empty(), + "[{file_name}:{}:{mode}] Expected no error diagnostics, but got: {:?}\n Expression: {}", + test.name, errors, test.expression + ); + } +} + +fn check_result(file_name: &str, test: &TestCase, result: &ExpressionAnalysis) { + let mode = if test.strict.as_bool() { + "strict" + } else { + "loose" + }; + + if let Some(expected_rt) = &test.return_type { + let expected: VariableType = serde_json::from_str(expected_rt).unwrap_or_else(|e| { + panic!( + "[{file_name}:{}] Failed to parse return_type '{}': {e}", + test.name, expected_rt + ) + }); + assert_eq!( + result.return_type, expected, + "[{file_name}:{}:{mode}] return_type mismatch.\n Expression: {}\n Expected: {:?}\n Got: {:?}", + test.name, test.expression, expected, result.return_type + ); + } + + if let Some(expected_reads) = &test.reads { + let actual_stripped: Vec<_> = result.reads.iter().map(|r| r.without_spans()).collect(); + let expected_stripped: Vec<_> = expected_reads.iter().map(|r| r.without_spans()).collect(); + assert_eq!( + actual_stripped, + expected_stripped, + "[{file_name}:{}] reads mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, + test.expression, + serde_json::to_string_pretty(expected_reads).unwrap(), + serde_json::to_string_pretty(&result.reads).unwrap() + ); + } + + if let Some(expected_reads) = &test.reads_with_spans { + assert_eq!( + &result.reads, + expected_reads, + "[{file_name}:{}] reads_with_spans mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, + test.expression, + serde_json::to_string_pretty(expected_reads).unwrap(), + serde_json::to_string_pretty(&result.reads).unwrap() + ); + } + + if let Some(expected_refs) = &test.references { + assert_eq!( + &result.references, + expected_refs, + "[{file_name}:{}] references mismatch.\n Expression: {}\n Expected: {}\n Got: {}", + test.name, + test.expression, + serde_json::to_string_pretty(expected_refs).unwrap(), + serde_json::to_string_pretty(&result.references).unwrap() + ); + } + + if let Some(expected_diags) = &test.diagnostics { + check_diagnostics(file_name, test, result, expected_diags, mode); + } else if test.reads.is_some() || test.return_type.is_some() { + let errors: Vec<_> = result + .diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .collect(); + assert!( + errors.is_empty(), + "[{file_name}:{}:{mode}] Expected no error diagnostics, but got: {:?}\n Expression: {}", + test.name, errors, test.expression + ); + } +} + +fn check_diagnostics( + file_name: &str, + test: &TestCase, + result: &ExpressionAnalysis, + expected_diags: &[ExpectedDiagnostic], + mode: &str, +) { + assert!( + result.diagnostics.len() >= expected_diags.len(), + "[{file_name}:{}:{mode}] Expected at least {} diagnostics, got {}.\n Expression: {}\n Diagnostics: {:?}", + test.name, expected_diags.len(), result.diagnostics.len(), test.expression, result.diagnostics + ); + + for (i, expected) in expected_diags.iter().enumerate() { + let expected_source = parse_diagnostic_source(&expected.source); + let expected_severity = parse_severity(&expected.severity); + + let matching = result + .diagnostics + .iter() + .any(|d| d.source == expected_source && d.severity == expected_severity); + + assert!( + matching, + "[{file_name}:{}:{mode}] Diagnostic #{i} not found: expected source={}, severity={}.\n Expression: {}\n Got diagnostics: {:?}", + test.name, expected.source, expected.severity, test.expression, result.diagnostics + ); + } +} + +#[test] +fn analysis_types() { + run_test_file("types", include_str!("data/analysis/types.toml")); +} + +#[test] +fn analysis_dependencies() { + run_test_file( + "dependencies", + include_str!("data/analysis/dependencies.toml"), + ); +} + +#[test] +fn analysis_closures() { + run_test_file("closures", include_str!("data/analysis/closures.toml")); +} + +#[test] +fn analysis_functions() { + run_test_file("functions", include_str!("data/analysis/functions.toml")); +} + +#[test] +fn analysis_diagnostics() { + run_test_file( + "diagnostics", + include_str!("data/analysis/diagnostics.toml"), + ); +} + +#[test] +fn analysis_references() { + run_test_file("references", include_str!("data/analysis/references.toml")); +} + +#[test] +fn analysis_edge_cases() { + run_test_file("edge_cases", include_str!("data/analysis/edge_cases.toml")); +} + +#[derive(Debug, Deserialize)] +struct CompletionTestFile { + test: Vec, +} + +#[derive(Debug, Deserialize)] +struct CompletionTestCase { + name: String, + expression: String, + pos: u32, + input: Option, + #[serde(default)] + includes: Vec, + #[serde(default)] + excludes: Vec, +} + +fn run_completion_test_file(file_name: &str, toml_data: &str) { + let test_file: CompletionTestFile = + toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}")); + + for test in &test_file.test { + let mut is = IntelliSense::new(); + + let data_type = match &test.input { + Some(input_str) => { + let input: Value = serde_json5::from_str(input_str).unwrap_or_else(|e| { + panic!("[{file_name}:{}] Failed to parse input: {e}", test.name) + }); + VariableType::from(input) + } + None => VariableType::Any, + }; + + let completions = is.completions(&test.expression, test.pos, &data_type); + let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect(); + + for inc in &test.includes { + assert!( + labels.contains(&inc.as_str()), + "[{file_name}:{}] Expected completion '{}' not found.\n Expression: {:?} @ pos {}\n Got: {:?}", + test.name, inc, test.expression, test.pos, labels + ); + } + + for exc in &test.excludes { + assert!( + !labels.contains(&exc.as_str()), + "[{file_name}:{}] Unexpected completion '{}' found.\n Expression: {:?} @ pos {}\n Got: {:?}", + test.name, exc, test.expression, test.pos, labels + ); + } + } +} + +#[test] +fn analysis_completions() { + run_completion_test_file( + "completions", + include_str!("data/analysis/completions.toml"), + ); +} + +#[derive(Debug, Deserialize)] +struct InspectTestFile { + test: Vec, +} + +#[derive(Debug, Deserialize)] +struct InspectTestCase { + name: String, + expression: String, + pos: u32, + input: Option, + label: Option, + kind: Option, +} + +fn run_inspect_test_file(file_name: &str, toml_data: &str) { + let test_file: InspectTestFile = + toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}")); + + for test in &test_file.test { + let mut is = IntelliSense::new(); + + let data_type = match &test.input { + Some(input_str) => { + let input: Value = serde_json5::from_str(input_str).unwrap_or_else(|e| { + panic!("[{file_name}:{}] Failed to parse input: {e}", test.name) + }); + VariableType::from(input) + } + None => VariableType::Any, + }; + + let result = is.inspect(&test.expression, test.pos, &data_type); + + if test.label.is_some() || test.kind.is_some() { + let result = result.unwrap_or_else(|| { + panic!( + "[{file_name}:{}] Expected inspect result, got None.\n Expression: {:?} @ pos {}", + test.name, test.expression, test.pos + ) + }); + + if let Some(expected_label) = &test.label { + assert_eq!( + &result.label, expected_label, + "[{file_name}:{}] label mismatch.\n Expression: {:?} @ pos {}", + test.name, test.expression, test.pos + ); + } + + if let Some(expected_kind) = &test.kind { + let expected: VariableType = + serde_json::from_str(expected_kind).unwrap_or_else(|e| { + panic!( + "[{file_name}:{}] Failed to parse kind '{}': {e}", + test.name, expected_kind + ) + }); + assert_eq!( + result.kind, expected, + "[{file_name}:{}] kind mismatch.\n Expression: {:?} @ pos {}", + test.name, test.expression, test.pos + ); + } + } + } +} + +#[test] +fn analysis_inspect() { + run_inspect_test_file("inspect", include_str!("data/analysis/inspect.toml")); +} + +#[test] +fn analysis_unary() { + run_test_file_unary("unary", include_str!("data/analysis/unary.toml")); +} + +#[test] +fn analysis_enums() { + run_test_file("enums", include_str!("data/analysis/enums.toml")); +} + +#[test] +fn analysis_read_spans() { + run_test_file("read_spans", include_str!("data/analysis/read_spans.toml")); +} + +#[test] +fn analysis_nullable() { + run_test_file("nullable", include_str!("data/analysis/nullable.toml")); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn analysis_nested_closures_complete_quickly() { + let depth = 25; + let mut source = String::new(); + (0..depth).for_each(|_| source.push_str("map([0], ")); + source.push('1'); + (0..depth).for_each(|_| source.push(')')); + + let start = std::time::Instant::now(); + let mut is = IntelliSense::new().with_strict(true); + let analysis = is.analyze(&source, &VariableType::Any); + assert!( + start.elapsed() < std::time::Duration::from_secs(5), + "nested closure type-check took {:?}", + start.elapsed() + ); + + let errors: Vec<_> = analysis + .diagnostics + .iter() + .filter(|d| d.severity == Severity::Error) + .collect(); + assert!(errors.is_empty(), "unexpected diagnostics: {errors:?}"); +} + +#[test] +fn analysis_nested_closure_shallow_diagnostics_unchanged() { + let input = VariableType::from(serde_json::json!({"items": [1, 2]})); + let mut is = IntelliSense::new().with_strict(true); + let analysis = is.analyze("filter(items, # + 1)", &input); + assert!( + analysis + .diagnostics + .iter() + .any(|d| d.severity == Severity::Error && d.source == DiagnosticSource::TypeCheck), + "expected callback return-type error, got: {:?}", + analysis.diagnostics + ); +} + +#[test] +fn analysis_overlapping_enum_equality_has_no_hint() { + let input: VariableType = serde_json::from_str( + r#"{"Object":{"a":{"Enum":[null,["a","b"]]},"b":{"Enum":[null,["b","c"]]}}}"#, + ) + .unwrap(); + + for strict in [false, true] { + for expression in ["a == b", "a != b"] { + let mut is = IntelliSense::new().with_strict(strict); + let analysis = is.analyze(expression, &input); + assert!( + analysis.diagnostics.is_empty(), + "[{expression}:strict={strict}] expected no diagnostics, got: {:?}", + analysis.diagnostics + ); + } + } +} + +#[test] +fn analysis_structured_equality_hint_fires() { + let input: VariableType = serde_json::from_str( + r#"{"Object":{"a":{"Object":{"x":"Number"}},"b":{"Object":{"x":"Number"}},"c":{"Array":"Number"},"d":{"Array":"Number"}}}"#, + ) + .unwrap(); + + for strict in [false, true] { + for expression in ["a == b", "a != b", "c == d", "c != d"] { + let mut is = IntelliSense::new().with_strict(strict); + let analysis = is.analyze(expression, &input); + assert!( + analysis + .diagnostics + .iter() + .any(|d| d.severity == Severity::Warning + && d.source == DiagnosticSource::TypeCheck), + "[{expression}:strict={strict}] expected always-constant hint, got: {:?}", + analysis.diagnostics + ); + } + } +} + +#[test] +fn analysis_nullable_structured_equality_no_hint() { + let input: VariableType = serde_json::from_str( + r#"{"Object":{"a":{"Nullable":{"Object":{"x":"Number"}}},"b":{"Nullable":{"Object":{"x":"Number"}}},"c":{"Object":{"x":"Number"}}}}"#, + ) + .unwrap(); + + for strict in [false, true] { + for expression in ["a == b", "a != b", "c == null", "c != null", "a == c"] { + let mut is = IntelliSense::new().with_strict(strict); + let analysis = is.analyze(expression, &input); + assert!( + analysis.diagnostics.is_empty(), + "[{expression}:strict={strict}] expected no diagnostics, got: {:?}", + analysis.diagnostics + ); + } + } +} + +#[test] +fn analysis_disjoint_enum_equality_hint_fires() { + let input: VariableType = serde_json::from_str( + r#"{"Object":{"a":{"Enum":[null,["a","b"]]},"b":{"Enum":[null,["c","d"]]}}}"#, + ) + .unwrap(); + + for strict in [false, true] { + for expression in ["a == b", "a != b"] { + let mut is = IntelliSense::new().with_strict(strict); + let analysis = is.analyze(expression, &input); + assert!( + analysis + .diagnostics + .iter() + .any(|d| d.severity == Severity::Warning + && d.source == DiagnosticSource::TypeCheck), + "[{expression}:strict={strict}] expected always-constant hint, got: {:?}", + analysis.diagnostics + ); + } + } +} diff --git a/core/expression/tests/data/analysis/closures.toml b/core/expression/tests/data/analysis/closures.toml new file mode 100644 index 00000000..0a52a3f9 --- /dev/null +++ b/core/expression/tests/data/analysis/closures.toml @@ -0,0 +1,262 @@ +# Closure and iteration dependency tracking +# # = current iteration item (CallbackReference / Node::Pointer) +# Closures produce Iteration reads with nested inner reads +# Alias reads (e.g. item.price) appear as inner direct reads — this is the key +# information for cross-entity dependency resolution + +[[test]] +name = "filter with callback ref" +expression = "filter(items, # > 10)" +input = '{"items": [5, 15, 20]}' +reads = [ + { type = "iteration", collection = ["items"], reads = [] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "filter with alias" +expression = "filter(items as item, item.price > threshold)" +input = '{"items": [{"price": 5}], "threshold": 10}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "price"] }, + { type = "direct", path = ["threshold"] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "map with callback ref" +expression = "map(items, # * 2)" +input = '{"items": [1, 2, 3]}' +reads = [ + { type = "iteration", collection = ["items"], reads = [] }, +] + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +[[test]] +name = "map with alias" +expression = "map(orders as order, order.total)" +input = '{"orders": [{"total": 100}]}' +reads = [ + { type = "iteration", collection = ["orders"], alias = "order", reads = [ + { type = "direct", path = ["order", "total"] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "some with alias" +expression = "some(items as item, item.active)" +input = '{"items": [{"active": true}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "active"] }, + ] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "all with alias" +expression = "all(items as item, item.valid)" +input = '{"items": [{"valid": true}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "valid"] }, + ] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "none with alias" +expression = "none(items as item, item.expired)" +input = '{"items": [{"expired": false}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "expired"] }, + ] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "count with alias" +expression = "count(items as item, item.active)" +input = '{"items": [{"active": true}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "active"] }, + ] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "one with alias" +expression = "one(items as item, item.special)" +input = '{"items": [{"special": true}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "special"] }, + ] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "closure with external reference" +expression = "filter(items as item, item.price > minPrice)" +input = '{"items": [{"price": 5}], "minPrice": 3}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "price"] }, + { type = "direct", path = ["minPrice"] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "nested closure" +expression = "map(groups as group, filter(group.items as item, item.active))" +input = '{"groups": [{"items": [{"active": true}]}]}' +reads = [ + { type = "iteration", collection = ["groups"], alias = "group", reads = [ + { type = "iteration", collection = ["group", "items"], alias = "item", reads = [ + { type = "direct", path = ["item", "active"] }, + ] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "callback ref member access in closure" +expression = "map(items, #.price)" +input = '{"items": [{"price": 10}]}' +reads = [ + { type = "iteration", collection = ["items"], reads = [] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "flatMap with alias" +expression = "flatMap(groups as group, group.items)" +input = '{"groups": [{"items": [1, 2]}]}' +reads = [ + { type = "iteration", collection = ["groups"], alias = "group", reads = [ + { type = "direct", path = ["group", "items"] }, + ] }, +] + +# flatMap(coll, body) = flatten(map(coll, body)). Since `group.items` is +# Array, flatMap returns Array. +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +# A closure whose collection is itself a `filter(...)` binds the outer alias +# through the filter to the underlying collection, so `x.price` surfaces as a +# read of `items.price` (via the alias) rather than leaking an unbound `x`. +[[test]] +name = "alias bound through filter collection" +expression = "map(filter(items as i, i.active) as x, x.price)" +input = '{"items": [{"active": true, "price": 10}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "i", reads = [ + { type = "direct", path = ["i", "active"] }, + ] }, + { type = "iteration", collection = ["items"], alias = "x", reads = [ + { type = "direct", path = ["x", "price"] }, + ] }, +] + +[test.loose] + +[test.strict] + +# A map whose collection is `coll ?? []` binds the alias to `coll` so aliased +# reads in the body resolve through it instead of leaking an unbound alias. +[[test]] +name = "alias bound through nullish-coalesced collection" +expression = "map(items ?? [] as i, i.price)" +input = '{"items": [{"price": 10}]}' +reads = [ + { type = "direct", path = ["items"] }, + { type = "iteration", collection = ["items"], alias = "i", reads = [ + { type = "direct", path = ["i", "price"] }, + ] }, +] + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +[[test]] +name = "callback ref over any collection" +expression = "map(x, # > 1)" +input = '{"Object": {"x": "Any"}}' + +[test.loose] +return_type = '{"Array": "Bool"}' + +[test.strict] +return_type = '{"Array": "Bool"}' + +[[test]] +name = "callback ref misuse over number collection" +expression = "map(x, # + '')" +input = '{"Object": {"x": {"Array": "Number"}}}' + +[test.loose] +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +diagnostics = [{ source = "type_check", severity = "error" }] diff --git a/core/expression/tests/data/analysis/completions.toml b/core/expression/tests/data/analysis/completions.toml new file mode 100644 index 00000000..23128630 --- /dev/null +++ b/core/expression/tests/data/analysis/completions.toml @@ -0,0 +1,203 @@ +# Completion tests — Rust filters by prefix, returns only matching completions +# includes: these labels MUST appear in results +# excludes: these labels MUST NOT appear in results + +# ── Scope completions ──────────────────────────────────────────────────────── + +[[test]] +name = "scope at empty" +expression = "" +pos = 0 +input = '{"price": 10, "quantity": 5}' +includes = ["price", "quantity", "sum", "len", "map", "filter", "$root"] + +[[test]] +name = "scope filters by prefix" +expression = "p" +pos = 1 +input = '{"price": 10, "quantity": 5}' +includes = ["price"] +excludes = ["quantity", "sum", "len", "$root"] + +[[test]] +name = "scope prefix su" +expression = "su" +pos = 2 +input = '{"price": 10}' +includes = ["sum"] +excludes = ["price", "len", "filter"] + +[[test]] +name = "scope after operator" +expression = "a + " +pos = 4 +input = '{"a": 1, "b": 2}' +includes = ["a", "b", "sum", "len"] + +[[test]] +name = "scope after operator with prefix" +expression = "a + b" +pos = 5 +input = '{"a": 1, "b": 2, "bonus": 3}' +includes = ["b", "bonus", "bool"] +excludes = ["a", "sum", "len"] + +[[test]] +name = "scope after open paren" +expression = "sum(" +pos = 4 +input = '{"items": [1, 2]}' +includes = ["items", "sum"] + +[[test]] +name = "scope includes closure functions" +expression = "" +pos = 0 +input = '{"x": 1}' +includes = ["all", "none", "some", "one", "filter", "map", "flatMap", "count"] + +[[test]] +name = "scope includes math functions" +expression = "" +pos = 0 +input = '{"x": 1}' +includes = ["abs", "floor", "ceil", "round", "min", "max", "avg", "median"] + +[[test]] +name = "scope includes string functions" +expression = "" +pos = 0 +input = '{"x": 1}' +includes = ["upper", "lower", "trim", "contains", "startsWith", "endsWith", "split"] + +[[test]] +name = "scope prefix case insensitive" +expression = "S" +pos = 1 +input = '{"score": 10}' +includes = ["score", "sum", "split", "startsWith", "string"] +excludes = ["len", "filter"] + +# ── Property completions ───────────────────────────────────────────────────── + +[[test]] +name = "property after dot" +expression = "customer." +pos = 9 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +includes = ["firstName", "lastName"] +excludes = ["customer", "sum", "len", "$root"] + +[[test]] +name = "property filtered by prefix" +expression = "customer.first" +pos = 14 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +includes = ["firstName"] +excludes = ["lastName", "customer", "sum"] + +[[test]] +name = "property prefix filters to none" +expression = "customer.xyz" +pos = 12 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +excludes = ["firstName", "lastName"] + +[[test]] +name = "deep property" +expression = "customer.address." +pos = 17 +input = '{"customer": {"address": {"city": "NYC", "zip": "10001"}}}' +includes = ["city", "zip"] +excludes = ["address", "customer", "sum"] + +[[test]] +name = "deep property filtered" +expression = "customer.address.c" +pos = 18 +input = '{"customer": {"address": {"city": "NYC", "zip": "10001"}}}' +includes = ["city"] +excludes = ["zip"] + +[[test]] +name = "property on object with many fields" +expression = "order." +pos = 6 +input = '{"order": {"total": 100, "items": [1], "status": "active"}}' +includes = ["total", "items", "status"] +excludes = ["order", "sum"] + +[[test]] +name = "property does not include scope items" +expression = "data." +pos = 5 +input = '{"data": {"x": 1}}' +includes = ["x"] +excludes = ["sum", "len", "filter", "map", "$root"] + +# ── Edge cases ─────────────────────────────────────────────────────────────── + +[[test]] +name = "no input returns functions only" +expression = "" +pos = 0 +includes = ["sum", "len", "$root"] + +[[test]] +name = "dollar root prefix" +expression = "$" +pos = 1 +input = '{"x": 1}' +includes = ["$root"] +excludes = ["x", "sum"] + +[[test]] +name = "after binary returns scope" +expression = "a * b + " +pos = 8 +input = '{"a": 1, "b": 2, "c": 3}' +includes = ["a", "b", "c", "sum"] + +# ── Template strings ───────────────────────────────────────────────────────── + +[[test]] +name = "template string property after dot" +expression = '`Hello ${customer.}`' +pos = 18 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +includes = ["firstName", "lastName"] +excludes = ["customer", "sum"] + +[[test]] +name = "template string property filtered" +expression = '`${customer.first}`' +pos = 17 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +includes = ["firstName"] +excludes = ["lastName"] + +[[test]] +name = "template string scope inside expression" +expression = '`Hello ${}`' +pos = 9 +input = '{"name": "world", "greeting": "Hi"}' +includes = ["name", "greeting", "sum"] + +[[test]] +name = "incomplete template property completions" +expression = '`${customer.' +pos = 12 +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +includes = ["firstName", "lastName"] +excludes = ["customer", "sum"] + +# ── Lexer failure fallback ─────────────────────────────────────────────────── +# When the lexer fails (unmatched quotes/braces), we fall back to +# scope completions without prefix filtering since there's no parsed context. + +[[test]] +name = "unmatched quote falls back to scope" +expression = "'hello" +pos = 6 +input = '{"price": 10, "qty": 5}' +includes = ["price", "qty", "sum"] diff --git a/core/expression/tests/data/analysis/dependencies.toml b/core/expression/tests/data/analysis/dependencies.toml new file mode 100644 index 00000000..9a3f17a7 --- /dev/null +++ b/core/expression/tests/data/analysis/dependencies.toml @@ -0,0 +1,343 @@ +# Read dependency extraction + +# Member access + +[[test]] +name = "simple member access" +expression = "customer.firstName" +input = '{"customer": {"firstName": "John"}}' +reads = [ + { type = "direct", path = ["customer", "firstName"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "deep member access" +expression = "customer.address.city" +input = '{"customer": {"address": {"city": "NYC"}}}' +reads = [ + { type = "direct", path = ["customer", "address", "city"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "multiple member reads" +expression = "customer.firstName + customer.lastName" +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +reads = [ + { type = "direct", path = ["customer", "firstName"] }, + { type = "direct", path = ["customer", "lastName"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "array index access" +expression = "items[0]" +input = '{"items": [1, 2, 3]}' +reads = [ + { type = "direct", path = ["items"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "dynamic member access" +expression = "data[key]" +input = '{"data": {"a": 1}, "key": "a"}' +reads = [ + { type = "direct", path = ["data"] }, + { type = "direct", path = ["key"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "deeply nested member" +expression = "a.b.c.d.e" +input = '{"a": {"b": {"c": {"d": {"e": 42}}}}}' +reads = [ + { type = "direct", path = ["a", "b", "c", "d", "e"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "root reference" +expression = "$root.customer.name" +input = '{"customer": {"name": "John"}}' +reads = [ + { type = "direct", path = ["$root", "customer", "name"] }, +] + +[test.loose] + +[test.strict] + +# Assignments — local bindings, not external writes + +[[test]] +name = "assignment reads from value" +expression = "customer.fullName = customer.firstName + customer.lastName" +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +reads = [ + { type = "direct", path = ["customer", "firstName"] }, + { type = "direct", path = ["customer", "lastName"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "simple local does not leak as read" +expression = "x = a + b; y = x * 2" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "local with output" +expression = "x = a + b; y = x * 2; y + 1" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "local does not leak as read" +expression = "total = price * quantity; total" +input = '{"price": 10, "quantity": 5}' +reads = [ + { type = "direct", path = ["price"] }, + { type = "direct", path = ["quantity"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "assignment value reads before binding" +expression = "x = x + 1" +input = '{"x": 5}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "property binding reads from value" +expression = "order.total = order.price * order.quantity" +input = '{"order": {"price": 10, "quantity": 5}}' +reads = [ + { type = "direct", path = ["order", "price"] }, + { type = "direct", path = ["order", "quantity"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "assignment with iteration" +expression = "result.total = sum(map(items as item, item.price * item.qty))" +input = '{"items": [{"price": 10, "qty": 2}]}' +reads = [ + { type = "iteration", collection = ["items"], alias = "item", reads = [ + { type = "direct", path = ["item", "price"] }, + { type = "direct", path = ["item", "qty"] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "all static bindings produce no reads" +expression = "customer.firstName = 'something'; customer.lastName = 'else'; customer.firstName + ' ' + customer.lastName" +reads = [] + +[test.loose] + +[test.strict] + +[[test]] +name = "object binding makes identifier local" +expression = "customer = { firstName: 'a', lastName: 'b' }; customer.firstName + customer.lastName" +reads = [] + +[test.loose] + +[test.strict] + +[[test]] +name = "partial binding still reads unbound" +expression = "customer.firstName = 'John'; customer.firstName + customer.lastName" +input = '{"customer": {"lastName": "Doe"}}' +reads = [ + { type = "direct", path = ["customer", "lastName"] }, +] + +[test.loose] + +[test.strict] + +# No dependencies + +[[test]] +name = "no dependencies for pure literals" +expression = "1 + 2 * 3" + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "no reads for pure literal" +expression = "'hello'" +reads = [] + +[test.loose] + +[test.strict] + +[[test]] +name = "boolean expression no input" +expression = "true and false" +reads = [] + +[test.loose] + +[test.strict] + +# Multiple references + +[[test]] +name = "same variable referenced multiple times" +expression = "x + x + x" +input = '{"x": 1}' +reads = [ + { type = "direct", path = ["x"] }, + { type = "direct", path = ["x"] }, + { type = "direct", path = ["x"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "mixed arithmetic and member access" +expression = "(order.price * order.quantity) - order.discount" +input = '{"order": {"price": 100, "quantity": 2, "discount": 10}}' +reads = [ + { type = "direct", path = ["order", "price"] }, + { type = "direct", path = ["order", "quantity"] }, + { type = "direct", path = ["order", "discount"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "member access after array index" +expression = "order.items[0].price" +input = '{"order": {"items": [{"price": 10}]}}' +reads = [ + { type = "direct", path = ["order", "items"] }, + { type = "direct", path = ["order", "items", "price"], via_index = true }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "bare root read" +expression = "len(keys($root))" +input = '{"customer": {"firstName": "John"}}' +reads = [ + { type = "direct", path = ["$root"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "root identity read" +expression = "$root" +input = '{"customer": {"firstName": "John"}}' +reads = [ + { type = "direct", path = ["$root"] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "alias-rooted nested collection carries via_alias" +expression = "map(groups as group, filter(group.items as item, item.active))" +input = '{"groups": [{"items": [{"active": true}]}]}' +reads = [ + { type = "iteration", collection = ["groups"], alias = "group", reads = [ + { type = "iteration", collection = ["group", "items"], alias = "item", reads = [ + { type = "direct", path = ["item", "active"] }, + ] }, + ] }, +] +references = [ + { path = ["groups"], spans = [[4, 10]] }, + { path = ["group", "items"], spans = [[28, 33], [34, 39]], via_alias = { alias = "group", collection = ["groups"] } }, + { path = ["item", "active"], spans = [[49, 53], [54, 60]], via_alias = { alias = "item", collection = ["groups", "items"] } }, +] + +[[test]] +name = "bare alias collection carries via_alias" +expression = "map(groups as group, count(group, # > 0))" +input = '{"groups": [[1, 2], [3]]}' +reads = [ + { type = "iteration", collection = ["groups"], alias = "group", reads = [ + { type = "iteration", collection = ["group"], reads = [] }, + ] }, +] +references = [ + { path = ["groups"], spans = [[4, 10]] }, + { path = ["group"], spans = [[27, 32]], via_alias = { alias = "group", collection = ["groups"] } }, +] diff --git a/core/expression/tests/data/analysis/diagnostics.toml b/core/expression/tests/data/analysis/diagnostics.toml new file mode 100644 index 00000000..f55ce2bf --- /dev/null +++ b/core/expression/tests/data/analysis/diagnostics.toml @@ -0,0 +1,315 @@ +# Error and warning diagnostics from all pipeline stages + +# Type errors + +[[test]] +name = "type error: string minus number" +expression = "a - b" +input = '{"a": "hello", "b": 5}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Any"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Any"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "type error: not on number" +expression = "not a" +input = '{"a": 5}' +reads = [ + { type = "direct", path = ["a"] }, +] + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +# Type warnings + +[[test]] +name = "type warning: always false comparison" +expression = "a == b" +input = '{"a": "hello", "b": 5}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] + +[[test]] +name = "type warning: always true not-equal" +expression = "a != b" +input = '{"a": "hello", "b": 5}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] + +# Lexer errors + +[[test]] +name = "lexer error: invalid symbol" +expression = "@invalid" + +[test.loose] +diagnostics = [ + { source = "lexer", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "lexer", severity = "error" }, +] + +# Parser errors + +[[test]] +name = "parser error: incomplete expression" +expression = "a +" + +[test.loose] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +[[test]] +name = "parser error: unmatched paren" +expression = "(a + b" + +[test.loose] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +[[test]] +name = "unknown method call causes parse error" +expression = "name.upper()" +input = '{"name": "John"}' + +[test.loose] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "parser", severity = "error" }, +] + +# Invalid member access on typed objects + +[[test]] +name = "invalid member on typed object" +expression = "customer.nonExistent" +input = '{"customer": {"name": "John", "age": 30}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "valid member on typed object no error" +expression = "customer.name" +input = '{"customer": {"name": "John", "age": 30}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "invalid nested member" +expression = "order.customer.nonExistent" +input = '{"order": {"customer": {"name": "John"}}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "closure alias invalid member" +expression = "map(items as item, item.nonExistent)" +input = '{"items": [{"price": 10, "name": "a"}]}' + +[test.loose] +return_type = '{"Array":"Any"}' + +[test.strict] +return_type = '{"Array":"Any"}' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "closure alias valid member no error" +expression = "map(items as item, item.price)" +input = '{"items": [{"price": 10, "name": "a"}]}' + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +[[test]] +name = "closure callback ref invalid member" +expression = "map(items, #.nonExistent)" +input = '{"items": [{"price": 10, "name": "a"}]}' + +[test.loose] +return_type = '{"Array":"Any"}' + +[test.strict] +return_type = '{"Array":"Any"}' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "nested closure invalid inner member" +expression = "map(groups as g, sum(map(g.items as i, i.nonExistent)))" +input = '{"groups": [{"items": [{"value": 1}]}]}' + +[test.loose] + +[test.strict] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "empty object allows any member even in strict" +expression = "data.anything" +input = '{"data": {}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "conditional string vs number merges to any" +expression = "flag ? a : b" +input = '{"flag": true, "a": "hello", "b": 123}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "member on conditional any result is permissive in strict" +expression = "(flag ? obj1 : obj2).name" +input = '{"flag": true, "obj1": {"name": "John"}, "obj2": {"age": 30}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "slice with non-number from index" +expression = "arr[bad:2]" +input = '{"arr": [1, 2, 3], "bad": "x"}' + +[test.loose] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "slice with non-number to index" +expression = "arr[0:bad]" +input = '{"arr": [1, 2, 3], "bad": "x"}' + +[test.loose] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "valid slice has no diagnostics" +expression = "arr[1:2]" +input = '{"arr": [1, 2, 3]}' + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' diff --git a/core/expression/tests/data/analysis/edge_cases.toml b/core/expression/tests/data/analysis/edge_cases.toml new file mode 100644 index 00000000..60a509ee --- /dev/null +++ b/core/expression/tests/data/analysis/edge_cases.toml @@ -0,0 +1,81 @@ +# Edge cases, slices, intervals, and miscellaneous + +[[test]] +name = "empty-ish: just a number" +expression = "0" + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "parenthesized expression" +expression = "(a + b) * c" +input = '{"a": 1, "b": 2, "c": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, + { type = "direct", path = ["c"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "unary minus" +expression = "-x" +input = '{"x": 5}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "unary not" +expression = "not active" +input = '{"active": true}' +reads = [ + { type = "direct", path = ["active"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "interval" +expression = "x in [1..10]" +input = '{"x": 5}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "slice operation" +expression = "items[1:3]" +input = '{"items": [1, 2, 3, 4, 5]}' +reads = [ + { type = "direct", path = ["items"] }, +] + +[test.loose] + +[test.strict] diff --git a/core/expression/tests/data/analysis/enums.toml b/core/expression/tests/data/analysis/enums.toml new file mode 100644 index 00000000..ab2a2e00 --- /dev/null +++ b/core/expression/tests/data/analysis/enums.toml @@ -0,0 +1,327 @@ +# Enum and Const type support +# Input uses VariableType serialization for enum types: +# {"Object":{"field":{"Enum":[null,["a","b","c"]]}}} + +# ── Const return types ─────────────────────────────────────────────────────── + +[[test]] +name = "string literal is const" +expression = "'hello'" + +[test.loose] +return_type = '{"Const":"hello"}' + +[test.strict] +return_type = '{"Const":"hello"}' + +[[test]] +name = "string variable is string not const" +expression = "name" +input = '{"name": "world"}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "string concat is string" +expression = "'hello' + ' ' + 'world'" + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +# ── Enum return type inference ─────────────────────────────────────────────── + +[[test]] +name = "ternary produces enum" +expression = "x > 10 ? 'high' : 'low'" +input = '{"x": 15}' + +[test.loose] +return_type = '{"Enum":[null,["high","low"]]}' + +[test.strict] +return_type = '{"Enum":[null,["high","low"]]}' + +[[test]] +name = "nested ternary produces enum" +expression = "x > 100 ? 'high' : x > 50 ? 'mid' : 'low'" +input = '{"x": 75}' + +[test.loose] +return_type = '{"Enum":[null,["mid","low","high"]]}' + +[test.strict] +return_type = '{"Enum":[null,["mid","low","high"]]}' + +[[test]] +name = "assignment with enum return" +expression = "customer.status = customer.revenue > 1000 ? 'approved' : 'rejected'" +input = '{"Object":{"customer":{"Object":{"revenue":"Number"}}}}' + +[test.loose] +return_type = '{"Object":{"customer.status":{"Enum":[null,["approved","rejected"]]}}}' + +[test.strict] +return_type = '{"Object":{"customer.status":{"Enum":[null,["approved","rejected"]]}}}' + +[[test]] +name = "ternary with one const branch" +expression = "active ? 'yes' : name" +input = '{"active": true, "name": "hello"}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +# ── Enum comparison diagnostics ────────────────────────────────────────────── + +[[test]] +name = "valid enum comparison" +expression = "status == 'active'" +input = '{"Object":{"status":{"Enum":[null,["active","inactive","suspended"]]}}}' +reads = [ + { type = "direct", path = ["status"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "invalid enum comparison is error" +expression = "status == 'deleted'" +input = '{"Object":{"status":{"Enum":[null,["active","inactive","suspended"]]}}}' +reads = [ + { type = "direct", path = ["status"] }, +] + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "valid enum not equal" +expression = "status != 'active'" +input = '{"Object":{"status":{"Enum":[null,["active","inactive"]]}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "invalid enum not equal is error" +expression = "status != 'unknown'" +input = '{"Object":{"status":{"Enum":[null,["active","inactive"]]}}}' + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +# ── Enum in unary ──────────────────────────────────────────────────────────── + +[[test]] +name = "unary valid enum comparison" +expression = "== 'active'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "unary invalid enum comparison" +expression = "== 'deleted'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +# ── Unary simple value match ───────────────────────────────────────────────── + +[[test]] +name = "unary value match valid" +expression = "'active'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "unary value match invalid" +expression = "'deleted'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +# ── Comma-separated unary ──────────────────────────────────────────────────── + +[[test]] +name = "unary comma one invalid" +expression = "'active', 'deleted'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "unary comma all valid" +expression = "'active', 'inactive'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "unary comma all invalid" +expression = "'deleted', 'banned'" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +# ── Enum with in / not in ──────────────────────────────────────────────────── + +[[test]] +name = "unary in valid values" +expression = "in ['active', 'inactive']" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "unary not in" +expression = "not in ['active']" +input = '{"Object":{"$":{"Enum":[null,["active","inactive"]]}}}' +unary = true + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "enum in array of valid values" +expression = "status in ['active', 'inactive']" +input = '{"Object":{"status":{"Enum":[null,["active","inactive","suspended"]]}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "enum in array of overlapping values is not constant" +expression = "status in ['active', 'inactive']" +input = '{"Object":{"status":{"Enum":[null,["active","inactive","suspended"]]}}}' + +[test.loose] +return_type = '"Bool"' +diagnostics = [] + +[test.strict] +return_type = '"Bool"' +diagnostics = [] + +[[test]] +name = "enum in array of disjoint values is always false" +expression = "status in ['blocked', 'banned']" +input = '{"Object":{"status":{"Enum":[null,["active","inactive"]]}}}' + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "type_check", severity = "warning" }, +] diff --git a/core/expression/tests/data/analysis/functions.toml b/core/expression/tests/data/analysis/functions.toml new file mode 100644 index 00000000..60043c8d --- /dev/null +++ b/core/expression/tests/data/analysis/functions.toml @@ -0,0 +1,291 @@ +# Built-in function type inference and dependency tracking + +[[test]] +name = "sum function" +expression = "sum([a, b, c])" +input = '{"a": 1, "b": 2, "c": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, + { type = "direct", path = ["c"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "len function" +expression = "len(items)" +input = '{"items": [1, 2, 3]}' +reads = [ + { type = "direct", path = ["items"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "max function" +expression = "max([a, b])" +input = '{"a": 10, "b": 20}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "min function" +expression = "min([a, b])" +input = '{"a": 10, "b": 20}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "floor function" +expression = "floor(x)" +input = '{"x": 5.7}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "ceil function" +expression = "ceil(x)" +input = '{"x": 5.2}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "abs function" +expression = "abs(x)" +input = '{"x": -5}' +reads = [ + { type = "direct", path = ["x"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "upper function" +expression = "upper(name)" +input = '{"name": "hello"}' +reads = [ + { type = "direct", path = ["name"] }, +] + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "lower function" +expression = "lower(name)" +input = '{"name": "HELLO"}' +reads = [ + { type = "direct", path = ["name"] }, +] + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "contains function" +expression = "contains(text, 'world')" +input = '{"text": "hello world"}' +reads = [ + { type = "direct", path = ["text"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "startsWith function" +expression = "startsWith(text, 'hello')" +input = '{"text": "hello world"}' +reads = [ + { type = "direct", path = ["text"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "endsWith function" +expression = "endsWith(text, 'world')" +input = '{"text": "hello world"}' +reads = [ + { type = "direct", path = ["text"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "merge of an any-typed array" +expression = "merge([a, b])" +input = '{"Object":{"a":"Any","b":"Any"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge of empty array literal" +expression = "merge([])" + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge of scalar array errors" +expression = "merge([1, 2])" + +[test.loose] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[test.strict] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "merge nullable object with object" +expression = "merge([a, {y: 1}])" +input = '{"Object": {"a": {"Nullable": {"Object": {"x": "Number"}}}}}' + +[test.loose] +return_type = '{"Object": {"x": "Number", "y": "Number"}}' + +[test.strict] +return_type = '{"Object": {"x": "Number", "y": "Number"}}' + +[[test]] +name = "merge null literal with object" +expression = "merge([null, {y: 1}])" + +[test.loose] +return_type = '{"Object": {"y": "Number"}}' + +[test.strict] +return_type = '{"Object": {"y": "Number"}}' + +[[test]] +name = "mergeDeep nullable object with object" +expression = "mergeDeep([a, {y: 1}])" +input = '{"Object": {"a": {"Nullable": {"Object": {"x": "Number"}}}}}' + +[test.loose] +return_type = '{"Object": {"x": "Number", "y": "Number"}}' + +[test.strict] +return_type = '{"Object": {"x": "Number", "y": "Number"}}' + +[[test]] +name = "merge of non-array argument errors" +expression = "merge(a)" +input = '{"Object": {"a": {"Object": {"x": "Number"}}}}' + +[test.loose] +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "mergeDeep of scalar array errors" +expression = "mergeDeep([1, 2])" + +[test.loose] +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "date comparison methods return bool" +expression = "d(a).isSameOrAfter(d(b))" +input = '{"a": "2023-10-15", "b": "2023-10-14"}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "date comparison with unit returns bool" +expression = "d(a).isBefore(d(b), 'day')" +input = '{"a": "2023-10-15", "b": "2023-10-14"}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' diff --git a/core/expression/tests/data/analysis/inspect.toml b/core/expression/tests/data/analysis/inspect.toml new file mode 100644 index 00000000..68f330ea --- /dev/null +++ b/core/expression/tests/data/analysis/inspect.toml @@ -0,0 +1,54 @@ +# Inspect tests — type info at cursor position +# label: the source text at the inspected span +# kind: JSON-serialized VariableType + +[[test]] +name = "identifier" +expression = "price" +pos = 2 +input = '{"price": 42}' +label = "price" +kind = '"Number"' + +[[test]] +name = "member - on identifier part" +expression = "customer.firstName" +pos = 4 +input = '{"customer": {"firstName": "John"}}' +label = "customer" + +[[test]] +name = "member - on property part" +expression = "customer.firstName" +pos = 10 +input = '{"customer": {"firstName": "John"}}' +label = "firstName" + +[[test]] +name = "number literal" +expression = "42" +pos = 1 +label = "42" +kind = '"Number"' + +[[test]] +name = "string literal" +expression = "'hello'" +pos = 3 +label = "'hello'" + +[[test]] +name = "binary left operand" +expression = "a + b" +pos = 0 +input = '{"a": 1, "b": 2}' +label = "a" +kind = '"Number"' + +[[test]] +name = "binary right operand" +expression = "a + b" +pos = 4 +input = '{"a": 1, "b": 2}' +label = "b" +kind = '"Number"' diff --git a/core/expression/tests/data/analysis/nullable.toml b/core/expression/tests/data/analysis/nullable.toml new file mode 100644 index 00000000..deb1e261 --- /dev/null +++ b/core/expression/tests/data/analysis/nullable.toml @@ -0,0 +1,1053 @@ +# Nullable type system: merge behavior, member access, operators, strict mode +# +# Tests use [test.loose] and [test.strict] sections to verify both modes in one entry. + +# ============================================================================= +# Merge via ternary: T | null => Nullable(T) +# ============================================================================= + +[[test]] +name = "merge: string | null" +expression = "flag ? name : null" +input = '{"Object": {"flag": "Bool", "name": "String"}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "merge: null | string" +expression = "flag ? null : name" +input = '{"Object": {"flag": "Bool", "name": "String"}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "merge: number | null" +expression = "flag ? age : null" +input = '{"Object": {"flag": "Bool", "age": "Number"}}' + +[test.loose] +return_type = '{"Nullable": "Number"}' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "merge: null | number" +expression = "flag ? null : age" +input = '{"Object": {"flag": "Bool", "age": "Number"}}' + +[test.loose] +return_type = '{"Nullable": "Number"}' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "merge: bool | null" +expression = "flag ? active : null" +input = '{"Object": {"flag": "Bool", "active": "Bool"}}' + +[test.loose] +return_type = '{"Nullable": "Bool"}' + +[test.strict] +return_type = '{"Nullable": "Bool"}' + +[[test]] +name = "merge: date | null" +expression = "flag ? d : null" +input = '{"Object": {"flag": "Bool", "d": "Date"}}' + +[test.loose] +return_type = '{"Nullable": "Date"}' + +[test.strict] +return_type = '{"Nullable": "Date"}' + +[[test]] +name = "merge: null | null => Null" +expression = "flag ? null : null" +input = '{"Object": {"flag": "Bool"}}' + +[test.loose] +return_type = '"Null"' + +[test.strict] +return_type = '"Null"' + +[[test]] +name = "merge: same type no nullable" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": "String", "b": "String"}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "merge: same number no nullable" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": "Number", "b": "Number"}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "merge: same bool no nullable" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": "Bool", "b": "Bool"}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ============================================================================= +# Merge via ternary: Const / Enum interactions +# ============================================================================= + +[[test]] +name = "merge: const | null => Nullable(Const)" +expression = "flag ? 'hello' : null" +input = '{"Object": {"flag": "Bool"}}' + +[test.loose] +return_type = '{"Nullable": {"Const": "hello"}}' + +[test.strict] +return_type = '{"Nullable": {"Const": "hello"}}' + +[[test]] +name = "merge: null | const => Nullable(Const)" +expression = "flag ? null : 'world'" +input = '{"Object": {"flag": "Bool"}}' + +[test.loose] +return_type = '{"Nullable": {"Const": "world"}}' + +[test.strict] +return_type = '{"Nullable": {"Const": "world"}}' + +[[test]] +name = "merge: const | const => Enum" +expression = "flag ? 'hello' : 'world'" +input = '{"Object": {"flag": "Bool"}}' + +[test.loose] +return_type = '{"Enum": [null, ["hello", "world"]]}' + +[test.strict] +return_type = '{"Enum": [null, ["hello", "world"]]}' + +[[test]] +name = "merge: same const => Const" +expression = "flag ? 'same' : 'same'" +input = '{"Object": {"flag": "Bool"}}' + +[test.loose] +return_type = '{"Const": "same"}' + +[test.strict] +return_type = '{"Const": "same"}' + +[[test]] +name = "merge: const | string => String" +expression = "flag ? 'literal' : name" +input = '{"Object": {"flag": "Bool", "name": "String"}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "merge: string | const => String" +expression = "flag ? name : 'literal'" +input = '{"Object": {"flag": "Bool", "name": "String"}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +# ============================================================================= +# Merge via ternary: incompatible types +# ============================================================================= + +[[test]] +name = "merge: string | number => Any" +expression = "flag ? name : age" +input = '{"Object": {"flag": "Bool", "name": "String", "age": "Number"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge: bool | string => Any" +expression = "flag ? active : name" +input = '{"Object": {"flag": "Bool", "active": "Bool", "name": "String"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge: number | bool => Any" +expression = "flag ? age : active" +input = '{"Object": {"flag": "Bool", "age": "Number", "active": "Bool"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge: any | T => Any" +expression = "flag ? x : name" +input = '{"Object": {"flag": "Bool", "x": "Any", "name": "String"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge: T | any => Any" +expression = "flag ? name : x" +input = '{"Object": {"flag": "Bool", "name": "String", "x": "Any"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +# ============================================================================= +# Merge via ternary: Nullable inputs +# ============================================================================= + +[[test]] +name = "merge: Nullable(String) | Nullable(String)" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}, "b": {"Nullable": "String"}}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "merge: Nullable(Number) | Number" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "Number"}, "b": "Number"}}' + +[test.loose] +return_type = '{"Nullable": "Number"}' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "merge: Number | Nullable(Number)" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": "Number", "b": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '{"Nullable": "Number"}' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "merge: Nullable(String) | null" +expression = "flag ? a : null" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "merge: null | Nullable(Bool)" +expression = "flag ? null : a" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "Bool"}}}' + +[test.loose] +return_type = '{"Nullable": "Bool"}' + +[test.strict] +return_type = '{"Nullable": "Bool"}' + +[[test]] +name = "merge: Nullable(String) | Number => Any (incompatible inner)" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}, "b": "Number"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "merge: Any | Nullable(String) => Any" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": "Any", "b": {"Nullable": "String"}}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +# ============================================================================= +# Merge via ternary: arrays +# ============================================================================= + +[[test]] +name = "merge: array | null => Nullable(Array)" +expression = "flag ? items : null" +input = '{"Object": {"flag": "Bool", "items": {"Array": "Number"}}}' + +[test.loose] +return_type = '{"Nullable": {"Array": "Number"}}' + +[test.strict] +return_type = '{"Nullable": {"Array": "Number"}}' + +[[test]] +name = "merge: array | array => Array" +expression = "flag ? a : b" +input = '{"Object": {"flag": "Bool", "a": {"Array": "String"}, "b": {"Array": "String"}}}' + +[test.loose] +return_type = '{"Array": "String"}' + +[test.strict] +return_type = '{"Array": "String"}' + +# ============================================================================= +# Nullable member access +# ============================================================================= + +[[test]] +name = "member: nullable object property" +expression = "user.name" +input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "member: nullable object nested" +expression = "user.address" +input = '{"Object": {"user": {"Nullable": {"Object": {"address": {"Object": {"city": "String"}}}}}}}' + +[test.loose] +return_type = '{"Object": {"city": "String"}}' + +[test.strict] +return_type = '{"Nullable": {"Object": {"city": "String"}}}' + +[[test]] +name = "member: nullable object nested chain" +expression = "user.address.city" +input = '{"Object": {"user": {"Nullable": {"Object": {"address": {"Object": {"city": "String"}}}}}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "member: nullable array index" +expression = "items[0]" +input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "member: array index introduces nullable in strict" +expression = "items[0]" +input = '{"Object": {"items": {"Array": "Number"}}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "member: indexed property is nullable in strict" +expression = "companies[0].revenue" +input = '{"Object": {"companies": {"Array": {"Object": {"revenue": "Number"}}}}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "member: coalesce recovers from nullable index" +expression = "companies[0].revenue ?? 0" +input = '{"Object": {"companies": {"Array": {"Object": {"revenue": "Number"}}}}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "member: nullable object unknown key" +expression = "user.unknown" +input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "member: null type returns Null" +expression = "x.foo" +input = '{"Object": {"x": "Null"}}' + +[test.loose] +return_type = '"Null"' + +[test.strict] +return_type = '"Null"' + +# ============================================================================= +# Nullish coalescing (??) +# ============================================================================= + +[[test]] +name = "coalesce: Nullable(String) ?? const" +expression = "name ?? 'default'" +input = '{"Object": {"name": {"Nullable": "String"}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "coalesce: Nullable(Number) ?? number" +expression = "val ?? 0" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "coalesce: Null ?? number" +expression = "x ?? 42" +input = '{"Object": {"x": "Null"}}' + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "coalesce: non-nullable passes through" +expression = "name ?? 'fallback'" +input = '{"Object": {"name": "String"}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "coalesce: Nullable(Const) ?? const => Enum" +expression = "x ?? 'other'" +input = '{"Object": {"x": {"Nullable": {"Const": "hello"}}}}' + +[test.loose] +return_type = '{"Enum": [null, ["hello", "other"]]}' + +[test.strict] +return_type = '{"Enum": [null, ["hello", "other"]]}' + +[[test]] +name = "coalesce: Nullable(Bool) ?? bool" +expression = "active ?? false" +input = '{"Object": {"active": {"Nullable": "Bool"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "coalesce: Any ?? string => Any" +expression = "x ?? 'hi'" +input = '{"Object": {"x": "Any"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +# ============================================================================= +# Nullable in comparisons +# ============================================================================= + +[[test]] +name = "compare: nullable == literal" +expression = "val == 5" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "compare: nullable != literal" +expression = "val != 5" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "compare: nullable == null" +expression = "val == null" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "compare: nullable != null" +expression = "val != null" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "compare: nullable string == const" +expression = "name == 'hello'" +input = '{"Object": {"name": {"Nullable": "String"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "compare: nullable < number => error" +expression = "val < 10" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "compare: nullable > number => error" +expression = "val > 0" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +# ============================================================================= +# Nullable in arithmetic (must null-check first) +# ============================================================================= + +[[test]] +name = "arith: nullable + number => error" +expression = "val + 1" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "arith: nullable - number => error" +expression = "val - 1" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "arith: nullable * number => error" +expression = "val * 2" +input = '{"Object": {"val": {"Nullable": "Number"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "arith: nullable string + string => error" +expression = "name + ' world'" +input = '{"Object": {"name": {"Nullable": "String"}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +# ============================================================================= +# Nullable in logical operators +# ============================================================================= + +[[test]] +name = "logical: nullable and bool => error" +expression = "a and b" +input = '{"Object": {"a": {"Nullable": "Bool"}, "b": "Bool"}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "logical: nullable or bool => error" +expression = "a or b" +input = '{"Object": {"a": {"Nullable": "Bool"}, "b": "Bool"}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "logical: not nullable => error" +expression = "not a" +input = '{"Object": {"a": {"Nullable": "Bool"}}}' + +[test.loose] +return_type = '"Bool"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Bool"' +diagnostics = [{ source = "type_check", severity = "error" }] + +# ============================================================================= +# Edge cases +# ============================================================================= + +[[test]] +name = "edge: deeply nested ternary with nulls" +expression = "a ? (b ? x : null) : null" +input = '{"Object": {"a": "Bool", "b": "Bool", "x": "Number"}}' + +[test.loose] +return_type = '{"Nullable": "Number"}' + +[test.strict] +return_type = '{"Nullable": "Number"}' + +[[test]] +name = "edge: chained coalesce" +expression = "a ?? b ?? 'default'" +input = '{"Object": {"a": {"Nullable": "String"}, "b": {"Nullable": "String"}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "edge: coalesce then member" +expression = "(user ?? fallback).name" +input = '{"Object": {"user": {"Nullable": {"Object": {"name": "String"}}}, "fallback": {"Object": {"name": "String"}}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "edge: ternary producing Nullable then coalesce" +expression = "(flag ? name : null) ?? 'none'" +input = '{"Object": {"flag": "Bool", "name": "String"}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "edge: ternary with Any branch" +expression = "flag ? x : null" +input = '{"Object": {"flag": "Bool", "x": "Any"}}' + +[test.loose] +return_type = '"Any"' + +[test.strict] +return_type = '"Any"' + +[[test]] +name = "edge: double nullable merge stays single" +expression = "a ? (b ? x : null) : null" +input = '{"Object": {"a": "Bool", "b": "Bool", "x": "String"}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "edge: Nullable(Nullable) does not nest from merge" +expression = "flag ? a : null" +input = '{"Object": {"flag": "Bool", "a": {"Nullable": "String"}}}' + +[test.loose] +return_type = '{"Nullable": "String"}' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "edge: in operator with nullable array => error" +expression = "3 in items" +input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "edge: not in with nullable array => error" +expression = "3 not in items" +input = '{"Object": {"items": {"Nullable": {"Array": "Number"}}}}' + +[test.loose] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[test.strict] +return_type = '"Any"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "edge: in with non-nullable array works" +expression = "3 in items" +input = '{"Object": {"items": {"Array": "Number"}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ============================================================================= +# Nullable in closures +# ============================================================================= + +[[test]] +name = "closure: map over nullable array" +expression = "map(items as item, item.price)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}' + +[test.loose] +return_type = '{"Array": "Number"}' + +[test.strict] +return_type = '{"Array": "Number"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: map over nullable array with fallback" +expression = "map(items ?? [], #.price)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}' + +[test.loose] +return_type = '{"Array": "Number"}' + +[test.strict] +return_type = '{"Array": "Number"}' + +[[test]] +name = "closure: map unknown property on nullable array (strict error)" +expression = "map(items as item, item.unknown)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}' + +[test.loose] +return_type = '{"Array": "Any"}' + +[test.strict] +return_type = '{"Array": "Any"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: filter over nullable array" +expression = "filter(items as item, item.active)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"active": "Bool"}}}}}}' + +[test.loose] +return_type = '{"Nullable": {"Array": {"Object": {"active": "Bool"}}}}' + +[test.strict] +return_type = '{"Nullable": {"Array": {"Object": {"active": "Bool"}}}}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: some over nullable array" +expression = "some(items as item, item.active)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"active": "Bool"}}}}}}' + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: map over non-nullable array valid member" +expression = "map(companies as c, c.name)" +input = '{"Object": {"companies": {"Array": {"Object": {"name": "String", "revenue": "Number"}}}}}' + +[test.loose] +return_type = '{"Array": "String"}' + +[test.strict] +return_type = '{"Array": "String"}' + +[[test]] +name = "closure: map over non-nullable array unknown member (strict error)" +expression = "map(companies as c, c.unknownProperty)" +input = '{"Object": {"companies": {"Array": {"Object": {"name": "String", "revenue": "Number"}}}}}' + +[test.loose] +return_type = '{"Array": "Any"}' + +[test.strict] +return_type = '{"Array": "Any"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: callback ref on nullable array" +expression = "map(items, #.price)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}' + +[test.loose] +return_type = '{"Array": "Number"}' + +[test.strict] +return_type = '{"Array": "Number"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: callback ref unknown property (strict error)" +expression = "map(items, #.unknown)" +input = '{"Object": {"items": {"Nullable": {"Array": {"Object": {"price": "Number"}}}}}}' + +[test.loose] +return_type = '{"Array": "Any"}' + +[test.strict] +return_type = '{"Array": "Any"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: nested nullable array closure" +expression = "map(groups as g, map(g.items as item, item.value))" +input = '{"Object": {"groups": {"Nullable": {"Array": {"Object": {"items": {"Array": {"Object": {"value": "Number"}}}}}}}}}' + +[test.loose] +return_type = '{"Array": {"Array": "Number"}}' + +[test.strict] +return_type = '{"Array": {"Array": "Number"}}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "closure: nested nullable array closure with fallback" +expression = "map(groups ?? [], map(#.items, #.value))" +input = '{"Object": {"groups": {"Nullable": {"Array": {"Object": {"items": {"Array": {"Object": {"value": "Number"}}}}}}}}}' + +[test.loose] +return_type = '{"Array": {"Array": "Number"}}' + +[test.strict] +return_type = '{"Array": {"Array": "Number"}}' + +[[test]] +name = "string number index" +expression = "name[0]" +input = '{"name": "hello"}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "string number index concat" +expression = "name[0] + 'x'" +input = '{"name": "hello"}' + +[test.loose] +return_type = '"String"' + +[test.strict] +diagnostics = [ + { source = "type_check", severity = "error" }, +] + +[[test]] +name = "string number index coalesce concat" +expression = "(name[0] ?? '') + 'x'" +input = '{"name": "hello"}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "flatten nullable array strict error" +expression = "flatten(matrix)" +input = '{"Object": {"matrix": {"Nullable": {"Array": {"Array": "Number"}}}}}' + +[test.loose] +return_type = '{"Array": "Number"}' + +[test.strict] +return_type = '{"Array": "Number"}' +diagnostics = [{ source = "type_check", severity = "error" }] + +[[test]] +name = "flatten nullable array with fallback" +expression = "flatten(matrix ?? [])" +input = '{"Object": {"matrix": {"Nullable": {"Array": {"Array": "Number"}}}}}' + +[test.loose] +return_type = '{"Array": "Number"}' + +[test.strict] +return_type = '{"Array": "Number"}' + +[[test]] +name = "const string number index" +expression = "code[0]" +input = '{"Object": {"code": {"Const": "abc"}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '{"Nullable": "String"}' + +[[test]] +name = "enum string number index" +expression = "status[0]" +input = '{"Object": {"status": {"Enum": [null, ["active", "inactive"]]}}}' + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '{"Nullable": "String"}' diff --git a/core/expression/tests/data/analysis/read_spans.toml b/core/expression/tests/data/analysis/read_spans.toml new file mode 100644 index 00000000..65a2c27d --- /dev/null +++ b/core/expression/tests/data/analysis/read_spans.toml @@ -0,0 +1,116 @@ +# Read dependency span tracking + +[[test]] +name = "simple identifier span" +expression = "price" +input = '{"price": 10}' +reads_with_spans = [ + { type = "direct", path = ["price"], span = [0, 5] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "member access full span" +expression = "customer.firstName" +input = '{"customer": {"firstName": "John"}}' +reads_with_spans = [ + { type = "direct", path = ["customer", "firstName"], span = [0, 18] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "deep member span" +expression = "a.b.c" +input = '{"a": {"b": {"c": 1}}}' +reads_with_spans = [ + { type = "direct", path = ["a", "b", "c"], span = [0, 5] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "multiple reads each with span" +expression = "x + y" +input = '{"x": 1, "y": 2}' +reads_with_spans = [ + { type = "direct", path = ["x"], span = [0, 1] }, + { type = "direct", path = ["y"], span = [4, 5] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "binary with member spans" +expression = "order.price * order.quantity" +input = '{"order": {"price": 10, "quantity": 2}}' +reads_with_spans = [ + { type = "direct", path = ["order", "price"], span = [0, 11] }, + { type = "direct", path = ["order", "quantity"], span = [14, 28] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "iteration collection span" +expression = "map(items as item, item.price)" +input = '{"items": [{"price": 10}]}' +reads_with_spans = [ + { type = "iteration", collection = ["items"], span = [4, 9], alias = "item", reads = [ + { type = "direct", path = ["item", "price"], span = [19, 29] }, + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "filter with member collection span" +expression = "filter(order.items, $.active)" +input = '{"order": {"items": [{"active": true}]}}' +reads_with_spans = [ + { type = "iteration", collection = ["order", "items"], span = [7, 18], reads = [ + ] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "nested member in arithmetic" +expression = "customer.age + 10" +input = '{"customer": {"age": 30}}' +reads_with_spans = [ + { type = "direct", path = ["customer", "age"], span = [0, 12] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "conditional spans" +expression = "flag ? a.x : b.y" +input = '{"flag": true, "a": {"x": 1}, "b": {"y": 2}}' +reads_with_spans = [ + { type = "direct", path = ["flag"], span = [0, 4] }, + { type = "direct", path = ["a", "x"], span = [7, 10] }, + { type = "direct", path = ["b", "y"], span = [13, 16] }, +] + +[test.loose] + +[test.strict] diff --git a/core/expression/tests/data/analysis/references.toml b/core/expression/tests/data/analysis/references.toml new file mode 100644 index 00000000..cb2edbed --- /dev/null +++ b/core/expression/tests/data/analysis/references.toml @@ -0,0 +1,154 @@ +# Reference span tracking for rename refactoring +# path[i] corresponds to spans[i] — path for matching, spans for replacement + +[[test]] +name = "simple identifier reference" +expression = "x" +input = '{"x": 1}' +references = [ + { path = ["x"], spans = [[0, 1]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "member chain reference" +expression = "customer.firstName" +input = '{"customer": {"firstName": "John"}}' +references = [ + { path = ["customer", "firstName"], spans = [[0, 8], [9, 18]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "two member references in binary" +expression = "customer.firstName + customer.lastName" +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +references = [ + { path = ["customer", "firstName"], spans = [[0, 8], [9, 18]] }, + { path = ["customer", "lastName"], spans = [[21, 29], [30, 38]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "deep member chain" +expression = "a.b.c.d" +input = '{"a": {"b": {"c": {"d": 1}}}}' +references = [ + { path = ["a", "b", "c", "d"], spans = [[0, 1], [2, 3], [4, 5], [6, 7]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "iteration collection reference" +expression = "map(items as item, item.price)" +input = '{"items": [{"price": 10}]}' +references = [ + { path = ["items"], spans = [[4, 9]] }, + { path = ["item", "price"], spans = [[19, 23], [24, 29]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "iteration with external ref" +expression = "filter(items as item, item.price > minPrice)" +input = '{"items": [{"price": 5}], "minPrice": 3}' +references = [ + { path = ["items"], spans = [[7, 12]] }, + { path = ["item", "price"], spans = [[22, 26], [27, 32]] }, + { path = ["minPrice"], spans = [[35, 43]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "nested iteration references" +expression = "map(groups as group, map(group.items as item, item.total))" +input = '{"groups": [{"items": [{"total": 1}]}]}' +references = [ + { path = ["groups"], spans = [[4, 10]] }, + { path = ["group", "items"], spans = [[25, 30], [31, 36]] }, + { path = ["item", "total"], spans = [[46, 50], [51, 56]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "assignment does not produce references for writes" +expression = "customer.fullName = customer.firstName + customer.lastName" +input = '{"customer": {"firstName": "John", "lastName": "Doe"}}' +references = [ + { path = ["customer", "firstName"], spans = [[20, 28], [29, 38]] }, + { path = ["customer", "lastName"], spans = [[41, 49], [50, 58]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "locals do not produce references" +expression = "x = a + b; x * 2" +input = '{"a": 1, "b": 2}' +references = [ + { path = ["a"], spans = [[4, 5]] }, + { path = ["b"], spans = [[8, 9]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "conditional references" +expression = "active ? price : discount" +input = '{"active": true, "price": 100, "discount": 10}' +references = [ + { path = ["active"], spans = [[0, 6]] }, + { path = ["price"], spans = [[9, 14]] }, + { path = ["discount"], spans = [[17, 25]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "no references for literals" +expression = "1 + 2 * 3" +references = [] + +[test.loose] + +[test.strict] + +[[test]] +name = "template string references" +expression = '`${first} ${last}`' +input = '{"first": "John", "last": "Doe"}' +references = [ + { path = ["first"], spans = [[3, 8]] }, + { path = ["last"], spans = [[12, 16]] }, +] + +[test.loose] + +[test.strict] diff --git a/core/expression/tests/data/analysis/types.toml b/core/expression/tests/data/analysis/types.toml new file mode 100644 index 00000000..1d6b6f49 --- /dev/null +++ b/core/expression/tests/data/analysis/types.toml @@ -0,0 +1,456 @@ +# Return type inference for basic types, arithmetic, comparisons, and logical ops + +[[test]] +name = "number literal" +expression = "42" + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "string literal" +expression = "'hello'" + +[test.loose] +return_type = '{"Const":"hello"}' + +[test.strict] +return_type = '{"Const":"hello"}' + +[[test]] +name = "boolean literal" +expression = "true" + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "null literal" +expression = "null" + +[test.loose] +return_type = '"Null"' + +[test.strict] +return_type = '"Null"' + +# Arithmetic + +[[test]] +name = "number addition" +expression = "a + b" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "string concatenation" +expression = "a + b" +input = '{"a": "hello", "b": " world"}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "number subtraction" +expression = "a - b" +input = '{"a": 10, "b": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "number multiplication" +expression = "a * b" +input = '{"a": 5, "b": 4}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "number division" +expression = "a / b" +input = '{"a": 10, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "number modulus" +expression = "a % b" +input = '{"a": 10, "b": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "number exponent" +expression = "a ^ b" +input = '{"a": 2, "b": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +# Comparisons + +[[test]] +name = "equality" +expression = "a == b" +input = '{"a": 1, "b": 1}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "inequality" +expression = "a != b" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "less than" +expression = "a < b" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "greater than or equal" +expression = "a >= b" +input = '{"a": 5, "b": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "in operator" +expression = "a in b" +input = '{"a": 1, "b": [1, 2, 3]}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "not in operator" +expression = "a not in b" +input = '{"a": 1, "b": [1, 2, 3]}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# Logical + +[[test]] +name = "logical and" +expression = "a and b" +input = '{"a": true, "b": false}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "logical or" +expression = "a or b" +input = '{"a": true, "b": false}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "logical not" +expression = "not a" +input = '{"a": true}' +reads = [ + { type = "direct", path = ["a"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# Nullish coalescing + +[[test]] +name = "nullish coalescing" +expression = "a ?? b" +input = '{"a": null, "b": 5}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +# Conditional + +[[test]] +name = "ternary expression" +expression = "active ? price * rate : 0" +input = '{"active": true, "price": 100, "rate": 1.5}' +reads = [ + { type = "direct", path = ["active"] }, + { type = "direct", path = ["price"] }, + { type = "direct", path = ["rate"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +[[test]] +name = "ternary with string branches" +expression = "status ? 'yes' : 'no'" +input = '{"status": true}' +reads = [ + { type = "direct", path = ["status"] }, +] + +[test.loose] +return_type = '{"Enum":[null,["yes","no"]]}' + +[test.strict] +return_type = '{"Enum":[null,["yes","no"]]}' + +[[test]] +name = "nested ternary" +expression = "a > 10 ? a > 20 ? 'high' : 'mid' : 'low'" +input = '{"a": 15}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["a"] }, +] + +[test.loose] +return_type = '{"Enum":[null,["high","mid","low"]]}' + +[test.strict] +return_type = '{"Enum":[null,["high","mid","low"]]}' + +# Arrays and objects + +[[test]] +name = "array literal with vars" +expression = "[a, b, c]" +input = '{"a": 1, "b": 2, "c": 3}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, + { type = "direct", path = ["c"] }, +] + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +[[test]] +name = "object literal with vars" +expression = "{ x: a, y: b }" +input = '{"a": 1, "b": 2}' +reads = [ + { type = "direct", path = ["a"] }, + { type = "direct", path = ["b"] }, +] + +[test.loose] + +[test.strict] + +# Template strings + +[[test]] +name = "template string" +expression = '`Hello ${name}`' +input = '{"name": "world"}' +reads = [ + { type = "direct", path = ["name"] }, +] + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +[[test]] +name = "template string multiple vars" +expression = '`${first} ${last}`' +input = '{"first": "John", "last": "Doe"}' +reads = [ + { type = "direct", path = ["first"] }, + { type = "direct", path = ["last"] }, +] + +[test.loose] +return_type = '"String"' + +[test.strict] +return_type = '"String"' + +# Slicing an array yields an array of the same element type (not the element). +# Indexing (`arr[i]`) is a separate node that yields the element. +[[test]] +name = "array slice yields array" +expression = "numbers[0:2]" +input = '{"numbers": [1, 2, 3]}' +reads = [ + { type = "direct", path = ["numbers"] }, +] + +[test.loose] +return_type = '{"Array":"Number"}' + +[test.strict] +return_type = '{"Array":"Number"}' + +# A slice feeding an aggregate that expects an array must type-check. +[[test]] +name = "sum over array slice" +expression = "sum(numbers[0:1])" +input = '{"numbers": [10, 20, 30]}' +reads = [ + { type = "direct", path = ["numbers"] }, +] + +[test.loose] +return_type = '"Number"' + +[test.strict] +return_type = '"Number"' + +# Empty-array branch unifies with a typed-array branch instead of widening to +# `any[]` — the empty literal carries no element constraint. +[[test]] +name = "ternary empty array unifies with typed array" +expression = "flag ? [] : [{ amount: 1 }]" +input = '{"flag": true}' + +[test.loose] +return_type = '{"Array":{"Object":{"amount":"Number"}}}' + +[test.strict] +return_type = '{"Array":{"Object":{"amount":"Number"}}}' diff --git a/core/expression/tests/data/analysis/unary.toml b/core/expression/tests/data/analysis/unary.toml new file mode 100644 index 00000000..f5c4884a --- /dev/null +++ b/core/expression/tests/data/analysis/unary.toml @@ -0,0 +1,311 @@ +# Unary expression analysis — conditions tested against $ (current value) +# return_type is always Bool +# In unary, $ is the implicit left operand. The caller provides $ in the data. + +# ── Basic comparisons ──────────────────────────────────────────────────────── + +[[test]] +name = "greater than literal" +expression = "> 5" +input = '{"$": 10}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "less than literal" +expression = "< 10" +input = '{"$": 5}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "greater than or equal" +expression = ">= 100" +input = '{"$": 100}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "equality check string" +expression = "== 'active'" +input = '{"$": "active"}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "not equal" +expression = "!= null" +input = '{"$": 5}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── Comparisons with variables ─────────────────────────────────────────────── + +[[test]] +name = "greater than variable" +expression = "> threshold" +input = '{"$": 15, "threshold": 10}' +reads = [ + { type = "direct", path = ["threshold"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "equal to member access" +expression = "== config.minValue" +input = '{"$": 5, "config": {"minValue": 5}}' +reads = [ + { type = "direct", path = ["config", "minValue"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "comparison with two variables" +expression = "> minPrice and < maxPrice" +input = '{"$": 50, "minPrice": 10, "maxPrice": 100}' +reads = [ + { type = "direct", path = ["minPrice"] }, + { type = "direct", path = ["maxPrice"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── Logical combinations ───────────────────────────────────────────────────── + +[[test]] +name = "and combination" +expression = "> 5 and < 10" +input = '{"$": 7}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "or combination" +expression = ">= 10 or < 5" +input = '{"$": 10}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "comma separated (or)" +expression = "> 10, < -5" +input = '{"$": 15}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── Intervals ──────────────────────────────────────────────────────────────── + +[[test]] +name = "closed interval" +expression = "[-10..10]" +input = '{"$": 0}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "open interval" +expression = "(-10..10)" +input = '{"$": 0}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "interval with variable bounds" +expression = "[minVal..maxVal]" +input = '{"$": 50, "minVal": 0, "maxVal": 100}' +reads = [ + { type = "direct", path = ["minVal"] }, + { type = "direct", path = ["maxVal"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "interval combined with comparison" +expression = "[-10..0] and > -5" +input = '{"$": -3}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── In operator ────────────────────────────────────────────────────────────── + +[[test]] +name = "in array literal" +expression = "in ['a', 'b', 'c']" +input = '{"$": "a"}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "in variable array" +expression = "in allowedValues" +input = '{"$": "x", "allowedValues": ["x", "y"]}' +reads = [ + { type = "direct", path = ["allowedValues"] }, +] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "not in array" +expression = "not in ['blocked', 'banned']" +input = '{"$": "ok"}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── Boolean literals ───────────────────────────────────────────────────────── + +[[test]] +name = "true literal" +expression = "true" +input = '{"$": true}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +[[test]] +name = "false literal" +expression = "false" +input = '{"$": false}' +reads = [] + +[test.loose] +return_type = '"Bool"' + +[test.strict] +return_type = '"Bool"' + +# ── References ─────────────────────────────────────────────────────────────── + +[[test]] +name = "variable reference has spans" +expression = "> threshold" +input = '{"$": 15, "threshold": 10}' +references = [ + { path = ["threshold"], spans = [[2, 11]] }, +] + +[test.loose] + +[test.strict] + +[[test]] +name = "member reference has spans" +expression = "== config.limit" +input = '{"$": 5, "config": {"limit": 5}}' +references = [ + { path = ["config", "limit"], spans = [[3, 9], [10, 15]] }, +] + +[test.loose] + +[test.strict] + +# ── Diagnostics ────────────────────────────────────────────────────────────── + +[[test]] +name = "lexer error in unary" +expression = "@" + +[test.loose] +return_type = '"Bool"' +diagnostics = [ + { source = "lexer", severity = "error" }, +] + +[test.strict] +return_type = '"Bool"' +diagnostics = [ + { source = "lexer", severity = "error" }, +] diff --git a/core/expression/tests/isolate.rs b/core/expression/tests/isolate.rs index 1bb79915..e197f46b 100644 --- a/core/expression/tests/isolate.rs +++ b/core/expression/tests/isolate.rs @@ -641,6 +641,34 @@ fn isolate_standard_test() { }, ]), }, + TestEnv { + env: json!({ + "name": "hello", + "zero": 0, + }), + cases: Vec::from([ + TestCase { + expr: "name[0]", + result: json!("h"), + }, + TestCase { + expr: "name[zero]", + result: json!("h"), + }, + TestCase { + expr: "name[4]", + result: json!("o"), + }, + TestCase { + expr: "name[100]", + result: json!(null), + }, + TestCase { + expr: "name[zero + 100]", + result: json!(null), + }, + ]), + }, ]); let mut isolate = Isolate::new(); diff --git a/core/expression/tests/lexer.rs b/core/expression/tests/lexer.rs index ba2fccfd..413ddf50 100644 --- a/core/expression/tests/lexer.rs +++ b/core/expression/tests/lexer.rs @@ -451,13 +451,14 @@ fn lexer_test() { ]); let mut lexer = Lexer::new(); + let bump = bumpalo::Bump::new(); for LexerTest { test, result } in tests { - let tokens = lexer.tokenize(test); + let tokens = lexer.tokenize(&bump, test); assert!(tokens.is_ok()); assert_eq!( - tokens.unwrap(), + tokens.unwrap().as_slice(), result.as_slice(), "Expression failed: {test}" ); diff --git a/core/expression/tests/standard.rs b/core/expression/tests/standard.rs index f534d354..d90f8279 100644 --- a/core/expression/tests/standard.rs +++ b/core/expression/tests/standard.rs @@ -295,23 +295,13 @@ fn standard_test() { let mut bump = Bump::new(); for StandardTest { src, result } in tests { - let tokens = lexer.tokenize(src).unwrap(); - let unary_parser = Parser::try_new(tokens, &bump).unwrap().standard(); + bump.reset(); + let tokens = lexer.tokenize(&bump, src).unwrap(); + let unary_parser = Parser::try_new(&tokens, &bump).unwrap().standard(); let parser_result = unary_parser.parse(); - // let Ok(ast) = parser_result else { - // assert!( - // false, - // "Failed on expression: {}. Error: {:?}.", - // src, - // parser_result.unwrap_err() - // ); - // return; - // }; assert!(parser_result.error().is_ok(), "Expression failed: {src}"); assert_eq!(parser_result.root, result, "Failed on expression: {}", src); - - bump.reset(); } } @@ -323,12 +313,11 @@ fn failure_tests() { let mut bump = Bump::new(); for test in tests { - let tokens = lexer.tokenize(test).unwrap(); - let parser = Parser::try_new(tokens, &bump).unwrap().standard(); + bump.reset(); + let tokens = lexer.tokenize(&bump, test).unwrap(); + let parser = Parser::try_new(&tokens, &bump).unwrap().standard(); let parser_result = parser.parse(); assert!(parser_result.error().is_err(), "{parser_result:?}"); - - bump.reset(); } } diff --git a/core/expression/tests/unary.rs b/core/expression/tests/unary.rs index c7ba1ac7..3d1123b7 100644 --- a/core/expression/tests/unary.rs +++ b/core/expression/tests/unary.rs @@ -143,14 +143,13 @@ fn unary_test() { let mut bump = Bump::new(); for UnaryTest { src, result } in tests { - let tokens = lexer.tokenize(src).unwrap(); - let parser = Parser::try_new(tokens, &bump).unwrap().unary(); + bump.reset(); + let tokens = lexer.tokenize(&bump, src).unwrap(); + let parser = Parser::try_new(&tokens, &bump).unwrap().unary(); let parser_result = parser.parse(); assert!(parser_result.error().is_ok(), "Parser failed"); assert_eq!(parser_result.root, result); - - bump.reset(); } } @@ -162,15 +161,14 @@ fn failure_tests() { let mut bump = Bump::new(); for test in tests { - let tokens = lexer.tokenize(test).unwrap(); - let unary_parser = Parser::try_new(tokens, &bump).unwrap().standard(); + bump.reset(); + let tokens = lexer.tokenize(&bump, test).unwrap(); + let unary_parser = Parser::try_new(&tokens, &bump).unwrap().standard(); let parser_result = unary_parser.parse(); assert!( parser_result.error().is_err(), "Parsing expected to fail for: {test}" ); - - bump.reset(); } } diff --git a/core/expression_repl/src/main.rs b/core/expression_repl/src/main.rs index 69ca6759..f30b49df 100644 --- a/core/expression_repl/src/main.rs +++ b/core/expression_repl/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + use colored::Colorize; use rustyline::config::Configurer; use rustyline::{DefaultEditor, Result}; diff --git a/core/macros/src/lib.rs b/core/macros/src/lib.rs index 109b12cf..9beff4e6 100644 --- a/core/macros/src/lib.rs +++ b/core/macros/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + use proc_macro::TokenStream; mod to_variable; diff --git a/core/template/src/interpreter.rs b/core/template/src/interpreter.rs index 433a2b5a..30d7862f 100644 --- a/core/template/src/interpreter.rs +++ b/core/template/src/interpreter.rs @@ -16,7 +16,7 @@ pub(crate) enum InterpreterResult<'a> { #[derive(Debug)] pub(crate) struct Interpreter<'source, 'nodes> { cursor: Peekable>>, - isolate: Isolate<'source>, + isolate: Isolate, results: Vec>, } diff --git a/core/template/src/lib.rs b/core/template/src/lib.rs index 9f142c06..27d835c4 100644 --- a/core/template/src/lib.rs +++ b/core/template/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + mod error; mod interpreter; mod lexer; diff --git a/core/types/src/decision/mod.rs b/core/types/src/decision/mod.rs index 4fa51bda..bf1c75ca 100644 --- a/core/types/src/decision/mod.rs +++ b/core/types/src/decision/mod.rs @@ -116,9 +116,10 @@ pub struct DecisionTableContent { pub transform_attributes: TransformAttributes, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum DecisionTableHitPolicy { + #[default] First, Collect, } @@ -127,6 +128,7 @@ pub enum DecisionTableHitPolicy { #[serde(rename_all = "camelCase")] pub struct DecisionTableInputField { pub id: Arc, + #[serde(default = "empty_arc_str")] pub name: Arc, #[serde(default, deserialize_with = "empty_string_is_none")] pub field: Option>, @@ -136,10 +138,15 @@ pub struct DecisionTableInputField { #[serde(rename_all = "camelCase")] pub struct DecisionTableOutputField { pub id: Arc, + #[serde(default = "empty_arc_str")] pub name: Arc, pub field: Arc, } +fn empty_arc_str() -> Arc { + Arc::from("") +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExpressionNodeContent { diff --git a/core/types/src/lib.rs b/core/types/src/lib.rs index 660e80b0..c0934aba 100644 --- a/core/types/src/lib.rs +++ b/core/types/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + mod constant; pub mod decision; pub mod rcvalue; diff --git a/core/types/src/variable/mod.rs b/core/types/src/variable/mod.rs index 57a90a2b..4ced7d80 100644 --- a/core/types/src/variable/mod.rs +++ b/core/types/src/variable/mod.rs @@ -27,6 +27,11 @@ pub(crate) type RcCell = Rc>; pub type VariableMap = HashMap, Variable>; +thread_local! { + static DOLLAR_KEY: Rc = Rc::from("$"); + static ROOT_KEY: Rc = Rc::from("$root"); +} + pub enum Variable { Null, Bool(bool), @@ -46,6 +51,14 @@ pub trait DynamicVariable: Display { } impl Variable { + pub fn dollar_key() -> Rc { + DOLLAR_KEY.with(Rc::clone) + } + + pub fn root_key() -> Rc { + ROOT_KEY.with(Rc::clone) + } + pub fn from_array(arr: Vec) -> Self { Self::Array(Rc::new(RefCell::new(arr))) } diff --git a/core/types/src/variable/ref_deser.rs b/core/types/src/variable/ref_deser.rs index d3762141..26d741c3 100644 --- a/core/types/src/variable/ref_deser.rs +++ b/core/types/src/variable/ref_deser.rs @@ -67,7 +67,7 @@ impl RefDeserializer { } let root_value = root_obj - .remove(&Rc::from("$root")) + .remove(&Variable::root_key()) .ok_or_else(|| RefDeserializeError::InvalidFormat("Missing $root".into()))?; self.deserialize_value(&root_value) diff --git a/core/types/src/variable/ref_ser.rs b/core/types/src/variable/ref_ser.rs index 047e3d07..d96e7520 100644 --- a/core/types/src/variable/ref_ser.rs +++ b/core/types/src/variable/ref_ser.rs @@ -58,7 +58,7 @@ impl RefSerializer { result.insert(Rc::from("$refs"), RcValue::Array(self.ref_data)); } - result.insert(Rc::from("$root"), data); + result.insert(Variable::root_key(), data); RcValue::Object(result) } diff --git a/core/types/src/variable_type/conv.rs b/core/types/src/variable_type/conv.rs index 943b5bda..648e92f2 100644 --- a/core/types/src/variable_type/conv.rs +++ b/core/types/src/variable_type/conv.rs @@ -48,7 +48,7 @@ impl From<&Value> for VariableType { impl From> for VariableType { fn from(arr: Vec) -> Self { - if arr.len() == 0 { + if arr.is_empty() { return VariableType::Array(Rc::new(VariableType::Any)); } @@ -65,7 +65,7 @@ impl From> for VariableType { impl From<&Vec> for VariableType { fn from(arr: &Vec) -> Self { - if arr.len() == 0 { + if arr.is_empty() { return VariableType::Array(Rc::new(VariableType::Any)); } diff --git a/core/types/src/variable_type/mod.rs b/core/types/src/variable_type/mod.rs index dabcf776..0dbd68e7 100644 --- a/core/types/src/variable_type/mod.rs +++ b/core/types/src/variable_type/mod.rs @@ -23,6 +23,7 @@ pub enum VariableType { Const(Rc), Enum(Option>, Vec>), + Nullable(Rc), } impl VariableType { @@ -69,6 +70,7 @@ impl Display for VariableType { } VariableType::Array(v) => write!(f, "{v}[]"), VariableType::Object(_) => write!(f, "object"), + VariableType::Nullable(inner) => write!(f, "{inner}?"), } } } @@ -105,6 +107,10 @@ impl Hash for VariableType { Hash::hash(&pairs, state); } + VariableType::Nullable(inner) => { + 11.hash(state); + inner.hash(state); + } } } } diff --git a/core/types/src/variable_type/util.rs b/core/types/src/variable_type/util.rs index 03e17d18..686eb897 100644 --- a/core/types/src/variable_type/util.rs +++ b/core/types/src/variable_type/util.rs @@ -6,10 +6,18 @@ use std::collections::hash_map::Entry; use std::rc::Rc; impl VariableType { + pub fn unwrap_nullable(&self) -> (&VariableType, bool) { + match self { + VariableType::Nullable(inner) => (inner.as_ref(), true), + other => (other, false), + } + } + pub fn iterator(&self) -> Option> { match self { VariableType::Array(item) => Some(item.clone()), VariableType::Interval => Some(Rc::new(VariableType::Number)), + VariableType::Nullable(inner) => inner.iterator(), _ => None, } } @@ -17,6 +25,7 @@ impl VariableType { pub fn as_const_str(&self) -> Option> { match self { VariableType::Const(s) => Some(s.clone()), + VariableType::Nullable(inner) => inner.as_const_str(), _ => None, } } @@ -27,6 +36,7 @@ impl VariableType { let obj = obj.borrow(); obj.get(key).cloned().unwrap_or(VariableType::Any) } + VariableType::Nullable(inner) => inner.get(key), _ => VariableType::Null, } } @@ -34,6 +44,10 @@ impl VariableType { pub fn satisfies(&self, constraint: &Self) -> bool { match (self, constraint) { (VariableType::Any, _) | (_, VariableType::Any) => true, + (VariableType::Nullable(a), VariableType::Nullable(b)) => a.satisfies(b), + (VariableType::Nullable(_), _) => false, + (other, VariableType::Nullable(inner)) => other.satisfies(inner), + (VariableType::Null, VariableType::Null) => true, (VariableType::Bool, VariableType::Bool) => true, (VariableType::String, VariableType::String) => true, @@ -47,8 +61,8 @@ impl VariableType { let o1 = o1.borrow(); let o2 = o2.borrow(); - o1.iter() - .all(|(k, v)| o2.get(k).is_some_and(|tv| v.satisfies(tv))) + o2.iter() + .all(|(k, v)| o1.get(k).is_some_and(|tv| tv.satisfies(v))) } (VariableType::Const(c1), VariableType::Const(c2)) => c1 == c2, @@ -70,6 +84,7 @@ impl VariableType { pub fn is_array(&self) -> bool { match self { VariableType::Any | VariableType::Array(_) => true, + VariableType::Nullable(inner) => inner.is_array(), _ => false, } } @@ -77,6 +92,7 @@ impl VariableType { pub fn is_iterable(&self) -> bool { match self { VariableType::Any | VariableType::Interval | VariableType::Array(_) => true, + VariableType::Nullable(inner) => inner.is_iterable(), _ => false, } } @@ -84,6 +100,7 @@ impl VariableType { pub fn is_string(&self) -> bool { match self { VariableType::String => true, + VariableType::Nullable(inner) => inner.is_string(), _ => false, } } @@ -91,6 +108,7 @@ impl VariableType { pub fn is_object(&self) -> bool { match self { VariableType::Any | VariableType::Object(_) => true, + VariableType::Nullable(inner) => inner.is_object(), _ => false, } } @@ -102,17 +120,32 @@ impl VariableType { } } + pub fn is_nullable(&self) -> bool { + matches!(self, VariableType::Nullable(_) | VariableType::Null) + } + pub fn widen(&self) -> Self { match self { VariableType::Const(_) | VariableType::Enum(_, _) => VariableType::String, + VariableType::Nullable(inner) => { + let widened = inner.widen(); + VariableType::Nullable(Rc::new(widened)) + } _ => self.clone(), } } pub fn merge(&self, other: &Self) -> Self { - match (self, other) { + let (left, left_nullable) = self.unwrap_nullable(); + let (right, right_nullable) = other.unwrap_nullable(); + let nullable = left_nullable || right_nullable; + + let result = match (left, right) { (VariableType::Any, _) | (_, VariableType::Any) => VariableType::Any, (VariableType::Null, VariableType::Null) => VariableType::Null, + (VariableType::Null, other) | (other, VariableType::Null) => { + VariableType::Nullable(Rc::new(other.clone())) + } (VariableType::Bool, VariableType::Bool) => VariableType::Bool, (VariableType::String, VariableType::String) => VariableType::String, (VariableType::Number, VariableType::Number) => VariableType::Number, @@ -122,10 +155,14 @@ impl VariableType { if Rc::ptr_eq(a1, a2) { VariableType::Array(a1.clone()) } else { - VariableType::Array(Rc::new(a1.as_ref().merge(a2.as_ref()))) + match (a1.as_ref(), a2.as_ref()) { + (VariableType::Any, other) | (other, VariableType::Any) => { + VariableType::Array(Rc::new(other.clone())) + } + (l, r) => VariableType::Array(Rc::new(l.merge(r))), + } } } - (VariableType::Object(o1), VariableType::Object(o2)) => { let o1 = o1.borrow(); let o2 = o2.borrow(); @@ -139,8 +176,7 @@ impl VariableType { match merged.entry(k.clone()) { Entry::Occupied(mut entry) => { let current = entry.get(); - let merged_value = current.merge(v); - entry.insert(merged_value); + entry.insert(current.merge(v)); } Entry::Vacant(entry) => { entry.insert(v.clone()); @@ -150,13 +186,11 @@ impl VariableType { VariableType::Object(Rc::new(RefCell::new(merged))) } - (VariableType::Const(c), VariableType::Enum(_, values)) => { let mut merged = values.clone(); if !merged.contains(c) { merged.push(c.clone()); } - VariableType::Enum(None, merged) } (VariableType::Const(c1), VariableType::Const(c2)) => { @@ -168,7 +202,6 @@ impl VariableType { } (VariableType::Const(_), VariableType::String) | (VariableType::String, VariableType::Const(_)) => VariableType::String, - (VariableType::Enum(n1, a), VariableType::Enum(n2, b)) => { let mut merged = a.clone(); for val in b { @@ -184,7 +217,6 @@ impl VariableType { VariableType::Enum(name, merged) } - (VariableType::Enum(_, values), VariableType::Const(c)) => { let mut merged = values.clone(); if !merged.contains(c) { @@ -192,11 +224,20 @@ impl VariableType { } VariableType::Enum(None, merged) } - (VariableType::Enum(_, _), VariableType::String) | (VariableType::String, VariableType::Enum(_, _)) => VariableType::String, - (_, _) => VariableType::Any, + }; + + if nullable + && !matches!( + &result, + VariableType::Any | VariableType::Null | VariableType::Nullable(_) + ) + { + VariableType::Nullable(Rc::new(result)) + } else { + result } } @@ -213,6 +254,7 @@ impl VariableType { VariableType::Object(obj) => VariableType::Object(obj.clone()), VariableType::Const(c) => VariableType::Const(c.clone()), VariableType::Enum(name, options) => VariableType::Enum(name.clone(), options.clone()), + VariableType::Nullable(inner) => VariableType::Nullable(Rc::new(inner.shallow_clone())), } } @@ -276,6 +318,9 @@ impl VariableType { .collect(), ))) } + VariableType::Nullable(inner) => { + VariableType::Nullable(Rc::new(inner.depth_clone(depth))) + } _ => self.shallow_clone(), }, }