diff --git a/.github/scripts/run-wasm-without-imports.mjs b/.github/scripts/run-wasm-without-imports.mjs new file mode 100644 index 0000000..39f0aee --- /dev/null +++ b/.github/scripts/run-wasm-without-imports.mjs @@ -0,0 +1,8 @@ +// Cargo runner for `wasm32-unknown-unknown` tests: instantiates the module +// with no imports (instantiating fails if it needs any), so with no clock, no +// filesystem and no source of randomness. The target cannot print, so +// libtest's output is lost; a failing test panics, which aborts, which traps. +import { readFile } from 'node:fs/promises'; + +const { instance } = await WebAssembly.instantiate(await readFile(process.argv[2]), {}); +process.exitCode = instance.exports.main(0, 0); diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 28fabb1..dbc8b93 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,11 +30,14 @@ jobs: run: cargo test --locked --all-targets - name: Test with no default features run: cargo test --locked --no-default-features --all-targets + # The tests needing model weights are ignored; this covers the rest. + - name: Test with neural synthesis + run: cargo test --locked --features neural-synthesis-hub --all-targets # `--all-targets` skips doctests, and the public API is documented with - # them. + # them; with neural synthesis, so its module's examples compile too. - name: Doctests - run: cargo test --locked --doc + run: cargo test --locked --features neural-synthesis-hub --doc lint: name: Format and Clippy @@ -54,6 +57,8 @@ jobs: run: cargo clippy --locked --all-targets -- -D warnings - name: Clippy (no default features) run: cargo clippy --locked --no-default-features --all-targets -- -D warnings + - name: Clippy (neural synthesis) + run: cargo clippy --locked --features neural-synthesis-hub --all-targets -- -D warnings docs: name: Documentation @@ -66,9 +71,11 @@ jobs: toolchain: nightly - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - # Nightly with `--cfg docsrs`, to match how docs.rs builds the crate. + # Nightly with `--cfg docsrs`, to match how docs.rs builds the crate + # (its features: `package.metadata.docs.rs` in Cargo.toml). Not + # `--all-features`: the GPU backends need their toolkits. - name: Build documentation - run: cargo doc --locked --all-features --no-deps + run: cargo doc --locked --features neural-synthesis-hub --no-deps env: RUSTDOCFLAGS: --cfg docsrs -D warnings @@ -90,9 +97,11 @@ jobs: toolchain: ${{ steps.msrv.outputs.msrv }} - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + # `neural-synthesis-hub` is left out: its Hub client needs a newer Rust + # (documented in the README), and the GPU backends need their toolkits. - name: Check on MSRV run: | - cargo check --locked --all-features + cargo check --locked --features neural-synthesis cargo check --locked --no-default-features # `wasm32-unknown-unknown` has no threads, no clock and no system randomness, @@ -120,18 +129,25 @@ jobs: - name: Build with no default features run: cargo build --locked --lib --no-default-features --target ${{ matrix.target }} - # `wasm32-wasip1` is the only one of the two that can run the test - # harness: `wasm32-unknown-unknown` has no way to start a process or - # print to stdout, which libtest needs. The tests that panic on purpose - # are `ignore`d for `wasm`, which is `panic = abort`. + # Neural synthesis needs no randomness at runtime: `getrandom` (via + # candle's `rand`) only has to compile, and its `unsupported` backend + # fails every request. The runner instantiates the test with no imports + # at all (no clock, filesystem or randomness); the target cannot print, + # but a failing test traps, which fails the step. + - name: Test neural synthesis without imports + if: matrix.target == 'wasm32-unknown-unknown' + env: + RUSTFLAGS: --cfg getrandom_backend="unsupported" + CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: node .github/scripts/run-wasm-without-imports.mjs + run: cargo test --locked --features neural-synthesis --test neural_synthesis --target ${{ matrix.target }} + - uses: bytecodealliance/actions/wasmtime/setup@9152e710e9f7182e4c29ad218e4f335a7b203613 # v1 if: matrix.target == 'wasm32-wasip1' - - name: Test with default features + - name: Test with default features and neural synthesis if: matrix.target == 'wasm32-wasip1' env: - # `--dir` grants the guest the `tests/data` the integration tests read. CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime --dir=. - run: cargo test --locked --tests --target ${{ matrix.target }} + run: cargo test --locked --tests --features neural-synthesis --target ${{ matrix.target }} - name: Test with no default features if: matrix.target == 'wasm32-wasip1' env: diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c3810..c95f084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this crate are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- `FastAutomaton::to_regex_with` and `Term::to_regex_with` convert an automaton with any `fast_automaton::RegexSynthesizer`, such as `fast_automaton::StateElimination` (what `to_regex` uses) or a dependent's own. +- Neural synthesis (`neural-synthesis` feature, off by default): `neural_synthesis::NeuralSynthesizer`, a `RegexSynthesizer` that asks a learned model for simpler regexes. Every proposal is checked against the automaton, and state elimination's regex is kept when it is simpler, so the result is never wrong. Automata too large for the model are split into pieces it can take. Models are loaded from their files, or from the Hugging Face Hub with `neural-synthesis-hub`, and run on the CPU or, with `neural-synthesis-cuda`, `-cudnn` or `-metal`, a GPU (`-mkl` and `-accelerate` speed up the CPU). The first supported model is [Kleene](https://huggingface.co/alexvbrdn/kleene1-9m-b16-t128), for automata of up to 16 states. The `neural_synthesis` example compares both conversions. + +### Changed +- `RegularExpression::evaluate_complexity` now returns the pattern's cognitive complexity, which neural synthesis models minimize: every class costs 1, and quantifiers, alternations and groups cost more the deeper they nest. State elimination uses it to order eliminations, so some converted patterns change; over the test corpus, patterns converted from DFAs are about 9% shorter. + ## [1.0.2] - 2026-09-14 ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38d515f..6482340 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,18 @@ Run what CI runs: cargo fmt --all --check cargo clippy --locked --all-targets -- -D warnings cargo clippy --locked --no-default-features --all-targets -- -D warnings +cargo clippy --locked --features neural-synthesis-hub --all-targets -- -D warnings cargo test --locked --all-targets cargo test --locked --no-default-features --all-targets -cargo test --locked --doc -RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps +cargo test --locked --features neural-synthesis-hub --all-targets +cargo test --locked --features neural-synthesis-hub --doc +RUSTDOCFLAGS="-D warnings" cargo doc --locked --features neural-synthesis-hub --no-deps +``` + +The tests of `neural_synthesis` that run a model are ignored by default, as the weights are not in the repository. With a model release directory (`config.json` and `model.safetensors`, e.g. from [the Hugging Face Hub](https://huggingface.co/alexvbrdn/kleene1-9m-b16-t128)): + +```sh +KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis neural_synthesis -- --ignored --skip bench ``` Please also: @@ -50,6 +58,8 @@ Patterns are parsed by `regex-syntax`, whose Unicode tables are compiled in, and `cargo test --test state_elimination_quality -- --ignored --nocapture` reports how large the patterns produced by automaton-to-regex conversion are over a corpus, which is the number to watch when changing a state-elimination heuristic. +With a model release directory, `KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis --lib -- --ignored --nocapture bench_generate bench_decompose` times the model (`bench_generate`, one DFA at a time and batched, with a checksum of the candidates so an optimization can be checked not to change them) and decomposition on automata beyond the model's limits (`bench_decompose`, with the complexity of the results against state elimination's). + ## Releasing Publishing is automated. Push a `v*` tag matching the version in `Cargo.toml`, and the release workflow checks formatting, clippy, tests, docs, the MSRV and a dry-run publish before it uploads to crates.io. diff --git a/Cargo.lock b/Cargo.lock index 2f05507..2c83b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accelerate-src" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415ed64958754dbe991900f3940677e6a7eefb4d7367afd70d642677b0c7d19d" + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -12,6 +24,7 @@ dependencies = [ "const-random", "getrandom 0.3.4", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -34,6 +47,21 @@ dependencies = [ "cc", ] +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -46,12 +74,97 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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 = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -86,12 +199,87 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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 = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bon" +version = "3.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214f5df094ce551a10a30ffb9c70243d61f121a3d985a6495933e181dee6a7d2" +dependencies = [ + "bon-macros", +] + +[[package]] +name = "bon-macros" +version = "3.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2706da6c749998cc555d04184909608956a69ad3f8ab1a076fbc867b4a3c3ce5" +dependencies = [ + "darling 0.24.1", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "borsh" version = "1.8.1" @@ -116,24 +304,160 @@ dependencies = [ "syn 3.0.5", ] +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "candle-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecb245093b0f791b89d3420c3df9c6d49c60ab63ba54db896bf8a3baf486706" +dependencies = [ + "accelerate-src", + "byteorder", + "candle-kernels", + "candle-metal-kernels", + "candle-ug", + "cudarc 0.19.10", + "float8", + "gemm 0.19.0", + "half", + "intel-mkl-src", + "libc", + "libm", + "memmap2", + "num-traits", + "num_cpus", + "objc2-foundation", + "objc2-metal", + "rand 0.9.5", + "rand_distr", + "rayon", + "safetensors 0.8.0", + "thiserror 2.0.21", + "tokenizers", + "yoke 0.8.3", + "zerocopy", + "zip", +] + +[[package]] +name = "candle-kernels" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67450168a281bbb195a14cc85cf164c7a12023a54a03409a3324f476346e0789" +dependencies = [ + "cudaforge", +] + +[[package]] +name = "candle-metal-kernels" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242e83c6acf639bb273c929d73c67a882bb4dd08a140f121096e19ba2f213d3e" +dependencies = [ + "block2", + "half", + "objc2", + "objc2-foundation", + "objc2-metal", + "once_cell", + "thiserror 2.0.21", + "tracing", +] + +[[package]] +name = "candle-nn" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa10b6ccc365b33210ce404fbf45e60d3e0bdac1004463cf1052e6ee1c1739a" +dependencies = [ + "accelerate-src", + "candle-core", + "candle-metal-kernels", + "half", + "intel-mkl-src", + "libc", + "num-traits", + "objc2-metal", + "rayon", + "safetensors 0.8.0", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "candle-ug" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257411c33abf7d898a31ac20d80813dfe96ff09e23a73039e22ab293aea9b871" +dependencies = [ + "ug", + "ug-cuda", + "ug-metal", +] + [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.4.5" @@ -141,6 +465,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -156,6 +482,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[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 = "ciborium" version = "0.2.2" @@ -209,763 +559,4258 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "const-random" -version = "0.1.18" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "const-random-macro", + "cc", ] [[package]] -name = "const-random-macro" -version = "0.1.16" +name = "colored" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", + "windows-sys 0.61.2", ] [[package]] -name = "criterion" -version = "0.8.2" +name = "combine" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", + "bytes", + "memchr", ] [[package]] -name = "criterion-plot" -version = "0.8.2" +name = "compact_str" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ - "cast", - "itertools", + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", ] [[package]] -name = "crossbeam-deque" -version = "0.8.7" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] -name = "crossbeam-epoch" -version = "0.9.20" +name = "const-random" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" dependencies = [ - "crossbeam-utils", + "const-random-macro", ] [[package]] -name = "crossbeam-utils" -version = "0.8.22" +name = "const-random-macro" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] -name = "crunchy" -version = "0.2.4" +name = "const-str" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] -name = "either" -version = "1.18.0" +name = "const_panic" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] [[package]] -name = "equivalent" -version = "1.0.2" +name = "constant_time_eq" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] -name = "find-msvc-tools" -version = "0.1.12" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "futures-core" -version = "0.3.34" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "futures-task" -version = "0.3.34" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "futures-util" -version = "0.3.34" +name = "core-graphics-types" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "core_detect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "countio" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9702aee5d1d744c01d82f6915644f950f898e014903385464c773b96fefdecb" dependencies = [ - "cfg-if", - "libc", - "wasi", + "futures-io", ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "cfg-if", "libc", - "r-efi", - "wasip2", ] [[package]] -name = "half" -version = "2.7.1" +name = "cpufeatures" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", + "libc", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "crc32fast" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] [[package]] -name = "indexmap" -version = "2.14.2" +name = "criterion" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ - "equivalent", - "hashbrown", + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", ] [[package]] -name = "irange" -version = "1.2.0" +name = "criterion-plot" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65ac99e6db0efa2b0c8d6216142342b7eaca2235981fccb0b351ba0cc2243e5" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] [[package]] -name = "itertools" -version = "0.13.0" +name = "crossbeam-channel" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ - "either", + "crossbeam-utils", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "crossbeam-deque" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] [[package]] -name = "js-sys" -version = "0.3.105" +name = "crossbeam-epoch" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "crossbeam-utils", ] [[package]] -name = "libc" -version = "0.2.189" +name = "crossbeam-utils" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] -name = "memchr" -version = "2.8.3" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "num-traits" -version = "0.2.19" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "autocfg", + "generic-array", + "typenum", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] [[package]] -name = "oorandom" -version = "11.1.5" +name = "ctor" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] [[package]] -name = "page_size" -version = "0.6.0" +name = "cudaforge" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +checksum = "8d475ff95b9e4096878f47fe0ca5dbad0e23849a79fc225305ea06c2f25771cd" dependencies = [ - "libc", - "winapi", + "anyhow", + "fs2", + "glob", + "num_cpus", + "rayon", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.21", + "walkdir", + "which", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "cudarc" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "0bf99ab37ee7072d64d906aa2dada9a3422f1d975cdf8c8055a573bc84897ed8" +dependencies = [ + "half", + "libloading 0.8.9", +] [[package]] -name = "plotters" -version = "0.3.7" +name = "cudarc" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +checksum = "7359bf1de037ddada72729c3acec9b040f3468400c6e9e740b01ba0cd2e89562" dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", + "float8", + "half", + "libloading 0.9.0", ] [[package]] -name = "plotters-backend" -version = "0.3.7" +name = "darling" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] [[package]] -name = "plotters-svg" -version = "0.3.7" +name = "darling" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "plotters-backend", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "darling_core" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ - "zerocopy", + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", ] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "darling_core" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ - "toml_edit", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.5", ] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "darling_macro" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "unicode-ident", + "darling_core 0.20.11", + "quote", + "syn 2.0.119", ] [[package]] -name = "proptest" -version = "1.11.0" +name = "darling_macro" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "unarray", + "darling_core 0.24.1", + "quote", + "syn 3.0.5", ] [[package]] -name = "quote" -version = "1.0.47" +name = "dary_heap" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" dependencies = [ - "proc-macro2", + "serde", ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] -name = "rand" -version = "0.9.5" +name = "derive_builder" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ - "rand_chacha", - "rand_core", + "derive_builder_macro", ] [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "derive_builder_core" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "ppv-lite86", - "rand_core", + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "derive_builder_macro" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ - "getrandom 0.3.4", + "derive_builder_core", + "syn 2.0.119", ] [[package]] -name = "rand_xorshift" -version = "0.4.0" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "rand_core", + "block-buffer 0.10.4", + "crypto-common 0.1.7", ] [[package]] -name = "rayon" -version = "1.12.0" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "either", - "rayon-core", + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] -name = "rayon-core" -version = "1.13.0" +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "dirs-sys 0.4.1", ] [[package]] -name = "regex" -version = "1.13.1" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "dirs-sys 0.5.0", ] [[package]] -name = "regex-automata" -version = "0.4.18" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] -name = "regex-charclass" -version = "1.2.0" +name = "dirs-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115f966197e7327844076cfeab1959ddd43c16fd3d98cab101efd940ec595280" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ - "irange", + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", ] [[package]] -name = "regex-syntax" -version = "0.8.11" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] [[package]] -name = "regexsolver" -version = "1.0.2" +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ - "ahash", - "bit-set 0.11.1", - "criterion", - "indexmap", - "proptest", - "rayon", - "regex", - "regex-charclass", - "regex-syntax", - "tracing", + "proc-macro2", + "quote", + "syn 3.0.5", ] [[package]] -name = "rustversion" -version = "1.0.23" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "same-file" -version = "1.0.6" +name = "dyn-stack" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" dependencies = [ - "winapi-util", + "bytemuck", + "dyn-stack-macros", ] [[package]] -name = "serde" -version = "1.0.229" +name = "dyn-stack-macros" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] +checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" [[package]] -name = "serde_core" -version = "1.0.229" +name = "either" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] -name = "serde_derive" -version = "1.0.229" +name = "encoding_rs" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] -name = "serde_json" -version = "1.0.151" +name = "enum-as-inner" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "shlex" -version = "2.0.1" +name = "env_home" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" [[package]] -name = "slab" -version = "0.4.12" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "syn" -version = "2.0.119" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "syn" -version = "3.0.5" +name = "esaxx-rs" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "filetime" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "serde", - "serde_json", + "cfg-if", + "libc", ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "find-msvc-tools" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" +name = "flate2" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "crc32fast", + "miniz_oxide", + "zlib-rs", ] [[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" +name = "float8" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc" dependencies = [ - "winnow", + "half", + "num-traits", + "rand 0.9.5", + "rand_distr", ] [[package]] -name = "tracing" -version = "0.1.44" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "tracing-attributes" -version = "0.1.31" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] -name = "tracing-core" -version = "0.1.36" +name = "foreign-types" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ - "once_cell", + "foreign-types-macros", + "foreign-types-shared", ] [[package]] -name = "unarray" -version = "0.1.4" +name = "foreign-types-macros" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] -name = "version_check" -version = "0.9.5" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] [[package]] -name = "walkdir" -version = "2.5.0" +name = "fs2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ - "same-file", - "winapi-util", + "libc", + "winapi", ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" +name = "futures" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ - "wit-bindgen", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "wasm-bindgen" -version = "0.2.128" +name = "futures-channel" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "futures-core", + "futures-sink", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.128" +name = "futures-core" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.128" +name = "futures-io" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ - "bumpalo", "proc-macro2", "quote", "syn 3.0.5", - "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.128" +name = "futures-sink" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" -dependencies = [ - "unicode-ident", -] +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] -name = "web-sys" -version = "0.3.105" +name = "futures-task" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] -name = "winapi" -version = "0.3.9" +name = "futures-util" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gearhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616e8f476a586f1b078d9eece21f7a071f27997e709e2b471f5697deea53bda9" + +[[package]] +name = "gemm" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" +dependencies = [ + "dyn-stack", + "gemm-c32 0.18.2", + "gemm-c64 0.18.2", + "gemm-common 0.18.2", + "gemm-f16 0.18.2", + "gemm-f32 0.18.2", + "gemm-f64 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" +dependencies = [ + "dyn-stack", + "gemm-c32 0.19.0", + "gemm-c64 0.19.0", + "gemm-common 0.19.0", + "gemm-f16 0.19.0", + "gemm-f32 0.19.0", + "gemm-f64 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-common" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.21.5", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-common" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.22.3", + "raw-cpuid", + "rayon", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-f16" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "gemm-f32 0.18.2", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f16" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "gemm-f32 0.19.0", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" +dependencies = [ + "dyn-stack", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" +dependencies = [ + "dyn-stack", + "gemm-common 0.19.0", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[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 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "bytemuck", + "cfg-if", + "crunchy", + "num-traits", + "rand 0.9.5", + "rand_distr", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapify" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hf-hub" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ccb6bcc85dec15413ef5949879f9a5497ca4568ed702547eb83fac23376e5c" +dependencies = [ + "base64 0.22.1", + "bon", + "bytes", + "futures", + "getrandom 0.2.17", + "globset", + "hf-xet", + "hyper", + "pathdiff", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.21", + "tokio", + "tokio-retry", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "hf-xet" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24754aef4f6c3d57bc4a0dc2648ca74487050eda01c361b3eefa11feeafd662" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "http", + "more-asserts", + "serde", + "serde_json", + "thiserror 2.0.21", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "uuid", + "xet-client", + "xet-core-structures", + "xet-data", + "xet-runtime", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke 0.8.3", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke 0.8.3", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "intel-mkl-src" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee70586cd5b3e772a8739a1bd43eaa90d4f4bf0fb2a4edc202e979937ee7f5e" +dependencies = [ + "anyhow", + "intel-mkl-tool", + "ocipkg", +] + +[[package]] +name = "intel-mkl-tool" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887a16b4537d82227af54d3372971cfa5e0cde53322e60f57584056c16ada1b4" +dependencies = [ + "anyhow", + "log", + "walkdir", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "irange" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65ac99e6db0efa2b0c8d6216142342b7eaca2235981fccb0b351ba0cc2243e5" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror 2.0.21", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "konst" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" +dependencies = [ + "const_panic", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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 = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "macro_rules_attribute" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" +dependencies = [ + "macro_rules_attribute-proc_macro", + "pastey", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", + "stable_deref_trait", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[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 = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "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 = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "oci-spec" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf88ddc01cc6bccbe1044adb6a29057333f523deadcb4953c011a73158cfa5e" +dependencies = [ + "derive_builder", + "getset", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 1.0.69", +] + +[[package]] +name = "ocipkg" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb3293021f06540803301af45e7ab81693d50e89a7398a3420bdab139e7ba5e" +dependencies = [ + "base16ct", + "base64 0.22.1", + "chrono", + "directories", + "flate2", + "lazy_static", + "log", + "oci-spec", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "thiserror 1.0.69", + "toml", + "ureq", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oneshot" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags 2.13.1", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +dependencies = [ + "memchr", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[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 = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.5", +] + +[[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.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "pulp" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +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 = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.5", +] + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.1", +] + +[[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-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools 0.14.0", + "rayon", +] + +[[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 = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redb" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba239c1c1693315d3cc0e601db3b3965543afbf48c41730fdca2f069f510f4a" +dependencies = [ + "libc", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-charclass" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115f966197e7327844076cfeab1959ddd43c16fd3d98cab101efd940ec595280" +dependencies = [ + "irange", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "regexsolver" +version = "1.0.2" +dependencies = [ + "ahash", + "bit-set 0.11.1", + "candle-core", + "candle-nn", + "criterion", + "hf-hub", + "indexmap", + "proptest", + "rayon", + "regex", + "regex-charclass", + "regex-syntax", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reqwest-middleware" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "thiserror 2.0.21", + "tower-service", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe-transmute" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" + +[[package]] +name = "safetensors" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b079b829cb27a1c3c374341345ed2e8b2c0c839034522cee576c140bd7f846" +dependencies = [ + "hashbrown 0.16.1", + "libc", + "serde", + "serde_json", + "tempfile", +] + +[[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 = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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 = "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.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "bstr", + "dirs", + "os_str_bytes", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +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.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "sysinfo" +version = "0.39.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[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.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +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.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "itertools 0.14.0", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand 0.9.5", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror 2.0.21", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-retry" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a129d95275ebf4c493ec53bf0f8cd95f5ac161bc4f381700809a54f595d4470" +dependencies = [ + "pin-project-lite", + "rand 0.10.3", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio_with_wasm" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[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.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[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 2.13.1", + "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-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.21", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + +[[package]] +name = "ug" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0" +dependencies = [ + "gemm 0.18.2", + "half", + "libloading 0.8.9", + "memmap2", + "num", + "num-traits", + "num_cpus", + "rayon", + "safetensors 0.4.5", + "serde", + "thiserror 1.0.69", + "tracing", + "yoke 0.7.5", +] + +[[package]] +name = "ug-cuda" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0a1fa748f26166778c33b8498255ebb7c6bffb472bcc0a72839e07ebb1d9b5" +dependencies = [ + "cudarc 0.17.8", + "half", + "serde", + "thiserror 1.0.69", + "ug", +] + +[[package]] +name = "ug-metal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7adf545a99a086d362efc739e7cf4317c18cbeda22706000fd434d70ea3d95" +dependencies = [ + "half", + "metal", + "objc", + "serde", + "thiserror 1.0.69", + "ug", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[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 = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +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 = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +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-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-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 = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] [[package]] -name = "winapi-util" -version = "0.1.11" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-sys", + "windows-core", + "windows-link", + "windows-threading", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] [[package]] name = "windows-link" @@ -973,6 +4818,63 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.61.2" @@ -982,6 +4884,145 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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", + "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-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[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.4" @@ -991,12 +5032,224 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xet-client" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15500e5fc9e17be15224f9e34cb07d41fb2805d9dbbf7bd3507c982fd1eb3efd" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "crc32fast", + "futures", + "http", + "hyper", + "more-asserts", + "rand 0.10.3", + "redb", + "reqwest", + "reqwest-middleware", + "serde", + "serde_json", + "serde_repr", + "statrs", + "tempfile", + "thiserror 2.0.21", + "tokio", + "tokio-retry", + "tokio_with_wasm", + "tracing", + "url", + "urlencoding", + "uuid", + "web-time", + "xet-core-structures", + "xet-runtime", +] + +[[package]] +name = "xet-core-structures" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5080d5803c8bf15fa56ef934050a46b225801bad3ea4cb8bb3c46c5c37976516" +dependencies = [ + "async-trait", + "base64 0.22.1", + "blake3", + "bytemuck", + "bytes", + "countio", + "futures", + "futures-util", + "getrandom 0.4.3", + "heapify", + "itertools 0.14.0", + "lz4_flex", + "more-asserts", + "rand 0.10.3", + "regex", + "safe-transmute", + "serde", + "static_assertions", + "thiserror 2.0.21", + "tokio", + "tokio-util", + "tracing", + "uuid", + "web-time", + "xet-runtime", +] + +[[package]] +name = "xet-data" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1287a1657fb67de846030f6605a9960dcdad401c467b490657e6e909d2e0d1c0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "gearhash", + "http", + "itertools 0.14.0", + "more-asserts", + "rand 0.10.3", + "serde", + "serde_json", + "sha2 0.11.0", + "tempfile", + "thiserror 2.0.21", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "url", + "uuid", + "web-time", + "xet-client", + "xet-core-structures", + "xet-runtime", +] + +[[package]] +name = "xet-runtime" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921953c885ce83e02b21e8d26444b75c78c9e83076699ced08d5c6fc1cdb48ac" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "colored", + "const-str", + "ctor", + "dirs", + "futures", + "git-version", + "humantime", + "konst", + "libc", + "more-asserts", + "oneshot", + "pin-project", + "rand 0.10.3", + "reqwest", + "serde", + "serde_json", + "shellexpand", + "sysinfo", + "thiserror 2.0.21", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "tracing-appender", + "tracing-subscriber", + "web-time", + "whoami", + "winapi", +] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[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 0.8.4", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure 0.14.0", +] + [[package]] name = "zerocopy" version = "0.8.56" @@ -1017,6 +5270,84 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke 0.8.3", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke 0.8.3", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "indexmap", + "memchr", + "typed-path", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 66afe83..d6f66ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ include = [ "/examples/**/*.rs", "/tests/**/*.rs", "/tests/data/*.txt", + "/tests/data/*.json", "/README.md", "/CHANGELOG.md", "/LICENSE", @@ -32,12 +33,20 @@ regex-syntax = "0.8.11" regex-charclass = { version = "1.2.0", features = ["ucd-16"] } bit-set = "0.11.1" indexmap = "2.13.0" +# `neural-synthesis`: learned DFA -> regex models, run with candle. +candle-core = { version = "0.11.0", optional = true } +candle-nn = { version = "0.11.0", optional = true } +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } # `wasm` has no threads: `rayon` would build, but only as its single-thread # fallback. The `parallel` feature stays enabled there and compiles to the same # sequential code as `--no-default-features`, minus the dependency. [target.'cfg(not(target_family = "wasm"))'.dependencies] rayon = { version = "1.10.0", optional = true } +# `neural-synthesis-hub`: downloads models from the Hugging Face Hub. Its +# blocking client runs a tokio runtime on a thread, which `wasm` does not have. +hf-hub = { version = "1.0.0", features = ["blocking"], optional = true } # `ahash` draws its hash keys from `getrandom`, which `wasm32-unknown-unknown` has # no backend for; there it falls back to `compile-time-rng`, drawing them at build @@ -52,6 +61,18 @@ ahash = { version = "0.8.11", default-features = false, features = ["std", "comp [features] default = ["parallel"] parallel = ["dep:rayon"] +# Converts small automata to short regexes with a learned model, each candidate +# verified against the automaton before use (`neural_synthesis` module). Models +# are loaded from their files (`config.json`, `model.safetensors`). +neural-synthesis = ["dep:candle-core", "dep:candle-nn", "dep:serde", "dep:serde_json"] +# Also downloads models from the Hugging Face Hub (not on `wasm`). +neural-synthesis-hub = ["neural-synthesis", "dep:hf-hub"] +# Hardware backends for the models (candle's). +neural-synthesis-cuda = ["neural-synthesis", "candle-core/cuda", "candle-nn/cuda"] +neural-synthesis-cudnn = ["neural-synthesis-cuda", "candle-core/cudnn", "candle-nn/cudnn"] +neural-synthesis-metal = ["neural-synthesis", "candle-core/metal", "candle-nn/metal"] +neural-synthesis-mkl = ["neural-synthesis", "candle-core/mkl", "candle-nn/mkl"] +neural-synthesis-accelerate = ["neural-synthesis", "candle-core/accelerate", "candle-nn/accelerate"] [dev-dependencies] proptest = { version = "1", default-features = false, features = ["std", "bit-set"] } @@ -60,8 +81,10 @@ regex = "1.13.1" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } +# Not `all-features`: the hardware backends need their toolkits (CUDA, Metal, +# MKL, Accelerate) to build, and add no API. [package.metadata.docs.rs] -all-features = true +features = ["neural-synthesis-hub"] rustdoc-args = ["--cfg", "docsrs"] [profile.bench] @@ -70,3 +93,7 @@ codegen-units = 1 [[bench]] name = "operations" harness = false + +[[example]] +name = "neural_synthesis" +required-features = ["neural-synthesis-hub"] diff --git a/README.md b/README.md index 2403a07..9d3a1c9 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,32 @@ execution_profile.run(|| { The same profile caps how many states an automaton may hold (`max_number_of_states`, failing with `EngineError::AutomatonHasTooManyStates`), and can refuse implicit determinization outright (`implicit_determinization(false)`), so the potentially exponential subset construction only ever runs through an explicit `determinize()` call. Both are documented with examples on [`ExecutionProfile`](https://docs.rs/regexsolver/latest/regexsolver/execution_profile/struct.ExecutionProfile.html). +## Simpler regexes with neural synthesis (optional) + +`to_regex` converts an automaton back to a pattern by state elimination, which is always correct but often hard to read. With the `neural-synthesis` feature, `to_regex_with` can ask a learned model instead: + +```rust,ignore +use regexsolver::neural_synthesis::{Device, NeuralSynthesizer}; + +// From the Hugging Face Hub (`neural-synthesis-hub` feature), or from its files with `from_files`. +let synthesizer = NeuralSynthesizer::from_hub(NeuralSynthesizer::DEFAULT_HUB_MODEL, Device::best_available())?; + +let regex = automaton.to_regex_with(&synthesizer)?; +// to_regex: [a-eg]*h(b(hb)*(h{2}|([a-eg]|h[ac-eg])[a-eg]*h)|([ac-eg][a-eg]*)?h)*b(hb)*h? +// to_regex_with: [a-egh]*hbh? +``` + +Every pattern the model proposes is checked against the automaton, so the result is never wrong, and state elimination's pattern is kept when it is simpler. The model, [Kleene](https://huggingface.co/alexvbrdn/kleene1-9m-b16-t128), takes automata of up to 16 states; larger ones are split into pieces it can take. Conversions are slower than state elimination: on a CPU without MKL or Accelerate, expect a few hundred milliseconds for automata of up to 16 states (occasionally more than a second), and from about a second to tens of seconds for larger ones. Actual times vary with the CPU. + +| Feature | Adds | +|---|---| +| `neural-synthesis` | `NeuralSynthesizer`, loading models from their files | +| `neural-synthesis-hub` | downloads from the Hugging Face Hub; needs Rust 1.95, not available on `wasm` | +| `neural-synthesis-cuda`, `-cudnn`, `-metal` | GPU devices; need the CUDA toolkit or Metal | +| `neural-synthesis-mkl`, `-accelerate` | faster CPU inference with Intel MKL (needs oneAPI MKL's static libraries) or Apple Accelerate | + +On `wasm32-unknown-unknown`, build with `RUSTFLAGS='--cfg getrandom_backend="unsupported"'`: `getrandom`, which candle depends on, only compiles there with a backend chosen, and neural synthesis never asks it for randomness. + ## Implementation - Patterns are parsed with [regex-syntax](https://docs.rs/regex-syntax/latest/regex_syntax/) and simplified into a small regular-expression AST; set operations run on finite automata; results convert back to patterns via state elimination. diff --git a/examples/neural_synthesis.rs b/examples/neural_synthesis.rs new file mode 100644 index 0000000..9ca1ac2 --- /dev/null +++ b/examples/neural_synthesis.rs @@ -0,0 +1,51 @@ +//! Converts automata to regexes with state elimination and with a neural +//! synthesis model, side by side. +//! +//! ```text +//! cargo run --release --features neural-synthesis-hub --example neural_synthesis -- [model] +//! ``` +//! +//! `model` is a directory holding `config.json` and `model.safetensors`, or a +//! Hugging Face Hub repository id (default: +//! `NeuralSynthesizer::DEFAULT_HUB_MODEL`). The model runs on the best +//! available device. + +use std::{path::Path, time::Instant}; + +use regexsolver::{ + Term, + neural_synthesis::{Device, NeuralSynthesizer}, +}; + +fn main() -> Result<(), Box> { + let model = std::env::args() + .nth(1) + .unwrap_or_else(|| NeuralSynthesizer::DEFAULT_HUB_MODEL.to_string()); + let device = Device::best_available(); + let synthesizer = if Path::new(&model).is_dir() { + NeuralSynthesizer::from_dir(&model, device)? + } else { + NeuralSynthesizer::from_hub(&model, device)? + }; + println!("{} on {device:?}", synthesizer.model_name()); + + for (a, b) in [ + (".*h(b|bh)", ".*[fi-z].*"), + (".*abc.*", ".*def.*"), + ("x*", "(xxx)*"), + ("(ab|cd)*", "(abcd)*"), + ("[a-z]{2,6}", ".*q.*"), + ] { + let difference = a.parse::()?.difference(&b.parse::()?)?; + let start = Instant::now(); + let eliminated = difference.to_regex()?; + let elapsed_eliminated = start.elapsed(); + let start = Instant::now(); + let synthesized = difference.to_regex_with(&synthesizer)?; + let elapsed_synthesized = start.elapsed(); + println!("{a} - {b}"); + println!(" state elimination ({elapsed_eliminated:.1?}): {eliminated}"); + println!(" neural synthesis ({elapsed_synthesized:.1?}): {synthesized}"); + } + Ok(()) +} diff --git a/src/fast_automaton/convert/mod.rs b/src/fast_automaton/convert/mod.rs index 49cfcb0..0ac8301 100644 --- a/src/fast_automaton/convert/mod.rs +++ b/src/fast_automaton/convert/mod.rs @@ -1,3 +1,3 @@ use super::*; -mod to_regex; +pub(crate) mod to_regex; diff --git a/src/fast_automaton/convert/to_regex/mod.rs b/src/fast_automaton/convert/to_regex/mod.rs index a44e8d1..70f2233 100644 --- a/src/fast_automaton/convert/to_regex/mod.rs +++ b/src/fast_automaton/convert/to_regex/mod.rs @@ -1,13 +1,59 @@ use super::*; +#[cfg(feature = "neural-synthesis")] +pub mod neural_synthesis; mod state_elimination; +/// A way to convert an automaton to a [`RegularExpression`], for +/// [`FastAutomaton::to_regex_with`] and +/// [`Term::to_regex_with`](crate::Term::to_regex_with). +/// +/// The crate provides [`StateElimination`], what +/// [`FastAutomaton::to_regex`] uses, and, with the `neural-synthesis` feature, +/// `neural_synthesis::NeuralSynthesizer`, which asks a learned model for a +/// simpler regex. +pub trait RegexSynthesizer { + /// A regular expression describing exactly the language of `automaton`. + fn synthesize(&self, automaton: &FastAutomaton) -> Result; +} + +/// Conversion by state elimination, what [`FastAutomaton::to_regex`] uses: +/// the automaton's states are removed one at a time, each removal folding the +/// paths through the state into regexes on the transitions around it, until +/// one transition from the start to acceptance remains. The result is always +/// exact, but often long and hard to read. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct StateElimination; + +impl RegexSynthesizer for StateElimination { + fn synthesize(&self, automaton: &FastAutomaton) -> Result { + automaton.to_regex() + } +} + impl FastAutomaton { - /// Converts the automaton to a [`RegularExpression`]. + /// Converts the automaton to a [`RegularExpression`] by state + /// elimination (see [`StateElimination`]). + /// + /// The result describes exactly the automaton's language, but can be long + /// and hard to read; [`to_regex_with`](Self::to_regex_with) takes other + /// ways to convert it. #[tracing::instrument(level = "debug", skip_all, fields(states = self.number_of_states()))] pub fn to_regex(&self) -> Result { state_elimination::convert_to_regex(self) } + + /// Converts the automaton to a [`RegularExpression`] with `synthesizer`, + /// for instance [`StateElimination`] (as [`to_regex`](Self::to_regex) + /// does) or, with the `neural-synthesis` feature, a + /// `neural_synthesis::NeuralSynthesizer`. + #[tracing::instrument(level = "debug", skip_all, fields(states = self.number_of_states()))] + pub fn to_regex_with( + &self, + synthesizer: &S, + ) -> Result { + synthesizer.synthesize(self) + } } #[cfg(test)] diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/analysis.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/analysis.rs new file mode 100644 index 0000000..2a55585 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/analysis.rs @@ -0,0 +1,222 @@ +//! Where to split a trimmed minimal DFA. + +use std::collections::VecDeque; + +use super::*; +use crate::IntMap; + +/// Most entry states of a frontier split's region. +const MAX_ENTRIES: usize = 4; +/// Most hubs of a hub split. +const MAX_HUBS: usize = 3; +/// Hubs are added until no strongly connected component is larger. +const HUB_TARGET: usize = 8; + +/// The state other than the start, closest to it, that every accepting path +/// visits; `dfa`'s start must not accept. +pub(super) fn closest_dominator(dfa: &FastAutomaton) -> Option { + let start_state = dfa.start_state(); + let distances = distances_from(dfa, start_state, None); + let mut candidates: Vec = dfa.states().filter(|s| *s != start_state).collect(); + candidates.sort_by_key(|state| distances.get(state).copied().unwrap_or(usize::MAX)); + candidates.into_iter().find(|&state| { + distances_from(dfa, start_state, Some(state)) + .keys() + .all(|reached| !dfa.is_accepted(*reached)) + }) +} + +/// The weakly connected components of `dfa` without `removed`. +pub(super) fn components_without(dfa: &FastAutomaton, removed: State) -> Vec> { + let mut neighbors: IntMap> = IntMap::default(); + for from_state in dfa.states().filter(|s| *s != removed) { + for to_state in dfa.direct_states(from_state).filter(|s| *s != removed) { + neighbors.entry(from_state).or_default().push(to_state); + neighbors.entry(to_state).or_default().push(from_state); + } + } + let mut seen = IntSet::default(); + let mut components = vec![]; + for root in dfa.states().filter(|s| *s != removed) { + if !seen.insert(root) { + continue; + } + let mut component = IntSet::default(); + component.insert(root); + let mut stack = vec![root]; + while let Some(state) = stack.pop() { + for &neighbor in neighbors.get(&state).into_iter().flatten() { + if seen.insert(neighbor) { + component.insert(neighbor); + stack.push(neighbor); + } + } + } + components.push(component); + } + components +} + +/// Where a frontier split cuts: a region closed under successors, without +/// the start, and the states it is entered at. +pub(super) struct Cut { + pub(super) region: IntSet, + pub(super) entries: Vec, +} + +/// The frontier split whose largest piece is the smallest, among the regions +/// of the states reachable from one state. +pub(super) fn best_cut(dfa: &FastAutomaton) -> Option { + let start_state = dfa.start_state(); + let number_of_states = dfa.number_of_states(); + let mut seen: Vec> = vec![]; + let mut best: Option<((usize, usize), Cut)> = None; + for root in dfa.states() { + let region: IntSet = distances_from(dfa, root, None).into_keys().collect(); + if region.contains(&start_state) || seen.contains(®ion) { + continue; + } + seen.push(region.clone()); + let mut entries: Vec = dfa + .states() + .filter(|state| !region.contains(state)) + .flat_map(|from_state| dfa.direct_states(from_state)) + .filter(|to_state| region.contains(to_state)) + .collect(); + entries.sort_unstable(); + entries.dedup(); + let largest_piece = (number_of_states - region.len() + 1).max(region.len()); + let score = (largest_piece, entries.len()); + if entries.len() <= MAX_ENTRIES + && largest_piece < number_of_states + && best + .as_ref() + .is_none_or(|(best_score, _)| score < *best_score) + { + best = Some((score, Cut { region, entries })); + } + } + best.map(|(_, cut)| cut) +} + +/// `Σ |C|²` over the strongly connected components `C` with a cycle: what +/// every piece of a hub split must lower. +pub(super) fn cyclic_weight(dfa: &FastAutomaton) -> usize { + strongly_connected_components(dfa, &IntSet::default()) + .iter() + .filter(|component| component.len() > 1 || dfa.has_transition(component[0], component[0])) + .map(|component| component.len() * component.len()) + .sum() +} + +/// A few states whose removal shatters the largest strongly connected +/// component, chosen greedily; none if it is small or does not shatter. +pub(super) fn choose_hubs(dfa: &FastAutomaton) -> Option> { + let largest = |removed: &IntSet| { + strongly_connected_components(dfa, removed) + .into_iter() + .max_by_key(Vec::len) + .unwrap_or_default() + }; + let mut removed = IntSet::default(); + let mut component = largest(&removed); + let initial = component.len(); + if initial < 3 { + return None; + } + let mut hubs = vec![]; + while hubs.is_empty() || component.len() > HUB_TARGET && hubs.len() < MAX_HUBS { + // The smallest remaining component, then the busiest state. + let hub = component.iter().copied().min_by_key(|&state| { + let mut without = removed.clone(); + without.insert(state); + let degree = dfa.in_degree(state) * dfa.out_degree(state); + (largest(&without).len(), usize::MAX - degree, state) + })?; + removed.insert(hub); + hubs.push(hub); + component = largest(&removed); + } + // Worth it only if the component shrinks beyond the hubs removed. + (component.len() + hubs.len() < initial).then_some(hubs) +} + +/// BFS distances from `from_state`, never entering `avoided`. +fn distances_from( + dfa: &FastAutomaton, + from_state: State, + avoided: Option, +) -> IntMap { + let mut distances = IntMap::default(); + distances.insert(from_state, 0); + let mut queue = VecDeque::from([from_state]); + while let Some(state) = queue.pop_front() { + let distance = distances[&state] + 1; + for to_state in dfa.direct_states(state) { + if Some(to_state) != avoided && !distances.contains_key(&to_state) { + distances.insert(to_state, distance); + queue.push_back(to_state); + } + } + } + distances +} + +/// The strongly connected components of `dfa` without `removed` +/// (Kosaraju's algorithm). +fn strongly_connected_components(dfa: &FastAutomaton, removed: &IntSet) -> Vec> { + let states: Vec = dfa.states().filter(|s| !removed.contains(s)).collect(); + let successors = |state: State| -> Vec { + dfa.direct_states(state) + .filter(|s| !removed.contains(s)) + .collect() + }; + + // States in post-order of a DFS of the transitions. + let mut finished = Vec::with_capacity(states.len()); + let mut seen = IntSet::default(); + for &root in &states { + if !seen.insert(root) { + continue; + } + let mut stack = vec![(root, successors(root), 0)]; + while let Some((state, next_states, next)) = stack.last_mut() { + if let Some(&to_state) = next_states.get(*next) { + *next += 1; + if seen.insert(to_state) { + stack.push((to_state, successors(to_state), 0)); + } + } else { + finished.push(*state); + stack.pop(); + } + } + } + + // Then the reversed transitions, in reverse post-order. + let mut predecessors: IntMap> = IntMap::default(); + for &from_state in &states { + for to_state in successors(from_state) { + predecessors.entry(to_state).or_default().push(from_state); + } + } + let mut assigned = IntSet::default(); + let mut components = vec![]; + for &root in finished.iter().rev() { + if !assigned.insert(root) { + continue; + } + let mut component = vec![root]; + let mut stack = vec![root]; + while let Some(state) = stack.pop() { + for &from_state in predecessors.get(&state).into_iter().flatten() { + if assigned.insert(from_state) { + component.push(from_state); + stack.push(from_state); + } + } + } + components.push(component); + } + components +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/bench.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/bench.rs new file mode 100644 index 0000000..a503506 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/bench.rs @@ -0,0 +1,139 @@ +//! Compares [`FastAutomaton::to_regex_with`] with and without decomposition, +//! and with state elimination, on DFAs beyond the model's limits: +//! `KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis decompose::bench -- --ignored --nocapture` + +use std::time::{Duration, Instant}; + +use super::*; +use crate::{execution_profile::ExecutionProfileBuilder, neural_synthesis::Device}; + +/// DFAs per set operation. +const PER_OPERATION: usize = 10; + +#[test] +#[ignore = "benchmark; needs the model weights (KLEENE_MODEL_DIR)"] +fn bench_decompose() { + let load = || { + NeuralSynthesizer::from_dir(std::env::var("KLEENE_MODEL_DIR").unwrap(), Device::Cpu) + .unwrap() + }; + let whole = load().with_decomposition(false); + let decomposing = load(); + + let reference: Vec = serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/kleene_parity.json" + ))) + .unwrap(); + let atoms: Vec = reference + .iter() + .map(|case| RegularExpression::new(case["pattern"].as_str().unwrap()).unwrap()) + .collect(); + + for (operation, dfas) in large_dfas(&atoms, whole.max_states()) { + let mut ratios = (0.0, 0.0); + let mut simpler = 0; + let mut times = (Duration::ZERO, Duration::ZERO); + for dfa in &dfas { + let eliminated = dfa.to_regex().unwrap().evaluate_complexity(); + let start = Instant::now(); + let without = dfa.to_regex_with(&whole).unwrap(); + times.0 += start.elapsed(); + let start = Instant::now(); + let with = dfa.to_regex_with(&decomposing).unwrap(); + times.1 += start.elapsed(); + assert!( + with.to_automaton().unwrap().equivalent(dfa).unwrap(), + "{with}" + ); + + ratios.0 += (without.evaluate_complexity() / eliminated).ln(); + ratios.1 += (with.evaluate_complexity() / eliminated).ln(); + simpler += usize::from(with.evaluate_complexity() < eliminated); + } + let count = dfas.len(); + println!( + "{operation:<13} {count} DFAs: complexity vs state elimination (geometric mean) {:.3} without decomposition, {:.3} with; simpler for {simpler}; {:?} and {:?} per DFA", + (ratios.0 / count as f64).exp(), + (ratios.1 / count as f64).exp(), + times.0 / count as u32, + times.1 / count as u32, + ); + } +} + +/// A set operation on three regexes. +type Operation = fn(&[&RegularExpression; 3]) -> Result; + +const OPERATIONS: [(&str, Operation); 5] = [ + ("concatenation", |[a, b, c]| { + a.concat(b, true).concat(c, true).to_automaton() + }), + ("star", |[a, b, c]| { + a.union(b).repeat(0, None).concat(c, true).to_automaton() + }), + ("difference", |[a, b, c]| { + a.concat(b, true) + .to_automaton()? + .difference(&containing(c)?) + }), + ("intersection", |[a, b, c]| { + a.union(b) + .repeat(0, None) + .to_automaton()? + .intersection(&containing(c)?) + }), + ("union", |[a, b, c]| a.union(b).union(c).to_automaton()), +]; + +/// The strings containing a match of `regex`. +fn containing(regex: &RegularExpression) -> Result { + let total = RegularExpression::new_total(); + total + .concat(regex, true) + .concat(&total, true) + .to_automaton() +} + +/// Minimal DFAs of more than `max_states` states (and at most 80) from set +/// operations on `atoms`, by operation. +fn large_dfas( + atoms: &[RegularExpression], + max_states: usize, +) -> Vec<(&'static str, Vec)> { + let profile = ExecutionProfileBuilder::new() + .execution_timeout(300) + .max_number_of_states(5000) + .build(); + let mut seed = 0x9e37_79b9_7f4a_7c15u64; + let mut random = move |bound: usize| { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + (seed % bound as u64) as usize + }; + OPERATIONS + .iter() + .map(|(name, operation)| { + let mut dfas = vec![]; + for _ in 0..1000 { + if dfas.len() == PER_OPERATION { + break; + } + let operands = [0, 1, 2].map(|_| &atoms[random(atoms.len())]); + let dfa = profile.run(|| { + let dfa = minimal_dfa(&operation(&operands)?)?; + // State elimination must finish too, for the comparison. + let _ = dfa.to_regex()?; + Ok::<_, EngineError>(dfa) + }); + if let Ok(dfa) = dfa + && (max_states + 1..=80).contains(&dfa.number_of_states()) + { + dfas.push(dfa); + } + } + (*name, dfas) + }) + .collect() +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/mod.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/mod.rs new file mode 100644 index 0000000..d6f0f47 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/mod.rs @@ -0,0 +1,685 @@ +//! Decomposition: splits a minimal DFA the model cannot convert into +//! independent pieces, converts each one (by the model or state elimination, +//! whichever is simpler) and combines their regexes. +//! +//! For a trimmed minimal DFA with start state `s`, these splits are exact: +//! +//! - **star**, when `s` has incoming transitions: `L = R* E`, with `R` the +//! strings leading from `s` back to `s` without visiting it in between, and +//! `E` the strings accepted without returning to `s`; +//! - **concatenation**, when `s` has none and a state `v` lies on every +//! accepting path: `L = F L_v`, with `F` the strings leading from `s` to its +//! first visit of `v` and `L_v` the language of `v`; +//! - **frontier**, over a region `B` closed under successors, without `s`, +//! entered at a few states `x`: `L = L_A ∪ ⋃ F_x L_x`, with `L_A` what is +//! accepted without entering `B` and `F_x` the strings entering it first at +//! `x`; +//! - **union**, when `s` has none and removing it leaves several components: +//! the union of the components' languages; +//! - **hubs**, which breaks up a large strongly connected component: for a +//! few states `H` whose removal shatters it, the paths between `s`, the +//! hubs and acceptance that visit no hub in between are pieces without the +//! component's cycles, and the regex is state elimination over the small +//! graph of `s`, `H` and acceptance, labelled by them. +//! +//! Each piece is a DFA, minimized on its own, so it has fewer states (or +//! smaller cycles) and usually far fewer bases than the whole. +//! +//! A conversion first splits the DFA into its tree of pieces, sharing equal +//! ones, then runs the model on every piece small enough, in batches, then +//! keeps for each piece, from the leaves up, the simplest of state +//! elimination, the model and the composition of its pieces. + +use ahash::AHashMap; + +use super::{NeuralSynthesizer, minimal_dfa}; +use crate::{ + CharRange, IntSet, + error::EngineError, + execution_profile::ExecutionProfile, + fast_automaton::{FastAutomaton, State}, + regex::RegularExpression, +}; + +mod analysis; +#[cfg(test)] +mod bench; +mod pieces; + +/// Most pieces the model runs on per conversion. +const NEURAL_BUDGET: usize = 256; +/// Most pieces per conversion. +const MAX_PIECES: usize = 1024; +/// Pieces per model batch. +const BATCH: usize = 128; +/// Pieces whose state elimination regex is at most this complex are not +/// worth a model run. +const SKIP_COMPLEXITY: f64 = 4.0; + +/// What a conversion did, for tracing. +#[derive(Clone, Debug, Default)] +pub(super) struct Stats { + /// Distinct pieces, including the whole. + pieces: usize, + /// Splits by kind, over all the pieces. + star_splits: usize, + optional_splits: usize, + concat_splits: usize, + frontier_splits: usize, + union_splits: usize, + hub_splits: usize, + /// Pieces the model ran on, and those it found a correct regex for. + neural_runs: usize, + neural_correct: usize, + /// Pieces in the result whose regex is the model's. + neural_kept: usize, + /// Whether the result is a composition. + composed: bool, + /// The largest piece in the result not split further, in states. + max_piece_states: usize, +} + +/// How a piece splits; the `usize`s are pieces. +#[derive(Clone, Debug)] +enum Plan { + Leaf, + Star { + returns: usize, + exits: usize, + }, + Optional { + rest: usize, + }, + Concat { + prefix: usize, + suffix: usize, + }, + Frontier { + stay: Option, + crossings: Vec<(usize, usize)>, + }, + Union { + branches: Vec, + }, + /// `paths[x][y]`: the paths from source `x` (the start, then the hubs) + /// to target `y` (the hubs, then acceptance) visiting no hub in between; + /// `None` when there are none. + Hubs { + hubs: usize, + paths: Vec>>, + }, +} + +impl Plan { + /// The pieces the plan combines. + fn children(&self) -> Vec { + match self { + Plan::Leaf => vec![], + Plan::Star { returns, exits } => vec![*returns, *exits], + Plan::Optional { rest } => vec![*rest], + Plan::Concat { prefix, suffix } => vec![*prefix, *suffix], + Plan::Frontier { stay, crossings } => stay + .iter() + .copied() + .chain( + crossings + .iter() + .flat_map(|(prefix, suffix)| [*prefix, *suffix]), + ) + .collect(), + Plan::Union { branches } => branches.clone(), + Plan::Hubs { paths, .. } => paths.iter().flatten().flatten().copied().collect(), + } + } + + /// The regex the plan makes of its pieces' regexes, `None` for a leaf. + fn compose(&self, regexes: &[RegularExpression]) -> Option { + let regex = |piece: &usize| ®exes[*piece]; + Some(match self { + Plan::Leaf => return None, + Plan::Star { returns, exits } => { + regex(returns).repeat(0, None).concat(regex(exits), true) + } + Plan::Optional { rest } => regex(rest).repeat(0, Some(1)), + Plan::Concat { prefix, suffix } => regex(prefix).concat(regex(suffix), true), + Plan::Frontier { stay, crossings } => { + let branches: Vec = stay + .iter() + .map(|piece| regex(piece).clone()) + .chain( + crossings + .iter() + .map(|(prefix, suffix)| regex(prefix).concat(regex(suffix), true)), + ) + .collect(); + RegularExpression::union_all(&branches) + } + Plan::Union { branches } => RegularExpression::union_all(branches.iter().map(regex)), + Plan::Hubs { hubs, paths } => { + let paths: Vec>> = paths + .iter() + .map(|row| { + row.iter() + .map(|path| path.map(|p| regex(&p).clone())) + .collect() + }) + .collect(); + eliminate_hubs(*hubs, &paths) + } + }) + } +} + +/// A minimal DFA, its state elimination regex and how it splits. +struct Piece { + dfa: FastAutomaton, + eliminated: RegularExpression, + plan: Plan, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Origin { + Eliminated, + Neural, + Composed, +} + +/// A minimal DFA's states numbered by BFS from the start, following +/// transitions in the order of their characters: equal for isomorphic DFAs. +type CanonicalForm = Vec<(bool, Vec<(CharRange, usize)>)>; + +pub(super) struct Decomposer<'a> { + synthesizer: Option<&'a NeuralSynthesizer>, + /// Whether the model already failed on the whole automaton, so is not + /// run on it again. + pub(super) whole_tried: bool, + pub(super) stats: Stats, + /// Children come before their parents. + pieces: Vec, + /// The pieces of a split often share sub-pieces. + index: AHashMap, + /// Pieces started, finished or not: bounds the exploration. + started: usize, +} + +impl<'a> Decomposer<'a> { + /// A decomposer running `synthesizer` on the pieces, or state elimination + /// alone without one. + pub(super) fn new(synthesizer: Option<&'a NeuralSynthesizer>) -> Self { + Self { + synthesizer, + whole_tried: false, + stats: Stats::default(), + pieces: vec![], + index: AHashMap::new(), + started: 0, + } + } + + /// Converts `automaton`, keeping for every piece the simplest of state + /// elimination, the model and the composition of its pieces. + #[tracing::instrument(level = "debug", skip_all, fields(states = automaton.number_of_states()))] + pub(super) fn convert( + &mut self, + automaton: &FastAutomaton, + ) -> Result { + let root = self.explore(minimal_dfa(automaton)?)?; + self.stats.pieces = self.pieces.len(); + let proposed = self.propose(root)?; + + let mut regexes: Vec = Vec::with_capacity(self.pieces.len()); + let mut origins: Vec = Vec::with_capacity(self.pieces.len()); + for (piece, proposed) in self.pieces.iter().zip(proposed) { + let mut regex = piece.eliminated.clone(); + let mut origin = Origin::Eliminated; + if let Some(proposed) = proposed + && proposed.evaluate_complexity() <= regex.evaluate_complexity() + { + regex = proposed; + origin = Origin::Neural; + } + if let Some(composed) = piece.plan.compose(®exes) + && composed.evaluate_complexity() < regex.evaluate_complexity() + { + regex = composed; + origin = Origin::Composed; + } + regexes.push(regex); + origins.push(origin); + } + self.count_result(root, &origins); + Ok(regexes.swap_remove(root)) + } + + /// The model's proposal for each piece small enough, within the budget, + /// the largest pieces (the closest to the whole) first. + fn propose(&mut self, root: usize) -> Result>, EngineError> { + let mut proposed = vec![None; self.pieces.len()]; + let Some(synthesizer) = self.synthesizer else { + return Ok(proposed); + }; + let eligible: Vec = (0..self.pieces.len()) + .rev() + .filter(|&piece| { + let Piece { + dfa, eliminated, .. + } = &self.pieces[piece]; + !(self.whole_tried && piece == root) + && dfa.number_of_states() <= synthesizer.max_states() + && eliminated.evaluate_complexity() > SKIP_COMPLEXITY + }) + .take(NEURAL_BUDGET) + .collect(); + for batch in eligible.chunks(BATCH) { + let dfas: Vec<&FastAutomaton> = + batch.iter().map(|piece| &self.pieces[*piece].dfa).collect(); + for (piece, regex) in batch.iter().zip(synthesizer.propose_batch(&dfas)?) { + self.stats.neural_runs += 1; + if regex.is_some() { + self.stats.neural_correct += 1; + } + proposed[*piece] = regex; + } + } + Ok(proposed) + } + + fn count_result(&mut self, root: usize, origins: &[Origin]) { + self.stats.composed = origins[root] == Origin::Composed; + let mut seen = IntSet::default(); + let mut stack = vec![root]; + while let Some(piece) = stack.pop() { + if !seen.insert(piece) { + continue; + } + match origins[piece] { + Origin::Composed => stack.extend(self.pieces[piece].plan.children()), + origin => { + if origin == Origin::Neural { + self.stats.neural_kept += 1; + } + let states = self.pieces[piece].dfa.number_of_states(); + self.stats.max_piece_states = self.stats.max_piece_states.max(states); + } + } + } + } + + /// The piece for `dfa`, a minimal DFA, split recursively. + fn explore(&mut self, dfa: FastAutomaton) -> Result { + ExecutionProfile::get().assert_not_timed_out()?; + let canonical_form = pieces::canonical_form(&dfa)?; + if let Some(piece) = self.index.get(&canonical_form) { + return Ok(*piece); + } + self.started += 1; + let eliminated = dfa.to_regex()?; + let plan = if self.started > MAX_PIECES { + Plan::Leaf + } else { + self.plan(&dfa)? + }; + let piece = self.pieces.len(); + self.pieces.push(Piece { + dfa, + eliminated, + plan, + }); + self.index.insert(canonical_form, piece); + Ok(piece) + } + + /// How `dfa` splits, its pieces explored. + fn plan(&mut self, dfa: &FastAutomaton) -> Result { + let number_of_states = dfa.number_of_states(); + if number_of_states <= 1 || dfa.is_empty() { + return Ok(Plan::Leaf); + } + let start_state = dfa.start_state(); + let smaller = |piece: &FastAutomaton| piece.number_of_states() < number_of_states; + + if dfa.in_degree(start_state) > 0 { + let returns = pieces::first_return(dfa, start_state)?; + let exits = pieces::without_return(dfa, start_state)?; + if smaller(&returns) && smaller(&exits) { + self.stats.star_splits += 1; + return Ok(Plan::Star { + returns: self.explore(returns)?, + exits: self.explore(exits)?, + }); + } + if let Some(plan) = self.frontier(dfa)? { + return Ok(plan); + } + return self.hubs(dfa); + } + + if dfa.is_accepted(start_state) { + // The same states without the empty string, which lets the other + // splits apply. + let mut rest = dfa.clone(); + rest.unaccept(start_state); + self.stats.optional_splits += 1; + return Ok(Plan::Optional { + rest: self.explore(minimal_dfa(&rest)?)?, + }); + } + + if let Some(state) = analysis::closest_dominator(dfa) { + let prefix = pieces::up_to(dfa, state)?; + let suffix = pieces::language_of(dfa, state)?; + if smaller(&prefix) && smaller(&suffix) { + self.stats.concat_splits += 1; + return Ok(Plan::Concat { + prefix: self.explore(prefix)?, + suffix: self.explore(suffix)?, + }); + } + } + + if let Some(plan) = self.frontier(dfa)? { + return Ok(plan); + } + + let components = analysis::components_without(dfa, start_state); + if components.len() >= 2 { + self.stats.union_splits += 1; + let mut branches = Vec::with_capacity(components.len()); + for component in &components { + let branch = pieces::start_into(dfa, component)?; + branches.push(self.explore(branch)?); + } + return Ok(Plan::Union { branches }); + } + + self.hubs(dfa) + } + + fn frontier(&mut self, dfa: &FastAutomaton) -> Result, EngineError> { + let number_of_states = dfa.number_of_states(); + let smaller = |piece: &FastAutomaton| piece.number_of_states() < number_of_states; + let Some(analysis::Cut { region, entries }) = analysis::best_cut(dfa) else { + return Ok(None); + }; + let stay = pieces::outside(dfa, ®ion)?; + if !smaller(&stay) { + return Ok(None); + } + let mut crossings = Vec::with_capacity(entries.len()); + for &entry in &entries { + let prefix = pieces::entering_at(dfa, ®ion, entry)?; + let suffix = pieces::language_of(dfa, entry)?; + if !smaller(&prefix) || !smaller(&suffix) { + return Ok(None); + } + crossings.push((prefix, suffix)); + } + + self.stats.frontier_splits += 1; + let stay = if stay.is_empty() { + None + } else { + Some(self.explore(stay)?) + }; + let mut explored = Vec::with_capacity(crossings.len()); + for (prefix, suffix) in crossings { + explored.push((self.explore(prefix)?, self.explore(suffix)?)); + } + Ok(Some(Plan::Frontier { + stay, + crossings: explored, + })) + } + + fn hubs(&mut self, dfa: &FastAutomaton) -> Result { + let Some(hubs) = analysis::choose_hubs(dfa) else { + return Ok(Plan::Leaf); + }; + // Every piece must have smaller cycles, or the split need not end. + let weight = analysis::cyclic_weight(dfa); + let sources: Vec = std::iter::once(dfa.start_state()) + .chain(hubs.iter().copied()) + .collect(); + let targets: Vec> = hubs + .iter() + .copied() + .map(Some) + .chain(std::iter::once(None)) + .collect(); + let mut paths = Vec::with_capacity(sources.len()); + for &source in &sources { + let mut row = Vec::with_capacity(targets.len()); + for &target in &targets { + let path = pieces::between_hubs(dfa, source, &hubs, target)?; + if !path.is_empty() && analysis::cyclic_weight(&path) >= weight { + return Ok(Plan::Leaf); + } + row.push(path); + } + paths.push(row); + } + + self.stats.hub_splits += 1; + let mut explored = Vec::with_capacity(paths.len()); + for row in paths { + let mut explored_row = Vec::with_capacity(row.len()); + for path in row { + explored_row.push(if path.is_empty() { + None + } else { + Some(self.explore(path)?) + }); + } + explored.push(explored_row); + } + Ok(Plan::Hubs { + hubs: hubs.len(), + paths: explored, + }) + } +} + +/// State elimination over the start (node 0), the `hubs` (1 to `hubs`) and +/// acceptance (`hubs + 1`), with `paths[x][y - 1]` from `x` to `y`: the +/// simplest result over the elimination orders. +fn eliminate_hubs(hubs: usize, paths: &[Vec>]) -> RegularExpression { + let accept = hubs + 1; + let mut orders: Vec> = vec![vec![]]; + for _ in 0..hubs { + let mut longer = Vec::with_capacity(orders.len() * hubs); + for order in &orders { + for hub in (1..=hubs).filter(|hub| !order.contains(hub)) { + let mut order = order.clone(); + order.push(hub); + longer.push(order); + } + } + orders = longer; + } + + let mut best: Option = None; + for order in orders { + // `edges[x][y]`, from node `x` to node `y`. + let mut edges: Vec>> = paths + .iter() + .map(|row| std::iter::once(None).chain(row.iter().cloned()).collect()) + .collect(); + let mut remaining: Vec = (1..=hubs).collect(); + for hub in order { + remaining.retain(|node| *node != hub); + let around = edges[hub][hub] + .as_ref() + .map_or_else(RegularExpression::new_empty_string, |cycle| { + cycle.repeat(0, None) + }); + let from_nodes: Vec = std::iter::once(0) + .chain(remaining.iter().copied()) + .collect(); + let to_nodes: Vec = remaining + .iter() + .copied() + .chain(std::iter::once(accept)) + .collect(); + for &from_node in &from_nodes { + let Some(into) = edges[from_node][hub].clone() else { + continue; + }; + for &to_node in &to_nodes { + let Some(out) = &edges[hub][to_node] else { + continue; + }; + let through = into.concat(&around, true).concat(out, true); + edges[from_node][to_node] = Some(match &edges[from_node][to_node] { + Some(direct) => direct.union(&through), + None => through, + }); + } + } + } + let regex = edges[0][accept] + .take() + .unwrap_or_else(RegularExpression::new_empty); + if best + .as_ref() + .is_none_or(|best| regex.evaluate_complexity() < best.evaluate_complexity()) + { + best = Some(regex); + } + } + best.unwrap_or_else(RegularExpression::new_empty) +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use regex_charclass::char::Char; + + use super::*; + + fn assert_keeps_the_language(automaton: &FastAutomaton) -> Stats { + let mut decomposer = Decomposer::new(None); + let regex = decomposer.convert(automaton).unwrap(); + assert!( + regex.to_automaton().unwrap().equivalent(automaton).unwrap(), + "{regex}" + ); + decomposer.stats + } + + #[test] + fn keeps_the_language() { + for pattern in [ + "a", + "", + "a*", + "(ab|cd)*efg", + "(abc|def)xyz", + "(abc|def)(x|yz)+", + "x(ab)*y(cd)*z", + "(a|b)*abb", + ".*abc.*def.*", + "(ab)?c", + "a(b|c)*d(e|f)*", + "((ab)*c)*d", + "[a-z]{2,6}q", + "(a+|b+)c(d|e)?", + ] { + let automaton = RegularExpression::new(pattern) + .unwrap() + .to_automaton() + .unwrap(); + assert_keeps_the_language(&automaton); + } + } + + /// Intersections, whose largest strongly connected component the other + /// splits leave whole. + #[test] + fn hub_splits_keep_the_language() { + let mut hub_splits = 0; + for (a, b) in [ + ("(a|b)*a(a|b){3}", ".*"), + ("((ab|ba)*c)*", ".*"), + (".*abc.*", ".*def.*"), + ("[a-e]*", ".*a.*b.*c.*"), + ("(xa*b*)*", ".*ab.*ba.*"), + ] { + let a = RegularExpression::new(a).unwrap().to_automaton().unwrap(); + let b = RegularExpression::new(b).unwrap().to_automaton().unwrap(); + let automaton = a.intersection(&b).unwrap(); + hub_splits += assert_keeps_the_language(&automaton).hub_splits; + } + assert!(hub_splits > 0); + } + + /// The stars state elimination of the minimal DFA tangles, the splits + /// find back. + #[test] + fn simplifies_nested_stars() { + let automaton = RegularExpression::new("((ab)*c(de)*f)*g") + .unwrap() + .to_automaton() + .unwrap(); + let dfa = minimal_dfa(&automaton).unwrap(); + assert_eq!( + dfa.to_regex().unwrap().to_string(), + "((ab)*c(de|fab(ab)*c|fc)*f)?g" + ); + let regex = Decomposer::new(None).convert(&dfa).unwrap(); + assert_eq!(regex.to_string(), "((ab)*c(de)*f)*g"); + } + + /// A small palette of character ranges for random automata. + fn palette(index: usize) -> CharRange { + let bounds = [ + ('a', 'a'), + ('b', 'b'), + ('c', 'c'), + ('a', 'c'), + ('b', 'd'), + ('x', 'z'), + ('\u{0}', '\u{10FFFF}'), + ]; + let (low, high) = bounds[index % bounds.len()]; + CharRange::new_from_range(Char::new(low)..=Char::new(high)) + } + + fn arb_automaton() -> impl Strategy { + ( + 2usize..7, + proptest::collection::vec((0usize..6, 0usize..6, 0usize..7), 1..15), + 0u8..=255, + ) + .prop_map(|(number_of_states, edges, accept_mask)| { + let mut automaton = FastAutomaton::new_empty(); + for _ in 1..number_of_states { + automaton.new_state(); + } + for (from_state, to_state, range) in edges { + automaton + .add_transition_from_range( + from_state % number_of_states, + to_state % number_of_states, + &palette(range), + ) + .unwrap(); + } + for state in 0..number_of_states { + if accept_mask & (1 << state) != 0 { + automaton.accept(state); + } + } + automaton + }) + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(256))] + + #[test] + fn keeps_the_language_of_random_automata(automaton in arb_automaton()) { + let regex = Decomposer::new(None).convert(&automaton).unwrap(); + prop_assert!(regex.to_automaton().unwrap().equivalent(&automaton).unwrap()); + } + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/pieces.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/pieces.rs new file mode 100644 index 0000000..0e19178 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/decompose/pieces.rs @@ -0,0 +1,188 @@ +//! The pieces of a split: sub-automata of a minimal DFA, each minimized. + +use super::*; +use crate::IntMap; + +/// `dfa`'s canonical form (see [`CanonicalForm`]); `dfa` must be minimal. +pub(super) fn canonical_form(dfa: &FastAutomaton) -> Result { + let spanning_set = dfa.spanning_set(); + let mut order = vec![dfa.start_state()]; + let mut numbers: IntMap = IntMap::default(); + numbers.insert(dfa.start_state(), 0); + let mut form = Vec::with_capacity(dfa.number_of_states()); + let mut next = 0; + while let Some(&state) = order.get(next) { + next += 1; + let mut transitions = Vec::with_capacity(dfa.out_degree(state)); + for (condition, to_state) in dfa.transitions_from(state) { + transitions.push((condition.to_range(spanning_set)?, *to_state)); + } + transitions.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + let transitions = transitions + .into_iter() + .map(|(range, to_state)| { + let number = *numbers.entry(to_state).or_insert_with(|| { + order.push(to_state); + order.len() - 1 + }); + (range, number) + }) + .collect(); + form.push((dfa.is_accepted(state), transitions)); + } + Ok(form) +} + +/// The strings leading from `state` back to it, without visiting it in +/// between. +pub(super) fn first_return( + dfa: &FastAutomaton, + state: State, +) -> Result { + let mut piece = dfa.clone(); + let start_state = copy_outgoing(&mut piece, state); + remove_outgoing(&mut piece, state); + accept_only(&mut piece, state); + start_at(&mut piece, start_state); + minimal_dfa(&piece) +} + +/// The strings accepted from `state`, the start, without returning to it. +pub(super) fn without_return( + dfa: &FastAutomaton, + state: State, +) -> Result { + let mut piece = dfa.clone(); + for (from_state, _) in dfa.transitions_to_vec(state) { + piece.remove_transition(from_state, state); + } + minimal_dfa(&piece) +} + +/// The strings leading from the start to its first visit of `state`. +pub(super) fn up_to(dfa: &FastAutomaton, state: State) -> Result { + let mut piece = dfa.clone(); + remove_outgoing(&mut piece, state); + accept_only(&mut piece, state); + minimal_dfa(&piece) +} + +/// The strings accepted from `state`. +pub(super) fn language_of(dfa: &FastAutomaton, state: State) -> Result { + let mut piece = dfa.clone(); + start_at(&mut piece, state); + minimal_dfa(&piece) +} + +/// `dfa` with only the transitions from the start into `component`, and the +/// start not accepting. +pub(super) fn start_into( + dfa: &FastAutomaton, + component: &IntSet, +) -> Result { + let start_state = dfa.start_state(); + let mut piece = dfa.clone(); + for to_state in dfa.direct_states(start_state) { + if !component.contains(&to_state) { + piece.remove_transition(start_state, to_state); + } + } + piece.unaccept(start_state); + minimal_dfa(&piece) +} + +/// The strings accepted without entering `region`. +pub(super) fn outside( + dfa: &FastAutomaton, + region: &IntSet, +) -> Result { + let mut piece = dfa.clone(); + remove_entries(dfa, &mut piece, region, None); + minimal_dfa(&piece) +} + +/// The strings leading from the start into `region`, entering it first at +/// `entry`. +pub(super) fn entering_at( + dfa: &FastAutomaton, + region: &IntSet, + entry: State, +) -> Result { + let mut piece = dfa.clone(); + remove_entries(dfa, &mut piece, region, Some(entry)); + remove_outgoing(&mut piece, entry); + accept_only(&mut piece, entry); + minimal_dfa(&piece) +} + +/// The paths from `source` to `target` (acceptance if `None`) that visit no +/// hub after leaving `source`, except `target` at the end. +pub(super) fn between_hubs( + dfa: &FastAutomaton, + source: State, + hubs: &[State], + target: Option, +) -> Result { + let mut piece = dfa.clone(); + let start_state = copy_outgoing(&mut piece, source); + for &hub in hubs { + remove_outgoing(&mut piece, hub); + } + match target { + Some(target) => accept_only(&mut piece, target), + None => { + for &hub in hubs { + piece.unaccept(hub); + } + if dfa.is_accepted(source) { + piece.accept(start_state); + } + } + } + start_at(&mut piece, start_state); + minimal_dfa(&piece) +} + +/// A new state with the same outgoing transitions as `state`. +fn copy_outgoing(automaton: &mut FastAutomaton, state: State) -> State { + let copy = automaton.new_state(); + for (condition, to_state) in automaton.transitions_from_vec(state) { + automaton.add_transition(copy, to_state, &condition); + } + copy +} + +/// Removes the transitions of `piece` (a copy of `dfa`) entering `region` +/// from outside it, but those to `kept`. +fn remove_entries( + dfa: &FastAutomaton, + piece: &mut FastAutomaton, + region: &IntSet, + kept: Option, +) { + for from_state in dfa.states().filter(|state| !region.contains(state)) { + for to_state in dfa.direct_states(from_state) { + if region.contains(&to_state) && Some(to_state) != kept { + piece.remove_transition(from_state, to_state); + } + } + } +} + +fn remove_outgoing(automaton: &mut FastAutomaton, state: State) { + for to_state in automaton.direct_states_vec(state) { + automaton.remove_transition(state, to_state); + } +} + +fn accept_only(automaton: &mut FastAutomaton, state: State) { + for accept_state in automaton.accept_states().clone() { + automaton.unaccept(accept_state); + } + automaton.accept(state); +} + +fn start_at(automaton: &mut FastAutomaton, state: State) { + automaton.start_state = state; + automaton.minimal = false; +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/hub.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/hub.rs new file mode 100644 index 0000000..00fbf7b --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/hub.rs @@ -0,0 +1,72 @@ +//! Downloads models from the Hugging Face Hub. +//! +//! Files go through the Hub's local cache (`HF_HUB_CACHE`, else +//! `HF_HOME/hub`, else `~/.cache/huggingface/hub`), so a model is downloaded +//! once. `HF_TOKEN` authenticates for private models, and `HF_ENDPOINT` +//! points to another Hub. + +use std::path::PathBuf; + +use super::{Device, NeuralSynthesisError, NeuralSynthesizer}; + +impl NeuralSynthesizer { + /// The model [`from_hub`](Self::from_hub) loads by default. + pub const DEFAULT_HUB_MODEL: &str = "alexvbrdn/kleene1-9m-b16-t128"; + + /// Downloads a model from the Hugging Face Hub (its latest revision) and + /// loads it. `repo_id` is `owner/name`, e.g. + /// [`DEFAULT_HUB_MODEL`](Self::DEFAULT_HUB_MODEL). + pub fn from_hub(repo_id: &str, device: Device) -> Result { + Self::from_hub_revision(repo_id, "main", device) + } + + /// Downloads a model from the Hugging Face Hub at `revision` (a branch, + /// a release tag such as `v1.0`, or a commit hash) and loads it. + pub fn from_hub_revision( + repo_id: &str, + revision: &str, + device: Device, + ) -> Result { + let [config, weights] = download(repo_id, revision, ["config.json", "model.safetensors"])?; + Self::from_files(config, weights, device) + } +} + +fn download( + repo_id: &str, + revision: &str, + files: [&str; N], +) -> Result<[PathBuf; N], NeuralSynthesisError> { + let hub_error = |err: hf_hub::HFError| NeuralSynthesisError::Hub(format!("{repo_id}: {err}")); + let Some((owner, name)) = repo_id.split_once('/') else { + return Err(NeuralSynthesisError::Hub(format!( + "{repo_id}: expected a repository id of the form owner/name" + ))); + }; + let repository = hf_hub::HFClientSync::new() + .map_err(hub_error)? + .model(owner, name); + let mut paths = files.map(|_| PathBuf::new()); + for (path, file) in paths.iter_mut().zip(files) { + *path = repository + .download_file() + .filename(file) + .revision(revision.to_string()) + .send() + .map_err(hub_error)?; + } + Ok(paths) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_malformed_repository_ids() { + assert!(matches!( + NeuralSynthesizer::from_hub("kleene1-9m-b16-t128", Device::Cpu), + Err(NeuralSynthesisError::Hub(_)) + )); + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/attention.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/attention.rs new file mode 100644 index 0000000..481f211 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/attention.rs @@ -0,0 +1,253 @@ +//! The decoder's attention of one query per row over cached keys and values. +//! +//! On the CPU, [`AttendOne`] computes it in one pass per row and head: the +//! scores, their softmax and the weighted sum of the values, with no +//! intermediate tensor. Built from tensor operations instead +//! ([`attend_one_unfused`], for the other devices), each step writes a +//! `[rows, heads, keys, d / heads]` temporary, which dominates decoding. + +use candle_core::{CpuStorage, CustomOp3, D, Layout, Result, Shape, Tensor, bail}; + +/// One query per row, `q` `[B, d]`, over `keys` and `values` `[B or 1, +/// heads, T, d / heads]`, each row attending to its first `lengths[row]` keys (all +/// without `lengths`): `[B, d]`, before the output projection. Every tensor +/// must be `f32` and have unit stride on its last dimension. +pub(super) struct AttendOne<'a> { + pub(super) heads: usize, + pub(super) lengths: Option<&'a [u32]>, +} + +impl CustomOp3 for AttendOne<'_> { + fn name(&self) -> &'static str { + "attend-one" + } + + fn cpu_fwd( + &self, + q: &CpuStorage, + q_layout: &Layout, + keys: &CpuStorage, + keys_layout: &Layout, + values: &CpuStorage, + values_layout: &Layout, + ) -> Result<(CpuStorage, Shape)> { + let (rows, d) = q_layout.shape().dims2()?; + let (key_rows, heads, length, hd) = keys_layout.shape().dims4()?; + if values_layout.shape() != keys_layout.shape() + || heads != self.heads + || heads * hd != d + || (key_rows != 1 && key_rows != rows) + || self.lengths.is_some_and(|lengths| lengths.len() != rows) + { + bail!( + "attend-one: q {:?}, keys {:?}, values {:?}, {} heads", + q_layout.shape(), + keys_layout.shape(), + values_layout.shape(), + self.heads + ); + } + let layouts = [q_layout, keys_layout, values_layout]; + if layouts + .iter() + .any(|layout| layout.stride().last() != Some(&1)) + { + bail!("attend-one: the last dimensions must be contiguous"); + } + + let q = Strided::new(q, q_layout)?; + let keys = Strided::new(keys, keys_layout)?; + let values = Strided::new(values, values_layout)?; + let sqrt_hd = (hd as f32).sqrt(); + + let attend_row = |row: usize, out: &mut [f32]| { + let key_row = if key_rows == 1 { 0 } else { row }; + let length = self + .lengths + .map_or(length, |lengths| (lengths[row] as usize).min(length)); + let mut scores = vec![0f32; length]; + for head in 0..heads { + let query = q.slice(&[row, head * hd], hd); + let mut max = f32::NEG_INFINITY; + for (position, score) in scores.iter_mut().enumerate() { + let key = keys.slice(&[key_row, head, position, 0], hd); + *score = dot(query, key) / sqrt_hd; + max = max.max(*score); + } + let mut sum = 0.0; + for score in &mut scores { + *score = (*score - max).exp(); + sum += *score; + } + let out = &mut out[head * hd..(head + 1) * hd]; + for (position, score) in scores.iter().enumerate() { + let weight = score / sum; + let value = values.slice(&[key_row, head, position, 0], hd); + for (out, value) in out.iter_mut().zip(value) { + *out += value * weight; + } + } + } + }; + + let mut attended = vec![0f32; rows * d]; + #[cfg(all(feature = "parallel", not(target_family = "wasm")))] + { + use rayon::prelude::*; + attended + .par_chunks_mut(d) + .enumerate() + .for_each(|(row, out)| attend_row(row, out)); + } + #[cfg(any(not(feature = "parallel"), target_family = "wasm"))] + for (row, out) in attended.chunks_mut(d).enumerate() { + attend_row(row, out); + } + Ok((CpuStorage::F32(attended), Shape::from((rows, d)))) + } +} + +/// A strided view of an `f32` storage. +struct Strided<'a> { + data: &'a [f32], + offset: usize, + stride: Vec, +} + +impl<'a> Strided<'a> { + fn new(storage: &'a CpuStorage, layout: &Layout) -> Result { + Ok(Self { + data: storage.as_slice::()?, + offset: layout.start_offset(), + stride: layout.stride().to_vec(), + }) + } + + /// The `len` contiguous values from the element at `index`. + fn slice(&self, index: &[usize], len: usize) -> &[f32] { + let start = self.offset + + index + .iter() + .zip(&self.stride) + .map(|(i, stride)| i * stride) + .sum::(); + &self.data[start..start + len] + } +} + +fn dot(a: &[f32], b: &[f32]) -> f32 { + a.iter().zip(b).map(|(a, b)| a * b).sum() +} + +/// [`AttendOne`] from tensor operations, `keys` where `key_mask` `[B, 1, T]` +/// is -∞ left out. +/// +/// Multiply-and-sum rather than matmul: candle runs a batched matmul as one +/// gemm call per matrix, far slower for `B × heads` single rows. +pub(super) fn attend_one_unfused( + q: &Tensor, + keys: &Tensor, + values: &Tensor, + key_mask: Option<&Tensor>, + heads: usize, +) -> Result { + let (rows, d) = q.dims2()?; + let hd = d / heads; + let q = q.reshape((rows, heads, 1, hd))?; + let mut scores = (keys.broadcast_mul(&q)?.sum(D::Minus1)? / (hd as f64).sqrt())?; + if let Some(key_mask) = key_mask { + scores = scores.broadcast_add(key_mask)?; + } + let weights = candle_nn::ops::softmax_last_dim(&scores)?.unsqueeze(D::Minus1)?; + values.broadcast_mul(&weights)?.sum(2)?.reshape((rows, d)) +} + +#[cfg(test)] +mod tests { + use candle_core::Device; + + use super::*; + + fn random(shape: &[usize]) -> Tensor { + Tensor::randn(0f32, 1f32, shape, &Device::Cpu).unwrap() + } + + fn assert_close(a: &Tensor, b: &Tensor) { + let difference = (a - b) + .unwrap() + .abs() + .unwrap() + .flatten_all() + .unwrap() + .max(0) + .unwrap() + .to_scalar::() + .unwrap(); + assert!(difference < 1e-5, "difference {difference}"); + } + + #[test] + fn matches_the_tensor_operations() { + let (rows, heads, hd) = (5, 4, 8); + let q = random(&[rows, heads * hd]); + + // Per-row keys, read through a narrowed buffer as the self-attention + // cache does. + let keys = random(&[rows, heads, 12, hd]).narrow(2, 0, 7).unwrap(); + let values = random(&[rows, heads, 12, hd]).narrow(2, 0, 7).unwrap(); + let fused = q + .apply_op3_no_bwd( + &keys, + &values, + &AttendOne { + heads, + lengths: None, + }, + ) + .unwrap(); + let unfused = attend_one_unfused(&q, &keys, &values, None, heads).unwrap(); + assert_close(&fused, &unfused); + + // Keys shared by every row, with each row's own number of them, as + // the cross-attention does. + let keys = random(&[1, heads, 6, hd]); + let values = random(&[1, heads, 6, hd]); + let lengths = [6, 2, 5, 1, 3]; + let key_mask: Vec = lengths + .iter() + .flat_map(|length| { + (0..6).map(move |key| { + if key < *length { + 0.0 + } else { + f32::NEG_INFINITY + } + }) + }) + .collect(); + let key_mask = Tensor::from_vec(key_mask, (rows, 1, 6), &Device::Cpu).unwrap(); + let fused = q + .apply_op3_no_bwd( + &keys, + &values, + &AttendOne { + heads, + lengths: Some(&lengths), + }, + ) + .unwrap(); + let unfused = attend_one_unfused(&q, &keys, &values, Some(&key_mask), heads).unwrap(); + assert_close(&fused, &unfused); + } + + #[test] + fn rejects_mismatched_shapes() { + let q = random(&[3, 32]); + let keys = random(&[2, 4, 5, 8]); + let attend = AttendOne { + heads: 4, + lengths: None, + }; + assert!(q.apply_op3_no_bwd(&keys, &keys, &attend).is_err()); + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/canonical.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/canonical.rs new file mode 100644 index 0000000..d00d5f9 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/canonical.rs @@ -0,0 +1,373 @@ +//! Canonical form of a minimal DFA, invariant under state renumbering and +//! base relabeling: the form the model was trained on. +//! +//! Color refinement partitions the bases into classes that no isomorphism can +//! mix; only bases tied within a class are permuted. For each candidate base +//! order the states are numbered by BFS from the start (so the start is always +//! 0), and the lexicographically smallest encoding wins. + +use super::*; + +/// Above this many candidate base orders, ties are broken by the original +/// base order. +const MAX_BASE_PERMUTATIONS: usize = 5040; + +pub(super) const DEAD: u32 = u32::MAX; + +/// A DFA in canonical form. +#[derive(Clone, Debug)] +pub(super) struct CanonicalDfa { + pub n: usize, + pub k: usize, + /// Whether each canonical state accepts. The start is state 0. + pub accept: Vec, + /// `delta[s * k + b]`: target of canonical state `s` on canonical base + /// `b`, or [`DEAD`]. + pub delta: Vec, + /// Canonical base `i` as a character range. + pub bases: Vec, +} + +/// The bases of `spanning_set` in condition bit order (the rest first, when +/// non-empty). +fn bases(spanning_set: &SpanningSet) -> Vec { + std::iter::once(spanning_set.rest()) + .filter(|rest| !rest.is_empty()) + .chain(spanning_set.spanning_ranges()) + .cloned() + .collect() +} + +/// Canonicalizes a minimal (hence deterministic and trimmed) automaton. +pub(super) fn canonicalize(automaton: &FastAutomaton) -> CanonicalDfa { + debug_assert!(automaton.is_deterministic()); + let bases = bases(automaton.spanning_set()); + let k = bases.len(); + + let raw_states = automaton.states_vec(); + let n = raw_states.len(); + let max_raw = raw_states.iter().max().map_or(0, |s| s + 1); + let mut dense = vec![DEAD; max_raw]; + for (i, s) in raw_states.iter().enumerate() { + dense[*s] = i as u32; + } + + let mut delta = vec![DEAD; n * k]; + for (i, &from) in raw_states.iter().enumerate() { + for (condition, &to) in automaton.transitions_from(from) { + for b in condition.iter_set_bits() { + delta[i * k + b] = dense[to]; + } + } + } + let table = Table { + n, + k, + delta, + start: dense[automaton.start_state()] as usize, + accept: raw_states + .iter() + .map(|s| automaton.is_accepted(*s)) + .collect(), + }; + let (best, order) = table.canonical_encoding(); + + let mut accept = vec![false; n]; + for s in best.accepts { + accept[s as usize] = true; + } + CanonicalDfa { + n, + k, + accept, + delta: best.delta, + bases: order.iter().map(|b| bases[*b].clone()).collect(), + } +} + +struct Table { + n: usize, + k: usize, + /// `delta[s * k + b]`: target of state `s` on base `b`, or [`DEAD`]. + delta: Vec, + start: usize, + accept: Vec, +} + +/// The DFA renumbered for one base order. +struct Relabeled { + /// Sorted. + accepts: Vec, + /// Row-major over canonical states and bases. + delta: Vec, +} + +impl Relabeled { + fn cmp_key(&self) -> (&[u32], &[u32]) { + (&self.accepts, &self.delta) + } +} + +impl Table { + fn target(&self, s: usize, b: usize) -> Option { + let t = self.delta[s * self.k + b]; + (t != DEAD).then_some(t as usize) + } + + /// Returns the smallest encoding and its base order. + fn canonical_encoding(&self) -> (Relabeled, Vec) { + let base_colors = self.refine(); + + let mut order: Vec = (0..self.k).collect(); + order.sort_by_key(|b| (base_colors[*b], *b)); + let mut groups: Vec> = Vec::new(); + let mut i = 0; + while i < self.k { + let mut j = i + 1; + while j < self.k && base_colors[order[j]] == base_colors[order[i]] { + j += 1; + } + if j - i > 1 { + groups.push(i..j); + } + i = j; + } + + let mut product: usize = 1; + let exact = groups.iter().all(|g| { + product = (1..=g.len()).fold(product, |p, f| p.saturating_mul(f)); + product <= MAX_BASE_PERMUTATIONS + }); + if !exact { + return (self.relabel(&order), order); + } + + let mut best = self.relabel(&order); + let mut best_order = order.clone(); + // Odometer over per-group permutations; each group starts sorted + // (ascending original index) and `next_permutation` wraps it back. + loop { + let mut advanced = false; + for g in &groups { + if next_permutation(&mut order[g.clone()]) { + advanced = true; + break; + } + } + if !advanced { + break; + } + let candidate = self.relabel(&order); + if candidate.cmp_key() < best.cmp_key() { + best = candidate; + best_order.clone_from(&order); + } + } + (best, best_order) + } + + fn relabel(&self, order: &[usize]) -> Relabeled { + let (n, k) = (self.n, self.k); + let mut numbering = vec![DEAD; n]; + let mut queue = Vec::with_capacity(n); + numbering[self.start] = 0; + queue.push(self.start); + let mut head = 0; + while head < queue.len() { + let s = queue[head]; + head += 1; + for &b in order { + if let Some(t) = self.target(s, b) + && numbering[t] == DEAD + { + numbering[t] = queue.len() as u32; + queue.push(t); + } + } + } + // A minimal DFA has every state reachable; number any stragglers + // anyway so the encoding stays total. + for (s, number) in numbering.iter_mut().enumerate() { + if *number == DEAD { + *number = queue.len() as u32; + queue.push(s); + } + } + + let mut delta = vec![DEAD; n * k]; + for (new_s, &s) in queue.iter().enumerate() { + for (j, &b) in order.iter().enumerate() { + if let Some(t) = self.target(s, b) { + delta[new_s * k + j] = numbering[t]; + } + } + } + let mut accepts: Vec = (0..n) + .filter(|s| self.accept[*s]) + .map(|s| numbering[s]) + .collect(); + accepts.sort_unstable(); + Relabeled { accepts, delta } + } + + /// Alternating color refinement of states and bases; returns the stable + /// base colors. Colors are ranks of sorted signatures, so they are + /// invariant under renumbering and relabeling. + fn refine(&self) -> Vec { + let (n, k) = (self.n, self.k); + let mut state_colors = ranks( + &(0..n) + .map(|s| { + let out = (0..k).filter(|b| self.target(s, *b).is_some()).count(); + vec![(s == self.start) as u32, self.accept[s] as u32, out as u32] + }) + .collect::>(), + ); + let mut base_colors = ranks( + &(0..k) + .map(|b| { + let (mut defined, mut loops, mut into_accept) = (0, 0, 0); + for s in 0..n { + if let Some(t) = self.target(s, b) { + defined += 1; + loops += (t == s) as u32; + into_accept += self.accept[t] as u32; + } + } + vec![defined, loops, into_accept] + }) + .collect::>(), + ); + + let classes = |colors: &[u32]| colors.iter().max().map_or(0, |m| m + 1); + let mut total = classes(&state_colors) + classes(&base_colors); + loop { + let color_of = |t: Option, colors: &[u32]| t.map_or(DEAD, |t| colors[t]); + state_colors = ranks( + &(0..n) + .map(|s| { + let mut sig: Vec<(u32, u32)> = (0..k) + .map(|b| (base_colors[b], color_of(self.target(s, b), &state_colors))) + .collect(); + sig.sort_unstable(); + std::iter::once(state_colors[s]) + .chain(sig.into_iter().flat_map(|(a, b)| [a, b])) + .collect::>() + }) + .collect::>(), + ); + base_colors = ranks( + &(0..k) + .map(|b| { + let mut sig: Vec<(u32, u32)> = (0..n) + .map(|s| (state_colors[s], color_of(self.target(s, b), &state_colors))) + .collect(); + sig.sort_unstable(); + std::iter::once(base_colors[b]) + .chain(sig.into_iter().flat_map(|(a, b)| [a, b])) + .collect::>() + }) + .collect::>(), + ); + let new_total = classes(&state_colors) + classes(&base_colors); + if new_total == total { + return base_colors; + } + total = new_total; + } + } +} + +/// Maps each signature to its rank among the distinct sorted signatures. +fn ranks(signatures: &[T]) -> Vec { + let mut sorted = signatures.to_vec(); + sorted.sort_unstable(); + sorted.dedup(); + signatures + .iter() + .map(|s| sorted.binary_search(s).unwrap() as u32) + .collect() +} + +/// Advances `v` to its next lexicographic permutation; on the last one, +/// resets it to ascending order and returns `false`. +fn next_permutation(v: &mut [usize]) -> bool { + if v.len() < 2 { + return false; + } + let mut i = v.len() - 1; + while i > 0 && v[i - 1] >= v[i] { + i -= 1; + } + if i == 0 { + v.reverse(); + return false; + } + let mut j = v.len() - 1; + while v[j] <= v[i - 1] { + j -= 1; + } + v.swap(i - 1, j); + v[i..].reverse(); + true +} + +#[cfg(test)] +mod tests { + use crate::neural_synthesis::minimal_dfa; + + use super::*; + + fn canonical(pattern: &str) -> CanonicalDfa { + canonicalize( + &minimal_dfa( + &RegularExpression::new(pattern) + .unwrap() + .to_automaton() + .unwrap(), + ) + .unwrap(), + ) + } + + fn key(pattern: &str) -> (Vec, Vec) { + let c = canonical(pattern); + (c.accept, c.delta) + } + + #[test] + fn invariant_under_relabeling() { + assert_eq!(key("(ab|cd)+e"), key("(cd|ab)(ab|cd)*e")); + assert_eq!(key("a(ba)*"), key("(ab)*a")); + assert_eq!(key("a*b"), key("b*a")); + assert_eq!(key("x(y|z)*w"), key("q(r|s)*t")); + assert_ne!(key("ab"), key("ab*")); + assert_ne!(key("a*b"), key("a*a")); + } + + #[test] + fn start_is_zero_and_states_are_bfs_numbered() { + let c = canonical("a(b|c)*d"); + // Bases: the rest, `a`, `[bc]`, `d`. + assert_eq!((c.n, c.k), (3, 4)); + // From the start, only `a` leads anywhere: to state 1. + assert_eq!( + (0..c.k) + .filter(|b| c.delta[*b] != DEAD) + .map(|b| c.delta[b]) + .collect::>(), + vec![1] + ); + } + + #[test] + fn next_permutation_enumerates_all() { + let mut v = vec![0, 1, 2, 3]; + let mut count = 1; + while next_permutation(&mut v) { + count += 1; + } + assert_eq!(count, 24); + assert_eq!(v, vec![0, 1, 2, 3]); + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/grammar.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/grammar.rs new file mode 100644 index 0000000..5a85594 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/grammar.rs @@ -0,0 +1,126 @@ +//! Constrains decoding to well-formed regex trees. + +use super::tokens::{ALT, CHAR, CONCAT, COUNT_BASE, COUNT_INF, END, EOS, REPEAT}; + +/// What a sequence being decoded still expects; each costs at least one more +/// token. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Pending { + Node, + /// Children until `END`. + List, + Min, + Max, +} + +/// Tracks one sequence being decoded and says which tokens may come next, so +/// the output is always one tree (see [`super::tokens`]) closed within +/// `max_len` positions, BOS and EOS included. +#[derive(Clone, Debug)] +pub(super) struct PrefixGrammar { + max_len: usize, + stack: Vec, + /// Positions so far, BOS included. + length: usize, + /// The minimum of the repeat being closed. + min: u32, +} + +impl PrefixGrammar { + pub(super) fn new(max_len: usize) -> Self { + Self { + max_len, + stack: vec![Pending::Node], + length: 1, + min: 0, + } + } + + fn expands(token: u32) -> &'static [Pending] { + match token { + // Top last: the minimum comes first, then the maximum, then the node. + REPEAT => &[Pending::Node, Pending::Max, Pending::Min], + CONCAT | ALT => &[Pending::List], + _ => &[], + } + } + + fn stack_size_after(&self, top: Pending, token: u32) -> usize { + let pops = top != Pending::List || token == END; + self.stack.len() - pops as usize + Self::expands(token).len() + } + + /// Whether `token` may come next. + pub(super) fn allows(&self, token: u32) -> bool { + let Some(&top) = self.stack.last() else { + return token == EOS; + }; + let candidate = match top { + Pending::Min => (COUNT_BASE..COUNT_INF).contains(&token), + Pending::Max => (COUNT_BASE + self.min..=COUNT_INF).contains(&token), + Pending::Node => matches!(token, CHAR | REPEAT | CONCAT | ALT), + Pending::List => matches!(token, CHAR | REPEAT | CONCAT | ALT | END), + }; + // The token, then the open stack, then EOS must fit. + candidate && self.length + 1 + self.stack_size_after(top, token) < self.max_len + } + + /// Applies `token`, which must be allowed. + pub(super) fn step(&mut self, token: u32) { + let Some(&top) = self.stack.last() else { + return; + }; + if top != Pending::List || token == END { + self.stack.pop(); + } + if top == Pending::Min { + self.min = token - COUNT_BASE; + } + self.stack.extend_from_slice(Self::expands(token)); + self.length += 1; + } +} + +#[cfg(test)] +mod tests { + use crate::neural_synthesis::kleene::tokens::VOCAB_SIZE; + + use super::*; + + fn allowed(grammar: &PrefixGrammar) -> Vec { + (0..VOCAB_SIZE as u32) + .filter(|t| grammar.allows(*t)) + .collect() + } + + #[test] + fn follows_the_tree() { + let mut grammar = PrefixGrammar::new(128); + assert_eq!(allowed(&grammar), vec![CONCAT, ALT, CHAR, REPEAT]); + grammar.step(REPEAT); + assert_eq!( + allowed(&grammar), + (COUNT_BASE..COUNT_INF).collect::>() + ); + grammar.step(COUNT_BASE + 3); + assert_eq!( + allowed(&grammar), + (COUNT_BASE + 3..=COUNT_INF).collect::>() + ); + grammar.step(COUNT_INF); + grammar.step(CONCAT); + assert!(grammar.allows(END)); + grammar.step(CHAR); + grammar.step(END); + assert_eq!(allowed(&grammar), vec![EOS]); + } + + #[test] + fn closes_within_max_len() { + // BOS, then the tree, then EOS: 4 positions leave 2 for the tree. + let mut grammar = PrefixGrammar::new(4); + assert!(grammar.allows(CONCAT) && !grammar.allows(REPEAT)); + grammar.step(CONCAT); + assert_eq!(allowed(&grammar), vec![END]); + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/graph.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/graph.rs new file mode 100644 index 0000000..6558fdb --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/graph.rs @@ -0,0 +1,170 @@ +//! The model's input: a canonical DFA as a graph (`config.json`, `inputs`). + +use super::canonical::{CanonicalDfa, DEAD}; + +const NODE_STATE: u32 = 0; +const NODE_START: u32 = 1; +const NODE_SINK: u32 = 2; + +/// The complete DFA as a graph: a sink is appended (last node) when some +/// transition is missing; one node per state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(super) struct Graph { + pub nodes: usize, + /// 0 state, 1 start (node 0), 2 sink. + pub node_type: Vec, + pub is_accept: Vec, + /// Shortest word length from the start, clipped to `max_depth`. + pub fwd_depth: Vec, + /// Shortest word length to an accepting state, clipped to `max_depth`; + /// `max_depth + 1` if none. + pub bwd_depth: Vec, + /// Shortest word length to a rejecting state, clipped to `max_depth`; + /// `max_depth + 1` if none. + pub to_reject: Vec, + /// One edge per state and base, then the same edges reversed. + pub edge_src: Vec, + pub edge_dst: Vec, + /// `base` forward, `max_bases + base` reversed. + pub edge_type: Vec, +} + +impl Graph { + pub(super) fn new(dfa: &CanonicalDfa, max_bases: usize, max_depth: usize) -> Self { + let k = dfa.k; + let sink = dfa.delta.contains(&DEAD); + let n = dfa.n + sink as usize; + // `delta[s * k + b]`, every transition defined. + let delta: Vec = (0..n * k) + .map(|i| match dfa.delta.get(i) { + Some(&t) if t != DEAD => t as usize, + _ => dfa.n, + }) + .collect(); + let accept: Vec = (0..n).map(|s| s < dfa.n && dfa.accept[s]).collect(); + + let mut node_type = vec![NODE_STATE; n]; + node_type[0] = NODE_START; + if sink { + node_type[n - 1] = NODE_SINK; + } + let clip = |d: Vec| -> Vec { + d.into_iter() + .map(|d| if d >= n { max_depth + 1 } else { d.min(max_depth) } as u32) + .collect() + }; + let not_accept: Vec = accept.iter().map(|a| !a).collect(); + + let mut edge_src = Vec::with_capacity(2 * n * k); + let mut edge_dst = Vec::with_capacity(2 * n * k); + let mut edge_type = Vec::with_capacity(2 * n * k); + for (reverse, offset) in [(false, 0), (true, max_bases)] { + for s in 0..n { + for b in 0..k { + let (from, to) = (s as u32, delta[s * k + b] as u32); + let (from, to) = if reverse { (to, from) } else { (from, to) }; + edge_src.push(from); + edge_dst.push(to); + edge_type.push((offset + b) as u32); + } + } + } + + Graph { + nodes: n, + node_type, + is_accept: accept.iter().map(|a| *a as u32).collect(), + // Every state is reachable. + fwd_depth: clip(distance_from_start(&delta, n, k)) + .into_iter() + .map(|d| d.min(max_depth as u32)) + .collect(), + bwd_depth: clip(distance_to(&delta, n, k, &accept)), + to_reject: clip(distance_to(&delta, n, k, ¬_accept)), + edge_src, + edge_dst, + edge_type, + } + } +} + +/// Shortest word length from state 0 to each state; `n` if unreachable. +fn distance_from_start(delta: &[usize], n: usize, k: usize) -> Vec { + let mut d = vec![n; n]; + d[0] = 0; + let mut queue = std::collections::VecDeque::from([0]); + while let Some(s) = queue.pop_front() { + for &t in &delta[s * k..(s + 1) * k] { + if d[t] == n { + d[t] = d[s] + 1; + queue.push_back(t); + } + } + } + d +} + +/// Shortest word length from each state into `targets`; `n` if none. +fn distance_to(delta: &[usize], n: usize, k: usize, targets: &[bool]) -> Vec { + let mut d: Vec = targets.iter().map(|t| if *t { 0 } else { n }).collect(); + loop { + let mut changed = false; + for s in 0..n { + let via = delta[s * k..(s + 1) * k] + .iter() + .map(|t| d[*t] + 1) + .min() + .unwrap_or(n); + if via < d[s] { + d[s] = via; + changed = true; + } + } + if !changed { + return d; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + neural_synthesis::{kleene::canonical::canonicalize, minimal_dfa}, + regex::RegularExpression, + }; + + fn graph(pattern: &str) -> Graph { + let dfa = minimal_dfa( + &RegularExpression::new(pattern) + .unwrap() + .to_automaton() + .unwrap(), + ) + .unwrap(); + Graph::new(&canonicalize(&dfa), 16, 64) + } + + #[test] + fn appends_a_sink_when_incomplete() { + // States: start, after `a`, plus the sink; bases `a` and `b`... `ab`. + let g = graph("ab"); + assert_eq!(g.nodes, 4); + assert_eq!(g.node_type, vec![1, 0, 0, 2]); + assert_eq!(g.is_accept, vec![0, 0, 1, 0]); + assert_eq!(g.fwd_depth, vec![0, 1, 2, 1]); + assert_eq!(g.bwd_depth, vec![2, 1, 0, 65]); + assert_eq!(g.to_reject, vec![0, 0, 1, 0]); + assert_eq!(g.edge_src.len(), 2 * 4 * 3); + assert!(g.edge_type[..12].iter().all(|t| *t < 3)); + assert!(g.edge_type[12..].iter().all(|t| (16..19).contains(t))); + } + + #[test] + fn complete_dfa_has_no_sink() { + let g = graph(".*"); + assert_eq!(g.nodes, 1); + assert_eq!(g.node_type, vec![1]); + assert_eq!(g.to_reject, vec![65]); + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/mod.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/mod.rs new file mode 100644 index 0000000..87f8a75 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/mod.rs @@ -0,0 +1,453 @@ +//! The Kleene models (format `kleene` v1): a graph transformer +//! encoder over the canonical minimal DFA, and a transformer decoder writing +//! the regex in prefix order, constrained to well-formed trees. +//! +//! A port of the PyTorch implementation the models are trained with; the +//! input encoding is documented in each release's `config.json` (`inputs`). + +use serde::Deserialize; + +use super::{Model, NeuralSynthesisError, Sampling}; +use crate::{ + CharRange, + fast_automaton::{FastAutomaton, spanning_set::SpanningSet}, + regex::RegularExpression, +}; + +mod attention; +mod canonical; +mod grammar; +mod graph; +mod network; +mod tokens; + +use network::{ModelConfig, Network}; + +pub(super) const FORMAT: &str = "kleene"; +pub(super) const FORMAT_VERSION: u32 = 1; + +/// `config.json`, the parts used here. +#[derive(Deserialize)] +struct Config { + limits: Limits, + trained_on: TrainedOn, + model_config: ModelConfig, +} + +#[derive(Deserialize)] +struct Limits { + max_bases: usize, +} + +/// The largest DFAs seen in training: larger ones are left to state +/// elimination, the model's quality on them being unknown. +#[derive(Deserialize)] +struct TrainedOn { + max_states: usize, +} + +pub(super) struct Kleene { + network: Network, + max_states: usize, + max_bases: usize, +} + +impl Kleene { + /// Loads a model from its `config.json` and its weights. + pub(super) fn new( + config: &serde_json::Value, + weights: Vec, + device: &candle_core::Device, + ) -> Result { + let config = Config::deserialize(config) + .map_err(|err| NeuralSynthesisError::Config(err.to_string()))?; + let model_config = &config.model_config; + let vocab: std::collections::BTreeMap = tokens::vocab().into_iter().collect(); + if model_config.vocab != vocab { + return Err(NeuralSynthesisError::Config( + "unknown vocabulary".to_string(), + )); + } + if config.limits.max_bases > model_config.max_bases || model_config.max_bases > 64 { + return Err(NeuralSynthesisError::Config(format!( + "max_bases {} (model {}) is not supported", + config.limits.max_bases, model_config.max_bases + ))); + } + // The shapes the weights cannot check, which the network divides by + // or counts down from. + if model_config.n_heads == 0 || model_config.d_model % model_config.n_heads != 0 { + return Err(NeuralSynthesisError::Config(format!( + "d_model {} is not divisible into {} heads", + model_config.d_model, model_config.n_heads + ))); + } + if model_config.max_len < 2 { + return Err(NeuralSynthesisError::Config(format!( + "max_len {} leaves no room between BOS and EOS", + model_config.max_len + ))); + } + let network = Network::new(model_config, weights, device) + .map_err(|err| NeuralSynthesisError::Weights(err.to_string()))?; + Ok(Self { + network, + max_states: config.trained_on.max_states, + max_bases: config.limits.max_bases, + }) + } +} + +impl Model for Kleene { + fn max_states(&self) -> usize { + self.max_states + } + + fn max_bases(&self) -> usize { + self.max_bases + } + + fn candidates( + &self, + dfas: &[&FastAutomaton], + sampling: &Sampling, + ) -> Result>, String> { + let mut inputs = Vec::with_capacity(dfas.len()); + let mut bases = Vec::with_capacity(dfas.len()); + let mut decoded = vec![vec![]; dfas.len()]; + for (i, dfa) in dfas.iter().enumerate() { + if dfa.number_of_states() > self.max_states { + continue; + } + let canonical = canonical::canonicalize(dfa); + if canonical.k == 0 || canonical.k > self.max_bases { + continue; + } + let graph = graph::Graph::new( + &canonical, + self.network.max_bases(), + self.network.max_depth(), + ); + inputs.push((i, graph, canonical.k)); + bases.push(canonical.bases); + } + let graphs: Vec<(&graph::Graph, usize)> = inputs.iter().map(|(_, g, k)| (g, *k)).collect(); + let candidates = self + .network + .generate_batch( + &graphs, + sampling.candidates, + sampling.temperature, + sampling.seed, + ) + .map_err(|err| err.to_string())?; + for (((i, _, _), candidates), bases) in inputs.iter().zip(candidates).zip(&bases) { + decoded[*i] = candidates + .into_iter() + .filter_map(|(tokens, masks)| tokens::decode(&tokens, &masks, bases)) + .collect(); + } + Ok(decoded) + } +} + +#[cfg(test)] +mod tests { + use regex_charclass::CharacterClass; + + use crate::neural_synthesis::minimal_dfa; + + use super::*; + + /// Compares with the outputs of the PyTorch model the weights were trained + /// with (`tests/data/kleene_parity.json`, from the dfa2regex project): the + /// canonical bases, the input graph, the first logits and the greedy + /// decoding of each pattern's minimal DFA. + /// + /// `KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis neural_synthesis -- --ignored` + /// (`KLEENE_PARITY=` for other reference outputs). + #[test] + #[ignore = "needs the model weights (KLEENE_MODEL_DIR)"] + fn parity() { + let dir = std::path::PathBuf::from(std::env::var("KLEENE_MODEL_DIR").unwrap()); + let config: serde_json::Value = + serde_json::from_slice(&std::fs::read(dir.join("config.json")).unwrap()).unwrap(); + let weights = std::fs::read(dir.join("model.safetensors")).unwrap(); + let kleene = Kleene::new(&config, weights, &candle_core::Device::Cpu).unwrap(); + + let path = std::env::var("KLEENE_PARITY").unwrap_or_else(|_| { + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/kleene_parity.json").to_string() + }); + let reference: Vec = + serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap(); + let ints = |v: &serde_json::Value| -> Vec { + v.as_array() + .unwrap() + .iter() + .map(|x| x.as_u64().unwrap()) + .collect() + }; + let floats = |v: &serde_json::Value| -> Vec { + v.as_array() + .unwrap() + .iter() + .map(|x| x.as_f64().unwrap() as f32) + .collect() + }; + let mut max_diff = 0f32; + for case in &reference { + let pattern = case["pattern"].as_str().unwrap(); + let dfa = minimal_dfa( + &RegularExpression::new(pattern) + .unwrap() + .to_automaton() + .unwrap(), + ) + .unwrap(); + let canonical = canonical::canonicalize(&dfa); + let bases: Vec = canonical.bases.iter().map(|b| b.to_regex()).collect(); + let expected: Vec<&str> = case["base_ranges"] + .as_array() + .unwrap() + .iter() + .map(|b| b.as_str().unwrap()) + .collect(); + assert_eq!(bases, expected, "{pattern}: bases"); + + let g = graph::Graph::new(&canonical, 16, 64); + let r = &case["graph"]; + let as_u64 = |v: &[u32]| v.iter().map(|x| *x as u64).collect::>(); + assert_eq!(as_u64(&g.node_type), ints(&r["node_type"]), "{pattern}"); + assert_eq!(as_u64(&g.is_accept), ints(&r["is_accept"]), "{pattern}"); + assert_eq!(as_u64(&g.fwd_depth), ints(&r["fwd_depth"]), "{pattern}"); + assert_eq!(as_u64(&g.bwd_depth), ints(&r["bwd_depth"]), "{pattern}"); + assert_eq!(as_u64(&g.to_reject), ints(&r["to_reject"]), "{pattern}"); + assert_eq!(as_u64(&g.edge_src), ints(&r["edge_src"]), "{pattern}"); + assert_eq!(as_u64(&g.edge_dst), ints(&r["edge_dst"]), "{pattern}"); + assert_eq!(as_u64(&g.edge_type), ints(&r["edge_type"]), "{pattern}"); + + let (tok, mask) = kleene.network.first_logits(&g).unwrap(); + let expected: Vec = floats(&case["first_tok_logits"]) + .into_iter() + .chain(floats(&case["first_mask_logits"])) + .collect(); + for (a, b) in tok.iter().chain(&mask).zip(&expected) { + max_diff = max_diff.max((a - b).abs()); + } + + let greedy = kleene.network.generate(&g, canonical.k, 1, 1.0, 0).unwrap(); + let (tokens, masks) = &greedy[0]; + assert_eq!( + tokens.iter().map(|t| *t as u64).collect::>(), + ints(&case["tokens"]), + "{pattern}: tokens" + ); + assert_eq!(masks, &ints(&case["masks"]), "{pattern}: masks"); + } + println!("{} cases, max logit difference {max_diff}", reference.len()); + assert!(max_diff < 1e-3, "max logit difference {max_diff}"); + } + + /// Writes `tests/data/kleene_tiny`, a model of the format with tiny + /// dimensions and untrained weights, for the tests that need a model to + /// run but not good proposals. Its regexes are at most 6 tokens long: + /// untrained, longer ones nest counted repetitions too costly to check. + /// The weights are drawn from generators seeded by the parameters' names, + /// so writing it again gives the same files. + /// `cargo test --features neural-synthesis kleene::tests::write_tiny_model -- --ignored` + #[test] + #[ignore = "writes the tiny model fixture"] + fn write_tiny_model() { + let config = serde_json::json!({ + "name": "kleene-tiny", + "format": FORMAT, + "format_version": FORMAT_VERSION, + "limits": {"max_bases": 16}, + "trained_on": {"max_states": 16}, + "model_config": { + "vocab": tokens::vocab().into_iter().collect::>(), + "max_bases": 16, + "max_len": 8, + "d_model": 16, + "n_heads": 2, + "enc_layers": 1, + "enc_global_layers": 1, + "dec_layers": 1, + "ffn": 32, + "edge_dim": 4, + "max_depth": 64 + } + }); + let model_config = ModelConfig::deserialize(&config["model_config"]).unwrap(); + let varmap = candle_nn::VarMap::new(); + let vb = candle_nn::VarBuilder::from_varmap( + &varmap, + candle_core::DType::F32, + &candle_core::Device::Cpu, + ); + Network::from_var_builder(&model_config, vb, &candle_core::Device::Cpu).unwrap(); + for (name, var) in varmap.data().lock().unwrap().iter() { + // FNV-1a of the name, then SplitMix64: uniform in [-0.5, 0.5). + let mut state = name.bytes().fold(0xcbf2_9ce4_8422_2325u64, |hash, byte| { + (hash ^ byte as u64).wrapping_mul(0x100_0000_01b3) + }); + let values: Vec = (0..var.elem_count()) + .map(|_| { + state = state.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + ((z ^ (z >> 31)) >> 40) as f32 / (1u64 << 24) as f32 - 0.5 + }) + .collect(); + var.set( + &candle_core::Tensor::from_vec(values, var.shape(), &candle_core::Device::Cpu) + .unwrap(), + ) + .unwrap(); + } + + let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/kleene_tiny"); + std::fs::create_dir_all(dir).unwrap(); + std::fs::write( + format!("{dir}/config.json"), + serde_json::to_string_pretty(&config).unwrap() + "\n", + ) + .unwrap(); + varmap.save(format!("{dir}/model.safetensors")).unwrap(); + } + + #[test] + fn rejects_configs_the_network_cannot_run() { + let config = |n_heads: usize, max_len: usize| { + serde_json::json!({ + "limits": {"max_bases": 16}, + "trained_on": {"max_states": 16}, + "model_config": { + "vocab": tokens::vocab().into_iter().collect::>(), + "max_bases": 16, + "max_len": max_len, + "d_model": 256, + "n_heads": n_heads, + "enc_layers": 4, + "enc_global_layers": 2, + "dec_layers": 4, + "ffn": null, + "edge_dim": 32, + "max_depth": 64 + } + }) + }; + for (n_heads, max_len) in [(0, 128), (3, 128), (8, 1)] { + assert!( + matches!( + Kleene::new(&config(n_heads, max_len), vec![], &candle_core::Device::Cpu), + Err(NeuralSynthesisError::Config(_)) + ), + "n_heads {n_heads}, max_len {max_len}" + ); + } + } + + /// Times [`Network::generate`] over the parity patterns' DFAs, and prints + /// a checksum of the candidates so an optimization can be checked not to + /// change them: + /// `KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis kleene::tests::bench_generate -- --ignored --nocapture` + #[test] + #[ignore = "benchmark; needs the model weights (KLEENE_MODEL_DIR)"] + fn bench_generate() { + use std::{ + hash::{DefaultHasher, Hash, Hasher}, + time::Instant, + }; + + let dir = std::path::PathBuf::from(std::env::var("KLEENE_MODEL_DIR").unwrap()); + let config: serde_json::Value = + serde_json::from_slice(&std::fs::read(dir.join("config.json")).unwrap()).unwrap(); + let weights = std::fs::read(dir.join("model.safetensors")).unwrap(); + let kleene = Kleene::new(&config, weights, &candle_core::Device::Cpu).unwrap(); + + let reference: Vec = serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/kleene_parity.json" + ))) + .unwrap(); + let inputs: Vec<(graph::Graph, usize)> = reference + .iter() + .map(|case| { + let dfa = minimal_dfa( + &RegularExpression::new(case["pattern"].as_str().unwrap()) + .unwrap() + .to_automaton() + .unwrap(), + ) + .unwrap(); + let canonical = canonical::canonicalize(&dfa); + let g = graph::Graph::new( + &canonical, + kleene.network.max_bases(), + kleene.network.max_depth(), + ); + (g, canonical.k) + }) + .collect(); + + const RUNS: usize = 10; + for count in [1, 8] { + let mut checksum = DefaultHasher::new(); + let mut pass = || { + let start = Instant::now(); + for (g, k) in &inputs { + let candidates = kleene.network.generate(g, *k, count, 1.0, 0).unwrap(); + candidates.hash(&mut checksum); + } + start.elapsed() + }; + pass(); // warm-up + let mut times: Vec<_> = (0..RUNS).map(|_| pass()).collect(); + times.sort(); + let per_dfa = |t: std::time::Duration| t / inputs.len() as u32; + println!( + "{count} candidate(s): {:?} per DFA (median of {RUNS}; min {:?}, max {:?}), checksum {:016x}", + per_dfa(times[RUNS / 2]), + per_dfa(times[0]), + per_dfa(times[RUNS - 1]), + checksum.finish() + ); + + // All the DFAs in one batch: the same candidates, up to float + // rounding (counted), in less time. + let one_by_one: Vec<_> = inputs + .iter() + .map(|(g, k)| kleene.network.generate(g, *k, count, 1.0, 0).unwrap()) + .collect(); + let batch: Vec<(&graph::Graph, usize)> = inputs.iter().map(|(g, k)| (g, *k)).collect(); + let batched = kleene + .network + .generate_batch(&batch, count, 1.0, 0) + .unwrap(); + let same = one_by_one + .iter() + .zip(&batched) + .flat_map(|(a, b)| a.iter().zip(b)) + .filter(|(a, b)| a == b) + .count(); + let mut times: Vec<_> = (0..RUNS) + .map(|_| { + let start = Instant::now(); + kleene + .network + .generate_batch(&batch, count, 1.0, 0) + .unwrap(); + start.elapsed() + }) + .collect(); + times.sort(); + println!( + "{count} candidate(s), batched: {:?} per DFA (median of {RUNS}); {same}/{} candidates as one by one", + per_dfa(times[RUNS / 2]), + inputs.len() * count + ); + } + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/network.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/network.rs new file mode 100644 index 0000000..7d8396d --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/network.rs @@ -0,0 +1,932 @@ +//! The Kleene graph-to-sequence network, on candle: a graph transformer +//! encoder over the DFA, a transformer decoder over the regex tokens. A port +//! of the PyTorch model it was trained as; parameter names match its +//! `model.safetensors`. + +use candle_core::{D, DType, Device, Module, Result, Tensor}; +use candle_nn::{ + Embedding, LayerNorm, Linear, VarBuilder, embedding, layer_norm, linear, linear_no_bias, +}; +use serde::Deserialize; + +use super::attention::{AttendOne, attend_one_unfused}; +use super::grammar::PrefixGrammar; +use super::graph::Graph; +use super::tokens::{BOS, CHAR, EOS, VOCAB_SIZE}; + +const LAYER_NORM_EPS: f64 = 1e-5; +/// Most nodes encoded together. +const ENCODER_NODES: usize = 512; +/// Decoder positions the self-attention cache first has room for. +const INITIAL_CAPACITY: usize = 16; +/// Added to the attention denominators of the graph layers, as PyG does. +const SOFTMAX_EPS: f64 = 1e-16; + +/// `model_config` in `config.json`. +#[derive(Clone, Debug, Deserialize)] +pub(super) struct ModelConfig { + pub vocab: std::collections::BTreeMap, + pub max_bases: usize, + /// Decoder positions, BOS and EOS included. + pub max_len: usize, + pub d_model: usize, + pub n_heads: usize, + pub enc_layers: usize, + pub enc_global_layers: usize, + pub dec_layers: usize, + /// Defaults to `4 * d_model`. + pub ffn: Option, + pub edge_dim: usize, + pub max_depth: usize, +} + +/// A multi-head attention with PyTorch's packed input projection. +struct Attention { + /// Queries, keys and values at once. + qkv: Linear, + q: Linear, + /// Keys and values at once. + kv: Linear, + out: Linear, + heads: usize, +} + +impl Attention { + fn new(d: usize, heads: usize, vb: VarBuilder) -> Result { + let w = vb.get((3 * d, d), "in_proj_weight")?; + let b = vb.get(3 * d, "in_proj_bias")?; + let rows = |from: usize, to: usize| -> Result { + Ok(Linear::new( + w.narrow(0, from * d, (to - from) * d)?, + Some(b.narrow(0, from * d, (to - from) * d)?), + )) + }; + Ok(Self { + qkv: rows(0, 3)?, + q: rows(0, 1)?, + kv: rows(1, 3)?, + out: linear(d, d, vb.pp("out_proj"))?, + heads, + }) + } + + /// `[T, d]` → `[heads, T, d / heads]`. + fn split(&self, x: &Tensor) -> Result { + let (t, d) = x.dims2()?; + x.reshape((t, self.heads, d / self.heads))? + .transpose(0, 1)? + .contiguous() + } + + /// Self-attention over `x` `[N, d]`, the pairs where `mask` `[N, N]` is + /// -∞ left out. + fn self_attend(&self, x: &Tensor, mask: Option<&Tensor>) -> Result { + let (n, d) = x.dims2()?; + let qkv = self.qkv.forward(x)?; + let [q, k, v] = [0, 1, 2].map(|i| qkv.narrow(1, i * d, d).and_then(|x| self.split(&x))); + let (q, k, v) = (q?, k?, v?); + let mut scores = (q.matmul(&k.t()?)? / ((d / self.heads) as f64).sqrt())?; + if let Some(mask) = mask { + scores = scores.broadcast_add(mask)?; + } + let attended = candle_nn::ops::softmax_last_dim(&scores)?.matmul(&v)?; + self.out + .forward(&attended.transpose(0, 1)?.reshape((n, d))?) + } + + /// One query per row: `q` `[B, d]` over `keys` and `values` `[B or 1, + /// heads, T, d / heads]`, each row attending to its first + /// `key_lengths[row]` keys, all without them. `key_mask` `[B, 1, T]`, -∞ + /// past each row's keys, says the same: the CPU's [`AttendOne`] reads the + /// lengths, the other devices' tensor operations the mask. Returns + /// `[B, d]` after the output projection. + fn attend_one( + &self, + q: &Tensor, + keys: &Tensor, + values: &Tensor, + key_lengths: Option<&[u32]>, + key_mask: Option<&Tensor>, + ) -> Result { + let attended = if q.device().is_cpu() { + q.apply_op3_no_bwd( + keys, + values, + &AttendOne { + heads: self.heads, + lengths: key_lengths, + }, + )? + } else { + attend_one_unfused(q, keys, values, key_mask, self.heads)? + }; + self.out.forward(&attended) + } +} + +/// `linear2(gelu(linear1(x)))`. +struct FeedForward { + up: Linear, + down: Linear, +} + +impl FeedForward { + fn new(d: usize, ffn: usize, up: VarBuilder, down: VarBuilder) -> Result { + Ok(Self { + up: linear(d, ffn, up)?, + down: linear(ffn, d, down)?, + }) + } + + fn forward(&self, x: &Tensor) -> Result { + self.down.forward(&self.up.forward(x)?.gelu_erf()?) + } +} + +/// The edges of graphs, laid out for [`TransformerConv`]. +struct Edges { + src: Tensor, + dst: Tensor, + /// `dst` on the host, for the softmax's per-node maxima. + dst_nodes: Vec, + /// `[E, edge_dim]` edge-type embeddings. + embedded: Tensor, +} + +impl Edges { + /// For each edge, the largest of `scores` `[E, H]` over the edges into + /// the same node, per head. + fn incoming_max(&self, scores: &Tensor, nodes: usize) -> Result { + let (e, h) = scores.dims2()?; + let mut max = vec![f32::NEG_INFINITY; nodes * h]; + for (row, node) in scores.to_vec2::()?.iter().zip(&self.dst_nodes) { + let node = *node as usize; + for (max, score) in max[node * h..(node + 1) * h].iter_mut().zip(row) { + *max = max.max(*score); + } + } + let per_edge: Vec = self + .dst_nodes + .iter() + .flat_map(|node| { + max[*node as usize * h..(*node as usize + 1) * h] + .iter() + .copied() + }) + .collect(); + Tensor::from_vec(per_edge, (e, h), scores.device()) + } +} + +/// PyG's `TransformerConv` (`concat`, `beta`, `root_weight`, edge features), +/// aggregating over the edges into each node with scatter-adds. +struct TransformerConv { + key: Linear, + query: Linear, + value: Linear, + edge: Linear, + skip: Linear, + beta: Linear, + heads: usize, +} + +impl TransformerConv { + fn new(d: usize, heads: usize, edge_dim: usize, vb: VarBuilder) -> Result { + Ok(Self { + key: linear(d, d, vb.pp("lin_key"))?, + query: linear(d, d, vb.pp("lin_query"))?, + value: linear(d, d, vb.pp("lin_value"))?, + edge: linear_no_bias(edge_dim, d, vb.pp("lin_edge"))?, + skip: linear(d, d, vb.pp("lin_skip"))?, + beta: linear_no_bias(3 * d, 1, vb.pp("lin_beta"))?, + heads, + }) + } + + fn forward(&self, x: &Tensor, edges: &Edges) -> Result { + let (n, d) = x.dims2()?; + let (h, c) = (self.heads, d / self.heads); + let x_r = self.skip.forward(x)?; + let e = edges.src.dim(0)?; + let out = if e == 0 { + Tensor::zeros((n, d), x.dtype(), x.device())? + } else { + let edge = self.edge.forward(&edges.embedded)?; + let query = self.query.forward(x)?.index_select(&edges.dst, 0)?; + let key = (self.key.forward(x)?.index_select(&edges.src, 0)? + &edge)?; + let value = (self.value.forward(x)?.index_select(&edges.src, 0)? + &edge)?; + // [E, H] + let alpha = ((query * key)?.reshape((e, h, c))?.sum(D::Minus1)? / (c as f64).sqrt())?; + // Softmax over the edges into each node. + let max = edges.incoming_max(&alpha, n)?; + let exp = (alpha - max)?.exp()?; + let sum = + Tensor::zeros((n, h), exp.dtype(), exp.device())?.index_add(&edges.dst, &exp, 0)?; + let sum = (sum + SOFTMAX_EPS)?.index_select(&edges.dst, 0)?; + let alpha = (exp / sum)?; + let messages = value + .reshape((e, h, c))? + .broadcast_mul(&alpha.unsqueeze(D::Minus1)?)? + .reshape((e, d))?; + Tensor::zeros((n, d), messages.dtype(), messages.device())? + .index_add(&edges.dst, &messages, 0)? + }; + let beta = candle_nn::ops::sigmoid( + &self + .beta + .forward(&Tensor::cat(&[&out, &x_r, &(&out - &x_r)?], D::Minus1)?)?, + )?; + x_r.broadcast_mul(&beta)? + out.broadcast_mul(&(1.0 - beta)?)? + } +} + +/// A pre-norm graph layer: `TransformerConv`, then a feed-forward. +struct ConvBlock { + norm1: LayerNorm, + conv: TransformerConv, + norm2: LayerNorm, + ffn: FeedForward, +} + +impl ConvBlock { + fn forward(&self, x: &Tensor, edges: &Edges) -> Result { + let x = (x + self.conv.forward(&self.norm1.forward(x)?, edges)?)?; + &x + self.ffn.forward(&self.norm2.forward(&x)?)? + } +} + +/// A pre-norm `nn.TransformerEncoderLayer`. +struct EncoderLayer { + norm1: LayerNorm, + attn: Attention, + norm2: LayerNorm, + ffn: FeedForward, +} + +impl EncoderLayer { + fn forward(&self, x: &Tensor, mask: Option<&Tensor>) -> Result { + let x = (x + self.attn.self_attend(&self.norm1.forward(x)?, mask)?)?; + &x + self.ffn.forward(&self.norm2.forward(&x)?)? + } +} + +struct Encoder { + type_emb: Embedding, + accept_emb: Embedding, + fwd_depth_emb: Embedding, + to_accept_emb: Embedding, + to_reject_emb: Embedding, + edge_emb: Embedding, + layers: Vec, + global_layers: Vec, + out_norm: LayerNorm, +} + +impl Encoder { + fn new(cfg: &ModelConfig, vb: VarBuilder) -> Result { + let (d, depth) = (cfg.d_model, cfg.max_depth); + let ffn = cfg.ffn.unwrap_or(4 * d); + let layers = (0..cfg.enc_layers) + .map(|i| { + let vb = vb.pp("layers").pp(i); + Ok(ConvBlock { + norm1: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm1"))?, + conv: TransformerConv::new(d, cfg.n_heads, cfg.edge_dim, vb.pp("conv"))?, + norm2: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm2"))?, + ffn: FeedForward::new(d, ffn, vb.pp("ffn.0"), vb.pp("ffn.3"))?, + }) + }) + .collect::>()?; + let global_layers = (0..cfg.enc_global_layers) + .map(|i| { + let vb = vb.pp("global_layers.layers").pp(i); + Ok(EncoderLayer { + norm1: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm1"))?, + attn: Attention::new(d, cfg.n_heads, vb.pp("self_attn"))?, + norm2: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm2"))?, + ffn: FeedForward::new(d, ffn, vb.pp("linear1"), vb.pp("linear2"))?, + }) + }) + .collect::>()?; + Ok(Self { + type_emb: embedding(3, d, vb.pp("type_emb"))?, + accept_emb: embedding(2, d, vb.pp("accept_emb"))?, + fwd_depth_emb: embedding(depth + 1, d, vb.pp("fwd_depth_emb"))?, + to_accept_emb: embedding(depth + 2, d, vb.pp("to_accept_emb"))?, + to_reject_emb: embedding(depth + 2, d, vb.pp("to_reject_emb"))?, + edge_emb: embedding(2 * cfg.max_bases, cfg.edge_dim, vb.pp("edge_emb"))?, + layers, + global_layers, + out_norm: layer_norm(d, LAYER_NORM_EPS, vb.pp("out_norm"))?, + }) + } + + /// Node embeddings of `graphs`, one after the other: `[ΣN, d]`. The + /// graphs are encoded together, as one graph whose nodes attend only + /// within their own. + fn forward(&self, graphs: &[&Graph], device: &Device) -> Result { + let ids = |v: Vec| { + let len = v.len(); + Tensor::from_vec(v, len, device) + }; + let concat = |field: fn(&Graph) -> &Vec| -> Vec { + graphs + .iter() + .flat_map(|graph| field(graph).iter().copied()) + .collect() + }; + let mut h = (self.type_emb.forward(&ids(concat(|g| &g.node_type))?)? + + self.accept_emb.forward(&ids(concat(|g| &g.is_accept))?)?)?; + h = (h + self + .fwd_depth_emb + .forward(&ids(concat(|g| &g.fwd_depth))?)?)?; + h = (h + self + .to_accept_emb + .forward(&ids(concat(|g| &g.bwd_depth))?)?)?; + h = (h + self + .to_reject_emb + .forward(&ids(concat(|g| &g.to_reject))?)?)?; + + // Each graph's edges, renumbered to its place among the nodes. + let mut offset = 0; + let (mut src, mut dst) = (vec![], vec![]); + for graph in graphs { + src.extend(graph.edge_src.iter().map(|node| node + offset)); + dst.extend(graph.edge_dst.iter().map(|node| node + offset)); + offset += graph.nodes as u32; + } + let edges = Edges { + src: ids(src)?, + dst: ids(dst.clone())?, + dst_nodes: dst, + embedded: self.edge_emb.forward(&ids(concat(|g| &g.edge_type))?)?, + }; + for layer in &self.layers { + h = layer.forward(&h, &edges)?; + } + + let mask = if graphs.len() > 1 { + let n = offset as usize; + let mut mask = vec![f32::NEG_INFINITY; n * n]; + let mut start = 0; + for graph in graphs { + for row in start..start + graph.nodes { + mask[row * n + start..row * n + start + graph.nodes].fill(0.0); + } + start += graph.nodes; + } + Some(Tensor::from_vec(mask, (n, n), device)?) + } else { + None + }; + for layer in &self.global_layers { + h = layer.forward(&h, mask.as_ref())?; + } + self.out_norm.forward(&h) + } +} + +/// A pre-norm `nn.TransformerDecoderLayer`. +struct DecoderLayer { + norm1: LayerNorm, + self_attn: Attention, + norm2: LayerNorm, + cross_attn: Attention, + norm3: LayerNorm, + ffn: FeedForward, +} + +struct Decoder { + tok_emb: Embedding, + mask_emb: Linear, + pos_emb: Tensor, + start_proj: Linear, + layers: Vec, + norm: LayerNorm, + tok_head: Linear, + mask_head: Linear, +} + +/// The state of an incremental decoding of `B` sequences, over one DFA or +/// several. +struct Cache { + /// `[1 or B, d]`, added at position 0. + start: Tensor, + /// Per layer: memory keys and values, `[1, heads, N, d / heads]` for one + /// DFA, `[B, heads, N, d / heads]` (padded to the largest DFA) for + /// several. + cross: Vec<(Tensor, Tensor)>, + /// For several DFAs: each row's number of nodes, and `[B, 1, N]`, -∞ on + /// the padding nodes. + key_lengths: Option>, + key_mask: Option, + /// Per layer: self-attention keys and values of the positions so far, + /// in buffers with room for more, `[B, heads, capacity, d / heads]`. + past: Vec>, + /// Positions fed so far. + t: usize, +} + +impl Cache { + /// Writes layer `layer`'s self-attention keys and values of position + /// `self.t`, `[B, heads, 1, d / heads]`, in place (doubling the buffers + /// when full), and returns those of the positions so far. + fn append(&mut self, layer: usize, key: &Tensor, value: &Tensor) -> Result<(Tensor, Tensor)> { + let t = self.t; + let (keys, values) = match self.past[layer].take() { + None => { + let (rows, heads, _, hd) = key.dims4()?; + let buffer = || { + Tensor::zeros( + (rows, heads, INITIAL_CAPACITY, hd), + key.dtype(), + key.device(), + ) + }; + (buffer()?, buffer()?) + } + Some((keys, values)) if keys.dim(2)? == t => ( + keys.pad_with_zeros(2, 0, t)?, + values.pad_with_zeros(2, 0, t)?, + ), + Some(past) => past, + }; + keys.slice_set(&key.contiguous()?, 2, t)?; + values.slice_set(&value.contiguous()?, 2, t)?; + let so_far = (keys.narrow(2, 0, t + 1)?, values.narrow(2, 0, t + 1)?); + self.past[layer] = Some((keys, values)); + Ok(so_far) + } + + /// Keeps only the sequences `keep` (indices into the current rows). + fn retain(&mut self, keep: &[u32]) -> Result<()> { + if let Some(key_lengths) = &mut self.key_lengths { + *key_lengths = keep.iter().map(|row| key_lengths[*row as usize]).collect(); + } + let keep = &Tensor::new(keep, self.start.device())?; + for past in self.past.iter_mut().flatten() { + *past = (past.0.index_select(keep, 0)?, past.1.index_select(keep, 0)?); + } + if let Some(key_mask) = &self.key_mask { + self.key_mask = Some(key_mask.index_select(keep, 0)?); + for cross in &mut self.cross { + *cross = ( + cross.0.index_select(keep, 0)?, + cross.1.index_select(keep, 0)?, + ); + } + } + Ok(()) + } +} + +impl Decoder { + fn new(cfg: &ModelConfig, vb: VarBuilder) -> Result { + let d = cfg.d_model; + let ffn = cfg.ffn.unwrap_or(4 * d); + let layers = (0..cfg.dec_layers) + .map(|i| { + let vb = vb.pp("layers.layers").pp(i); + Ok(DecoderLayer { + norm1: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm1"))?, + self_attn: Attention::new(d, cfg.n_heads, vb.pp("self_attn"))?, + norm2: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm2"))?, + cross_attn: Attention::new(d, cfg.n_heads, vb.pp("multihead_attn"))?, + norm3: layer_norm(d, LAYER_NORM_EPS, vb.pp("norm3"))?, + ffn: FeedForward::new(d, ffn, vb.pp("linear1"), vb.pp("linear2"))?, + }) + }) + .collect::>()?; + Ok(Self { + tok_emb: embedding(VOCAB_SIZE, d, vb.pp("tok_emb"))?, + mask_emb: linear_no_bias(cfg.max_bases, d, vb.pp("mask_emb"))?, + pos_emb: vb.get((cfg.max_len, d), "pos_emb.weight")?, + start_proj: linear(d, d, vb.pp("start_proj"))?, + layers, + norm: layer_norm(d, LAYER_NORM_EPS, vb.pp("layers.norm"))?, + tok_head: linear(d, VOCAB_SIZE, vb.pp("tok_head"))?, + mask_head: linear(d, cfg.max_bases, vb.pp("mask_head"))?, + }) + } + + /// Prepares the decoding of `rows` sequences per DFA over `memory` + /// `[ΣN, d]`, the node embeddings of DFAs of `nodes` nodes one after the + /// other; each DFA's start state is its first node. + fn start_cache(&self, memory: &Tensor, nodes: &[usize], rows: usize) -> Result { + let d = memory.dim(1)?; + let device = memory.device(); + let dfas = nodes.len(); + let firsts: Vec = nodes + .iter() + .scan(0, |first, n| { + let this = *first; + *first += *n as u32; + Some(this) + }) + .collect(); + let start = self + .start_proj + .forward(&memory.index_select(&Tensor::new(firsts.as_slice(), device)?, 0)?)?; + if dfas == 1 { + let cross = self + .layers + .iter() + .map(|layer| { + let attn = &layer.cross_attn; + let kv = attn.kv.forward(memory)?; + Ok(( + attn.split(&kv.narrow(1, 0, d)?)?.unsqueeze(0)?, + attn.split(&kv.narrow(1, d, d)?)?.unsqueeze(0)?, + )) + }) + .collect::>()?; + return Ok(Cache { + start, + cross, + key_lengths: None, + key_mask: None, + past: vec![None; self.layers.len()], + t: 0, + }); + } + + // Each DFA's nodes in `longest` slots, the padding at a zero row + // appended after the nodes. + let longest = nodes.iter().copied().max().unwrap_or(0); + let padding = firsts[dfas - 1] + nodes[dfas - 1] as u32; + let mut slots = Vec::with_capacity(dfas * longest); + let mut key_mask = Vec::with_capacity(dfas * longest); + for (first, n) in firsts.iter().zip(nodes) { + for slot in 0..longest { + let real = slot < *n; + slots.push(if real { first + slot as u32 } else { padding }); + key_mask.push(if real { 0.0 } else { f32::NEG_INFINITY }); + } + } + let slots = Tensor::from_vec(slots, dfas * longest, device)?; + // Row `r` decodes DFA `r / rows`. + let owner: Vec = (0..dfas * rows).map(|r| (r / rows) as u32).collect(); + let owner = Tensor::from_vec(owner, dfas * rows, device)?; + let cross = self + .layers + .iter() + .map(|layer| { + let attn = &layer.cross_attn; + let (heads, hd) = (attn.heads, d / attn.heads); + let kv = attn + .kv + .forward(memory)? + .pad_with_zeros(0, 0, 1)? + .index_select(&slots, 0)? + .reshape((dfas, longest, 2 * d))?; + // `[DFAs, longest, d]` → `[B, heads, longest, d / heads]`. + let per_row = |part: usize| -> Result { + kv.narrow(2, part * d, d)? + .reshape((dfas, longest, heads, hd))? + .transpose(1, 2)? + .contiguous()? + .index_select(&owner, 0) + }; + Ok((per_row(0)?, per_row(1)?)) + }) + .collect::>()?; + let key_mask = Tensor::from_vec(key_mask, (dfas, 1, longest), device)?; + let key_lengths = (0..dfas * rows).map(|r| nodes[r / rows] as u32).collect(); + Ok(Cache { + start: start.index_select(&owner, 0)?, + cross, + key_lengths: Some(key_lengths), + key_mask: Some(key_mask.index_select(&owner, 0)?), + past: vec![None; self.layers.len()], + t: 0, + }) + } + + /// Feeds position `cache.t`: `tokens` `[B]` and their base masks `[B, + /// max_bases]`. Returns the token logits `[B, vocab]` and base-mask + /// logits `[B, max_bases]` of the next position. + fn step(&self, cache: &mut Cache, tokens: &Tensor, masks: &Tensor) -> Result<(Tensor, Tensor)> { + let mut x = (self.tok_emb.forward(tokens)? + self.mask_emb.forward(masks)?)? + .broadcast_add(&self.pos_emb.narrow(0, cache.t, 1)?)?; + if cache.t == 0 { + x = x.broadcast_add(&cache.start)?; + } + let (b, d) = x.dims2()?; + for (i, layer) in self.layers.iter().enumerate() { + let attn = &layer.self_attn; + let (heads, hd) = (attn.heads, d / attn.heads); + let qkv = attn.qkv.forward(&layer.norm1.forward(&x)?)?; + let head = |part: usize| qkv.narrow(1, part * d, d)?.reshape((b, heads, 1, hd)); + let (k, v) = cache.append(i, &head(1)?, &head(2)?)?; + x = (x + attn.attend_one(&qkv.narrow(1, 0, d)?, &k, &v, None, None)?)?; + + let attn = &layer.cross_attn; + let q = attn.q.forward(&layer.norm2.forward(&x)?)?; + let (k, v) = &cache.cross[i]; + x = (&x + + attn.attend_one( + &q, + k, + v, + cache.key_lengths.as_deref(), + cache.key_mask.as_ref(), + )?)?; + + x = (&x + layer.ffn.forward(&layer.norm3.forward(&x)?)?)?; + } + let x = self.norm.forward(&x)?; + cache.t += 1; + Ok((self.tok_head.forward(&x)?, self.mask_head.forward(&x)?)) + } +} + +/// A small deterministic generator (SplitMix64) for sampling candidates. +struct Rng(u64); + +impl Rng { + /// The generator of candidate `index` for `seed`: each candidate has its + /// own, so a candidate does not depend on what is decoded with it. + fn for_candidate(seed: u64, index: usize) -> Self { + let mut rng = Self(seed ^ (index as u64).wrapping_mul(0xd1b5_4a32_d192_ed03)); + rng.next_u64(); + rng + } + + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + z ^ (z >> 31) + } + + /// Uniform in `[0, 1)`. + fn next_f64(&mut self) -> f64 { + (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 + } +} + +/// The network, ready for inference. +pub(super) struct Network { + encoder: Encoder, + decoder: Decoder, + max_bases: usize, + max_len: usize, + max_depth: usize, + device: Device, +} + +/// A decoded candidate: tokens between BOS and EOS, and one base mask per +/// token (bit `i` = base `i`). +pub(super) type Candidate = (Vec, Vec); + +impl Network { + pub(super) fn new(cfg: &ModelConfig, weights: Vec, device: &Device) -> Result { + let vb = VarBuilder::from_buffered_safetensors(weights, DType::F32, device)?; + Self::from_var_builder(cfg, vb, device) + } + + /// The network whose parameters `vb` provides. + pub(super) fn from_var_builder( + cfg: &ModelConfig, + vb: VarBuilder, + device: &Device, + ) -> Result { + Ok(Self { + encoder: Encoder::new(cfg, vb.pp("encoder"))?, + decoder: Decoder::new(cfg, vb.pp("decoder"))?, + max_bases: cfg.max_bases, + max_len: cfg.max_len, + max_depth: cfg.max_depth, + device: device.clone(), + }) + } + + /// Edge types are `base` and `max_bases + base`. + pub(super) fn max_bases(&self) -> usize { + self.max_bases + } + + /// Distances in the input graph are clipped to this. + pub(super) fn max_depth(&self) -> usize { + self.max_depth + } + + /// Node embeddings of `graphs`, one after the other: `[ΣN, d]`. The + /// graphs are encoded in groups of at most [`ENCODER_NODES`] nodes, which + /// bounds the global attention's `[N, N]` scores. + fn encode<'g>(&self, graphs: impl Iterator) -> Result { + let mut encoded = vec![]; + let mut group: Vec<&Graph> = vec![]; + let mut group_nodes = 0; + for graph in graphs { + if !group.is_empty() && group_nodes + graph.nodes > ENCODER_NODES { + encoded.push(self.encoder.forward(&group, &self.device)?); + group.clear(); + group_nodes = 0; + } + group.push(graph); + group_nodes += graph.nodes; + } + encoded.push(self.encoder.forward(&group, &self.device)?); + Tensor::cat(&encoded, 0) + } + + /// The logits after BOS, for comparison with the PyTorch model. + #[cfg(test)] + pub(super) fn first_logits(&self, graph: &Graph) -> Result<(Vec, Vec)> { + let memory = self.encoder.forward(&[graph], &self.device)?; + let mut cache = self.decoder.start_cache(&memory, &[graph.nodes], 1)?; + let (tok, mask) = self.decoder.step( + &mut cache, + &Tensor::new(&[BOS], &self.device)?, + &Tensor::zeros((1, self.max_bases), DType::F32, &self.device)?, + )?; + Ok((tok.squeeze(0)?.to_vec1()?, mask.squeeze(0)?.to_vec1()?)) + } + + /// `count` candidates for `graph`, a DFA with `bases` bases: see + /// [`generate_batch`](Self::generate_batch). + #[cfg(test)] + pub(super) fn generate( + &self, + graph: &Graph, + bases: usize, + count: usize, + temperature: f64, + seed: u64, + ) -> Result> { + Ok(self + .generate_batch(&[(graph, bases)], count, temperature, seed)? + .pop() + .unwrap()) + } + + /// `count` candidates for each of `inputs`, DFAs as graphs with their + /// number of bases, decoded together. For each DFA, the first candidate + /// is the greedy decoding, the others are sampled at `temperature` + /// (greedy too when it is not positive; tokens from the softmax, each + /// base-mask bit from its sigmoid), each from its own generator seeded + /// from `seed` and its index, so a DFA's candidates do not depend on the + /// other DFAs. Only well-formed trees within `max_len` are produced, + /// masks only use the DFA's bases, and the empty class `[]` appears only + /// as the whole regex. Candidates may repeat. + pub(super) fn generate_batch( + &self, + inputs: &[(&Graph, usize)], + count: usize, + temperature: f64, + seed: u64, + ) -> Result>> { + if inputs.is_empty() || count == 0 { + return Ok(vec![vec![]; inputs.len()]); + } + let nb = self.max_bases; + let memory = self.encode(inputs.iter().map(|(graph, _)| *graph))?; + let nodes: Vec = inputs.iter().map(|(graph, _)| graph.nodes).collect(); + let mut cache = self.decoder.start_cache(&memory, &nodes, count)?; + let present: Vec = inputs + .iter() + .map(|(_, bases)| { + if *bases >= 64 { + u64::MAX + } else { + (1 << bases) - 1 + } + }) + .collect(); + + let rows = inputs.len() * count; + let mut grammars = vec![PrefixGrammar::new(self.max_len); rows]; + let mut rngs: Vec = (0..rows) + .map(|r| Rng::for_candidate(seed, r % count)) + .collect(); + let mut tokens: Vec> = vec![vec![BOS]; rows]; + let mut masks: Vec> = vec![vec![0]; rows]; + // The rows still decoding, in the order of the cache's rows. + let mut active: Vec = (0..rows).collect(); + for _ in 0..self.max_len - 1 { + let last_tokens: Vec = + active.iter().map(|r| *tokens[*r].last().unwrap()).collect(); + let last_masks: Vec = active + .iter() + .flat_map(|r| { + let m = *masks[*r].last().unwrap(); + (0..nb).map(move |i| (m >> i & 1) as f32) + }) + .collect(); + let (tok_logits, mask_logits) = self.decoder.step( + &mut cache, + &Tensor::from_vec(last_tokens, active.len(), &self.device)?, + &Tensor::from_vec(last_masks, (active.len(), nb), &self.device)?, + )?; + let tok_logits = tok_logits.to_vec2::()?; + let mask_logits = mask_logits.to_vec2::()?; + + for (i, &row) in active.iter().enumerate() { + let (input, index) = (row / count, row % count); + let bases = inputs[input].1; + let greedy = index == 0 || temperature.is_nan() || temperature <= 0.0; + let rng = &mut rngs[row]; + let logits: Vec = tok_logits[i] + .iter() + .enumerate() + .map(|(t, l)| { + if grammars[row].allows(t as u32) { + *l as f64 + } else { + f64::NEG_INFINITY + } + }) + .collect(); + let next = if greedy { + argmax(&logits) + } else { + sample(&logits, temperature, rng) + } as u32; + + let ml = &mask_logits[i]; + let mut mask = 0u64; + if next == CHAR { + for (i, l) in ml.iter().enumerate() { + let set = if greedy { + *l > 0.0 + } else { + rng.next_f64() < sigmoid(*l as f64 / temperature) + }; + mask |= (set as u64) << i; + } + mask &= present[input]; + // No empty class [] except as the whole regex (the empty + // language): an empty mask gets its most likely base. + if mask == 0 && tokens[row].len() > 1 { + let likely = (0..nb.min(bases)) + .max_by(|a, b| ml[*a].total_cmp(&ml[*b]).then(b.cmp(a))) + .unwrap_or(0); + mask = 1 << likely; + } + } + + grammars[row].step(next); + tokens[row].push(next); + masks[row].push(mask); + } + + let keep: Vec = (0..active.len() as u32) + .filter(|i| *tokens[active[*i as usize]].last().unwrap() != EOS) + .collect(); + if keep.is_empty() { + break; + } + if keep.len() < active.len() { + active = keep.iter().map(|i| active[*i as usize]).collect(); + cache.retain(&keep)?; + } + } + + let mut candidates: Vec> = vec![Vec::with_capacity(count); inputs.len()]; + for (row, (tokens, masks)) in tokens.into_iter().zip(masks).enumerate() { + let end = tokens + .iter() + .position(|t| *t == EOS) + .unwrap_or(tokens.len()); + candidates[row / count].push((tokens[1..end].to_vec(), masks[1..end].to_vec())); + } + Ok(candidates) + } +} + +fn sigmoid(x: f64) -> f64 { + 1.0 / (1.0 + (-x).exp()) +} + +/// Index of the largest value, the first on ties. +fn argmax(values: &[f64]) -> usize { + let mut best = 0; + for (i, v) in values.iter().enumerate() { + if *v > values[best] { + best = i; + } + } + best +} + +/// Samples an index from the softmax of `logits / temperature`. +fn sample(logits: &[f64], temperature: f64, rng: &mut Rng) -> usize { + let max = logits.iter().copied().fold(f64::NEG_INFINITY, f64::max); + let weights: Vec = logits + .iter() + .map(|l| ((l - max) / temperature).exp()) + .collect(); + let mut target = rng.next_f64() * weights.iter().sum::(); + for (i, w) in weights.iter().enumerate() { + if *w > 0.0 { + if target < *w { + return i; + } + target -= w; + } + } + // Rounding left `target` past the last weight. + weights.iter().rposition(|w| *w > 0.0).unwrap_or(0) +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/tokens.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/tokens.rs new file mode 100644 index 0000000..afe3dee --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/kleene/tokens.rs @@ -0,0 +1,163 @@ +//! The model's output vocabulary, and the regex its tokens describe. +//! +//! A regex is written in prefix order: +//! +//! ```text +//! node := CHAR | REPEAT min max node | (CONCAT | ALT) node* END +//! ``` +//! +//! Each `CHAR` carries a mask over the DFA's bases (bit `i` = base `i`). + +use super::*; + +pub(super) const PAD: u32 = 0; +pub(super) const BOS: u32 = 1; +pub(super) const EOS: u32 = 2; +pub(super) const CONCAT: u32 = 3; +pub(super) const ALT: u32 = 4; +pub(super) const END: u32 = 5; +pub(super) const CHAR: u32 = 6; +pub(super) const REPEAT: u32 = 7; +pub(super) const MAX_COUNT: u32 = 16; +pub(super) const COUNT_BASE: u32 = 8; +pub(super) const COUNT_INF: u32 = COUNT_BASE + MAX_COUNT + 1; +pub(super) const VOCAB_SIZE: usize = COUNT_INF as usize + 1; + +/// The token names in id order, as `config.json` lists them. +pub(super) fn vocab() -> Vec<(String, u32)> { + let mut v: Vec<(String, u32)> = [ + ("PAD", PAD), + ("BOS", BOS), + ("EOS", EOS), + ("CONCAT", CONCAT), + ("ALT", ALT), + ("END", END), + ("CHAR", CHAR), + ("REPEAT", REPEAT), + ] + .into_iter() + .map(|(name, id)| (name.to_string(), id)) + .collect(); + v.extend((0..=MAX_COUNT).map(|n| (format!("COUNT_{n}"), COUNT_BASE + n))); + v.push(("COUNT_INF".to_string(), COUNT_INF)); + v +} + +/// Rebuilds the regex of prefix-order `tokens` (BOS and EOS excluded), bit +/// `i` of a `CHAR`'s mask denoting `bases[i]`. Returns `None` if the tokens do +/// not form exactly one tree. +pub(super) fn decode( + tokens: &[u32], + masks: &[u64], + bases: &[CharRange], +) -> Option { + fn count(tokens: &[u32], pos: &mut usize) -> Option> { + let token = *tokens.get(*pos)?; + *pos += 1; + match token { + COUNT_INF => Some(None), + t if (COUNT_BASE..COUNT_INF).contains(&t) => Some(Some(t - COUNT_BASE)), + _ => None, + } + } + + fn node( + tokens: &[u32], + masks: &[u64], + bases: &[CharRange], + pos: &mut usize, + ) -> Option { + let at = *pos; + let token = *tokens.get(at)?; + *pos += 1; + match token { + CHAR => { + let range = bases + .iter() + .enumerate() + .filter(|(i, _)| masks[at] >> i & 1 == 1) + .fold(CharRange::empty(), |range, (_, base)| range.union(base)); + Some(RegularExpression::Character(range)) + } + REPEAT => { + let min = count(tokens, pos)??; + let max = count(tokens, pos)?; + if max.is_some_and(|max| max < min) { + return None; + } + Some(node(tokens, masks, bases, pos)?.repeat(min, max)) + } + CONCAT | ALT => { + let mut children = Vec::new(); + while *tokens.get(*pos)? != END { + children.push(node(tokens, masks, bases, pos)?); + } + *pos += 1; + Some(if token == CONCAT { + RegularExpression::concat_all(&children) + } else { + RegularExpression::union_all(&children) + }) + } + _ => None, + } + } + + if tokens.len() != masks.len() { + return None; + } + let mut pos = 0; + let regex = node(tokens, masks, bases, &mut pos)?; + (pos == tokens.len()).then_some(regex) +} + +#[cfg(test)] +mod tests { + use regex_charclass::char::Char; + + use super::*; + + fn base(c: char) -> CharRange { + CharRange::new_from_range(Char::new(c)..=Char::new(c)) + } + + #[test] + fn vocab_is_dense() { + let vocab = vocab(); + assert_eq!(vocab.len(), VOCAB_SIZE); + assert!(vocab.iter().enumerate().all(|(i, (_, id))| *id == i as u32)); + } + + #[test] + fn decodes_prefix_order() { + let bases = [base('a'), base('b'), base('c')]; + // (a|b)*c + let tokens = [ + CONCAT, REPEAT, COUNT_BASE, COUNT_INF, ALT, CHAR, CHAR, END, CHAR, END, + ]; + let masks = [0, 0, 0, 0, 0, 0b001, 0b010, 0, 0b100, 0]; + let regex = decode(&tokens, &masks, &bases).unwrap(); + assert_eq!(regex.to_string(), "[ab]*c"); + + // a{2,5} + let tokens = [REPEAT, COUNT_BASE + 2, COUNT_BASE + 5, CHAR]; + let regex = decode(&tokens, &[0, 0, 0, 0b001], &bases).unwrap(); + assert_eq!(regex.to_string(), "a{2,5}"); + } + + #[test] + fn rejects_malformed_sequences() { + let bases = [base('a')]; + for tokens in [ + &[CONCAT, CHAR][..], + &[CHAR, CHAR], + &[REPEAT, COUNT_INF, COUNT_INF, CHAR], + &[REPEAT, COUNT_BASE + 3, COUNT_BASE + 2, CHAR], + &[END], + &[], + ] { + let masks = vec![1; tokens.len()]; + assert!(decode(tokens, &masks, &bases).is_none(), "{tokens:?}"); + } + } +} diff --git a/src/fast_automaton/convert/to_regex/neural_synthesis/mod.rs b/src/fast_automaton/convert/to_regex/neural_synthesis/mod.rs new file mode 100644 index 0000000..ad22c26 --- /dev/null +++ b/src/fast_automaton/convert/to_regex/neural_synthesis/mod.rs @@ -0,0 +1,604 @@ +//! Short, readable regexes for automata, proposed by learned models. +//! +//! [`FastAutomaton::to_regex`] converts an automaton by state elimination: +//! always correct, but often long and hard to read. A [`NeuralSynthesizer`] +//! runs a model that reads the automaton's minimal DFA and writes regexes for +//! it, usually much simpler ones. Every candidate is **checked** against the +//! automaton with an equivalence test and discarded unless it describes +//! exactly the same language, so a model can make a result simpler, never +//! wrong. A `NeuralSynthesizer` is a [`RegexSynthesizer`]: pass it to +//! [`FastAutomaton::to_regex_with`] or [`Term::to_regex_with`](crate::Term::to_regex_with). +//! +//! When the model has no correct regex for an automaton, or the automaton is +//! beyond its limits, the conversion splits the minimal DFA +//! into independent pieces (concatenations, unions, stars, and the paths +//! around a few hub states that break up large cycles), runs the model on +//! every piece small enough, in batches, and combines the simplest regex of +//! each piece. [`NeuralSynthesizer::with_decomposition`] turns this off. +//! +//! ```no_run +//! use regexsolver::{ +//! Term, +//! neural_synthesis::{Device, NeuralSynthesizer}, +//! }; +//! +//! // A model release directory: config.json and model.safetensors. +//! let synthesizer = NeuralSynthesizer::from_dir("kleene1-9m-b16-t128", Device::Cpu)?; +//! +//! let a: Term = ".*h(b|bh)".parse()?; +//! let b: Term = ".*[fi-z].*".parse()?; +//! let difference = a.difference(&b)?; +//! println!("{}", difference.to_regex_with(&synthesizer)?); +//! # Ok::<(), Box>(()) +//! ``` +//! +//! # Models +//! +//! A model is a pair of files: `config.json`, which names the model and its +//! format, and `model.safetensors`, its weights. They are not bundled with the +//! crate: load them from disk ([`NeuralSynthesizer::from_files`], +//! [`from_dir`](NeuralSynthesizer::from_dir), +//! [`from_bytes`](NeuralSynthesizer::from_bytes)), or, with the +//! `neural-synthesis-hub` feature, download them from the Hugging Face Hub +//! (`NeuralSynthesizer::from_hub`). The format is read from +//! `config.json`; the formats supported so far: +//! +//! - `kleene` v1, the Kleene models (`kleene1-9m-b16-t128`): a graph +//! transformer over the DFA and a transformer decoder over the regex, for +//! minimal DFAs of at most 16 states and 16 bases (the disjoint character +//! ranges labelling the transitions). +//! +//! # Devices +//! +//! Models run with [candle](https://github.com/huggingface/candle) on the CPU +//! by default. The `neural-synthesis-cuda`, `-cudnn`, `-metal`, `-mkl` and +//! `-accelerate` features enable candle's backends; pick the device with +//! [`Device`]. + +use std::{fmt, path::Path}; + +use serde::Deserialize; + +use crate::{ + error::EngineError, + execution_profile::ExecutionProfile, + fast_automaton::{FastAutomaton, RegexSynthesizer}, + regex::RegularExpression, +}; + +mod decompose; +#[cfg(all(feature = "neural-synthesis-hub", not(target_family = "wasm")))] +mod hub; +mod kleene; + +/// Where a model runs. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Device { + /// The CPU (faster with the `neural-synthesis-mkl` or + /// `neural-synthesis-accelerate` feature). + #[default] + Cpu, + /// The CUDA GPU with this ordinal; needs the `neural-synthesis-cuda` + /// feature. + Cuda(usize), + /// The Metal GPU with this ordinal; needs the `neural-synthesis-metal` + /// feature. + Metal(usize), +} + +impl Device { + /// The first CUDA GPU if the crate was built with CUDA support and one is + /// available, else the first Metal GPU likewise, else the CPU. + pub fn best_available() -> Self { + if candle_core::utils::cuda_is_available() { + Device::Cuda(0) + } else if candle_core::utils::metal_is_available() { + Device::Metal(0) + } else { + Device::Cpu + } + } + + fn to_candle(self) -> Result { + let device = match self { + Device::Cpu => return Ok(candle_core::Device::Cpu), + Device::Cuda(ordinal) => candle_core::Device::new_cuda(ordinal), + Device::Metal(ordinal) => candle_core::Device::new_metal(ordinal), + }; + device.map_err(|err| NeuralSynthesisError::Device(format!("{self:?}: {err}"))) + } +} + +/// An error loading a model. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum NeuralSynthesisError { + /// A model file could not be read. + Io(String), + /// `config.json` is invalid. + Config(String), + /// `config.json` describes a model format this version of the crate + /// cannot run. + UnsupportedFormat { + /// The `format` field. + format: String, + /// The `format_version` field. + version: u32, + }, + /// The weights do not match the model `config.json` describes. + Weights(String), + /// The device is not available, or the crate was built without its + /// backend. + Device(String), + /// The model could not be downloaded from the Hugging Face Hub. + Hub(String), +} + +impl fmt::Display for NeuralSynthesisError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NeuralSynthesisError::Io(err) => write!(f, "cannot read the model: {err}"), + NeuralSynthesisError::Config(err) => write!(f, "invalid model config: {err}"), + NeuralSynthesisError::UnsupportedFormat { format, version } => { + write!(f, "unsupported model format: {format} v{version}") + } + NeuralSynthesisError::Weights(err) => write!(f, "invalid model weights: {err}"), + NeuralSynthesisError::Device(err) => write!(f, "device unavailable: {err}"), + NeuralSynthesisError::Hub(err) => write!(f, "cannot download the model: {err}"), + } + } +} + +impl std::error::Error for NeuralSynthesisError {} + +impl From for NeuralSynthesisError { + fn from(err: std::io::Error) -> Self { + NeuralSynthesisError::Io(err.to_string()) + } +} + +/// How a model draws its candidates. +#[derive(Clone, Copy, Debug)] +struct Sampling { + /// At least 1: the greedy decoding, then samples. + candidates: usize, + temperature: f64, + seed: u64, +} + +/// A model format: turns a minimal DFA into candidate regexes, which the +/// caller verifies. +trait Model: Send + Sync { + /// The largest minimal DFA, in states, the model takes. + fn max_states(&self) -> usize; + + /// The largest minimal DFA, in bases, the model takes. + fn max_bases(&self) -> usize; + + /// Candidate regexes for each of `dfas`, DFAs from [`minimal_dfa`], + /// decoded together, in decoding order (the greedy one first); none for + /// a DFA beyond the model's limits. Candidates are not checked and may + /// repeat; a DFA's candidates do not depend on the others. + fn candidates( + &self, + dfas: &[&FastAutomaton], + sampling: &Sampling, + ) -> Result>, String>; +} + +/// The fields of `config.json` every format has. +#[derive(serde::Deserialize)] +struct Header { + name: String, + format: String, + format_version: u32, +} + +/// A loaded model, which proposes regexes for automata. See the [module +/// documentation](self). +/// +/// Loading parses the weights once; a `NeuralSynthesizer` is then cheap to +/// share between threads. +pub struct NeuralSynthesizer { + name: String, + model: Box, + sampling: Sampling, + decomposition: bool, +} + +impl fmt::Debug for NeuralSynthesizer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NeuralSynthesizer") + .field("name", &self.name) + .field("sampling", &self.sampling) + .field("decomposition", &self.decomposition) + .finish_non_exhaustive() + } +} + +impl NeuralSynthesizer { + /// Default number of candidates per automaton: the greedy decoding plus + /// seven samples. + pub const DEFAULT_CANDIDATES: usize = 8; + + /// Loads a model from its `config.json` and `model.safetensors` files. + pub fn from_files( + config: impl AsRef, + weights: impl AsRef, + device: Device, + ) -> Result { + let config = std::fs::read(config)?; + let weights = std::fs::read(weights)?; + Self::from_bytes(&config, weights, device) + } + + /// Loads a model from a directory holding `config.json` and + /// `model.safetensors`. + pub fn from_dir(dir: impl AsRef, device: Device) -> Result { + let dir = dir.as_ref(); + Self::from_files( + dir.join("config.json"), + dir.join("model.safetensors"), + device, + ) + } + + /// Loads a model from the contents of its `config.json` and + /// `model.safetensors`. + pub fn from_bytes( + config: &[u8], + weights: Vec, + device: Device, + ) -> Result { + let config: serde_json::Value = serde_json::from_slice(config) + .map_err(|err| NeuralSynthesisError::Config(err.to_string()))?; + let header = Header::deserialize(&config) + .map_err(|err| NeuralSynthesisError::Config(err.to_string()))?; + let model: Box = match (header.format.as_str(), header.format_version) { + (kleene::FORMAT, kleene::FORMAT_VERSION) => { + Box::new(kleene::Kleene::new(&config, weights, &device.to_candle()?)?) + } + _ => { + return Err(NeuralSynthesisError::UnsupportedFormat { + format: header.format, + version: header.format_version, + }); + } + }; + Ok(Self { + name: header.name, + model, + sampling: Sampling { + candidates: Self::DEFAULT_CANDIDATES, + temperature: 1.0, + seed: 0, + }, + decomposition: true, + }) + } + + /// Sets how many candidates are decoded per automaton (at least 1): the + /// greedy decoding, then samples. More candidates find a correct regex + /// more often, at the cost of decoding and checking them. + pub fn with_candidates(mut self, candidates: usize) -> Self { + self.sampling.candidates = candidates.max(1); + self + } + + /// Sets the sampling temperature of the candidates after the first + /// (default 1). A temperature that is not positive samples greedily, so + /// every candidate is the first. + pub fn with_temperature(mut self, temperature: f64) -> Self { + self.sampling.temperature = temperature; + self + } + + /// Sets the seed of the sampling (default 0). Proposals are + /// deterministic for a given seed and device. + pub fn with_seed(mut self, seed: u64) -> Self { + self.sampling.seed = seed; + self + } + + /// Sets whether converting ([`FastAutomaton::to_regex_with`]) splits an + /// automaton the model has no correct regex for, or is too large for, + /// into independent pieces and runs the model on those (default `true`). + /// Splitting finds simpler regexes for many more automata, at the cost of + /// running the model on many pieces: up to seconds on the CPU for + /// automata of dozens of states. + pub fn with_decomposition(mut self, decomposition: bool) -> Self { + self.decomposition = decomposition; + self + } + + /// The model's name, from its `config.json` (e.g. `kleene1-9m-b16-t128`). + pub fn model_name(&self) -> &str { + &self.name + } + + /// The largest minimal DFA, in states, the model takes. + pub fn max_states(&self) -> usize { + self.model.max_states() + } + + /// The largest minimal DFA, in bases (disjoint character ranges), the + /// model takes. + pub fn max_bases(&self) -> usize { + self.model.max_bases() + } + + /// Asks the model for a regex describing exactly the language of + /// `automaton`. + /// + /// Returns the simplest candidate equivalent to the automaton, or `None` + /// when its minimal DFA is beyond the model's limits or no candidate is + /// correct. Candidates whose check fails (for instance on the execution + /// profile's timeout) are skipped. + /// + /// Fails if `automaton` cannot be minimized within the execution + /// profile, or if the profile's deadline passes during inference. + pub fn propose( + &self, + automaton: &FastAutomaton, + ) -> Result, EngineError> { + Ok(self.propose_batch(&[automaton])?.pop().flatten()) + } + + /// [`propose`](Self::propose) for several automata at once: the model + /// decodes them together, which is much faster than one by one. The + /// result for each automaton is the same as `propose`'s. + pub fn propose_batch( + &self, + automata: &[&FastAutomaton], + ) -> Result>, EngineError> { + let mut dfas = Vec::with_capacity(automata.len()); + for automaton in automata { + dfas.push(minimal_dfa(automaton)?); + } + let eligible: Vec = (0..dfas.len()) + .filter(|i| dfas[*i].number_of_states() <= self.model.max_states()) + .collect(); + let mut proposals = vec![None; dfas.len()]; + if eligible.is_empty() { + return Ok(proposals); + } + let inputs: Vec<&FastAutomaton> = eligible.iter().map(|i| &dfas[*i]).collect(); + let candidates = match self.model.candidates(&inputs, &self.sampling) { + Ok(candidates) => candidates, + Err(err) => { + tracing::warn!("{} inference failed: {err}", self.name); + return Ok(proposals); + } + }; + // Inference is bounded by the model's size, not by the input, but can + // still outlast a short deadline. + ExecutionProfile::get().assert_not_timed_out()?; + + for (i, candidates) in eligible.into_iter().zip(candidates) { + proposals[i] = Self::check(&dfas[i], candidates); + } + Ok(proposals) + } + + /// A regex for `automaton`, which the model has no correct regex for or + /// cannot take, from its minimal DFA split into pieces the model converts + /// (see the [module documentation](self)); `None` when decomposition is + /// disabled. The result describes exactly the automaton's language. + /// + /// Fails if the execution profile's deadline passes. + pub(crate) fn propose_decomposed( + &self, + automaton: &FastAutomaton, + ) -> Result, EngineError> { + if !self.decomposition { + return Ok(None); + } + let mut decomposer = decompose::Decomposer::new(Some(self)); + decomposer.whole_tried = true; + let regex = decomposer.convert(automaton)?; + tracing::debug!(stats = ?decomposer.stats, "decomposed"); + Ok(Some(regex)) + } + + /// The simplest of `candidates` equivalent to `dfa`. + fn check(dfa: &FastAutomaton, candidates: Vec) -> Option { + let mut unique: Vec = Vec::with_capacity(candidates.len()); + for candidate in candidates { + if !unique.contains(&candidate) { + unique.push(candidate); + } + } + let mut ranked: Vec<(f64, usize, RegularExpression)> = unique + .into_iter() + .enumerate() + .map(|(i, regex)| (regex.evaluate_complexity(), i, regex)) + .collect(); + // The simplest first; ties keep the earliest (the greedy one first). + ranked.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1))); + + for (_, _, regex) in ranked { + match regex.to_automaton().and_then(|a| a.equivalent(dfa)) { + Ok(true) => return Some(regex), + Ok(false) => {} + Err(err) => tracing::debug!("candidate {regex} not checked: {err}"), + } + } + None + } +} + +impl RegexSynthesizer for NeuralSynthesizer { + /// The model first proposes a regex for the whole automaton. When it has + /// no correct one, or the automaton is beyond its limits, the automaton's + /// minimal DFA is split into independent pieces (unless + /// [`with_decomposition`](NeuralSynthesizer::with_decomposition) disables + /// it), each converted by the model or by state elimination, whichever is + /// simpler, and their regexes combined. Returns the simpler of that result + /// and [`FastAutomaton::to_regex`]'s state elimination; state elimination + /// alone if the model fails (for instance on the execution profile's + /// deadline). The result always describes exactly the automaton's + /// language. + fn synthesize(&self, automaton: &FastAutomaton) -> Result { + let proposed = self.propose(automaton).and_then(|proposed| match proposed { + Some(proposed) => Ok(Some(proposed)), + None => self.propose_decomposed(automaton), + }); + let proposed = match proposed { + Ok(proposed) => proposed, + Err(err) => { + tracing::debug!("neural synthesis skipped: {err}"); + None + } + }; + let Some(proposed) = proposed else { + return automaton.to_regex(); + }; + match automaton.to_regex() { + Ok(eliminated) if eliminated.evaluate_complexity() < proposed.evaluate_complexity() => { + Ok(eliminated) + } + // The proposal stands on its own if state elimination runs out + // of budget. + _ => Ok(proposed), + } + } +} + +/// Determinizes, minimizes, and shrinks the spanning set to the coarsest one +/// the transitions allow, so the number of bases is a property of the +/// language rather than of the construction. +fn minimal_dfa(automaton: &FastAutomaton) -> Result { + let mut dfa = automaton.determinize()?.into_owned(); + dfa.minimize()?; + Ok(dfa) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The model under `KLEENE_MODEL_DIR`, for the tests that need weights: + /// `KLEENE_MODEL_DIR= cargo test --release --features neural-synthesis neural_synthesis -- --ignored` + fn kleene() -> NeuralSynthesizer { + let dir = std::env::var("KLEENE_MODEL_DIR").expect("KLEENE_MODEL_DIR"); + NeuralSynthesizer::from_dir(dir, Device::Cpu).unwrap() + } + + #[test] + #[ignore = "needs the model weights (KLEENE_MODEL_DIR)"] + fn to_regex_with_keeps_the_language() { + let synthesizer = kleene(); + assert_eq!(synthesizer.model_name(), "kleene1-9m-b16-t128"); + assert_eq!( + (synthesizer.max_states(), synthesizer.max_bases()), + (16, 16) + ); + for (a, b) in [ + (".*h(b|bh)", ".*[fi-z].*"), + (".*abc.*", ".*def.*"), + ("x*", "(xxx)*"), + ("(ab|cd)*", "(abcd)*"), + ("[a-z]{2,6}", ".*q.*"), + ("(a|b)*abb", "a.*"), + ] { + let a = RegularExpression::new(a).unwrap().to_automaton().unwrap(); + let b = RegularExpression::new(b).unwrap().to_automaton().unwrap(); + let automaton = a.difference(&b).unwrap(); + let regex = automaton.to_regex_with(&synthesizer).unwrap(); + assert!( + regex + .to_automaton() + .unwrap() + .equivalent(&automaton) + .unwrap(), + "{regex}" + ); + let eliminated = automaton.to_regex().unwrap(); + assert!( + regex.evaluate_complexity() <= eliminated.evaluate_complexity(), + "{regex} vs {eliminated}" + ); + } + } + + #[test] + #[ignore = "needs the model weights (KLEENE_MODEL_DIR)"] + fn decomposes_automata_beyond_the_model() { + let synthesizer = kleene(); + let mut automaton = RegularExpression::new(".*abc.*") + .unwrap() + .to_automaton() + .unwrap(); + for other in [".*def.*", ".*gh.*"] { + let other = RegularExpression::new(other) + .unwrap() + .to_automaton() + .unwrap(); + automaton = automaton.intersection(&other).unwrap(); + } + // As a DFA, like the results of differences and complements. + let automaton = minimal_dfa(&automaton).unwrap(); + assert!(automaton.number_of_states() > synthesizer.max_states()); + let eliminated = automaton.to_regex().unwrap(); + + let regex = automaton.to_regex_with(&synthesizer).unwrap(); + assert!( + regex + .to_automaton() + .unwrap() + .equivalent(&automaton) + .unwrap(), + "{regex}" + ); + assert!(regex.evaluate_complexity() < eliminated.evaluate_complexity()); + + let undecomposed = automaton + .to_regex_with(&synthesizer.with_decomposition(false)) + .unwrap(); + assert_eq!(undecomposed, eliminated); + } + + #[test] + #[ignore = "needs the model weights (KLEENE_MODEL_DIR)"] + fn simplifies_the_model_card_example() { + // `[a-egh]*hbh?`, as state elimination writes it. + let automaton = RegularExpression::new( + "([a-eg]|h+[ac-eg]|(h+b)+([a-eg]|h[ac-eg]|h{2,}[ac-eg]))*(h+b)+h?", + ) + .unwrap() + .to_automaton() + .unwrap(); + let regex = kleene() + .with_candidates(1) + .propose(&automaton) + .unwrap() + .unwrap(); + assert_eq!(regex.to_string(), "[a-egh]*hbh?"); + } + + #[test] + fn rejects_unknown_formats() { + let config = br#"{"name": "x", "format": "other", "format_version": 1}"#; + assert!(matches!( + NeuralSynthesizer::from_bytes(config, vec![], Device::Cpu), + Err(NeuralSynthesisError::UnsupportedFormat { format, version: 1 }) if format == "other" + )); + assert!(matches!( + NeuralSynthesizer::from_bytes(b"not json", vec![], Device::Cpu), + Err(NeuralSynthesisError::Config(_)) + )); + } + + #[test] + fn devices_need_their_backend() { + assert!(Device::default().to_candle().unwrap().is_cpu()); + #[cfg(not(feature = "neural-synthesis-cuda"))] + assert!(matches!( + Device::Cuda(0).to_candle(), + Err(NeuralSynthesisError::Device(_)) + )); + #[cfg(not(feature = "neural-synthesis-cuda"))] + assert_eq!(Device::best_available(), Device::Cpu); + } +} diff --git a/src/fast_automaton/mod.rs b/src/fast_automaton/mod.rs index 19cc83c..6290da3 100644 --- a/src/fast_automaton/mod.rs +++ b/src/fast_automaton/mod.rs @@ -19,13 +19,14 @@ mod builder; /// Transition labels: the bitvector [`Condition`] type over an automaton's /// spanning set of disjoint character ranges. pub mod condition; -mod convert; +pub(crate) mod convert; mod generate; mod operation; /// The [`SpanningSet`]: an automaton's partition of the alphabet into disjoint /// character ranges, over which transition conditions are defined. pub mod spanning_set; +pub use convert::to_regex::{RegexSynthesizer, StateElimination}; pub use generate::{CharacterOrder, GenerationOptions, PathOrder}; /// The block of code points `char` cannot hold: [`regex_charclass::char::Char`] diff --git a/src/lib.rs b/src/lib.rs index 3abd7b1..de4b46a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,8 @@ pub mod execution_profile; /// Finite automata: [`FastAutomaton`] and its building blocks (conditions, /// spanning sets). pub mod fast_automaton; +#[cfg(feature = "neural-synthesis")] +pub use fast_automaton::convert::to_regex::neural_synthesis; /// The parsed-pattern AST: [`RegularExpression`]. pub mod regex; @@ -1005,7 +1007,8 @@ impl Term { /// /// Returns a [`Cow`]: borrows the expression when the term is already /// regex-backed, and allocates a new one when converting from a - /// [`FastAutomaton`] via state elimination. + /// [`FastAutomaton`] via state elimination (see + /// [`FastAutomaton::to_regex`]). #[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))] pub fn to_regex(&self) -> Result, EngineError> { Ok(match self { @@ -1014,6 +1017,19 @@ impl Term { }) } + /// Converts the term to a [`RegularExpression`] with `synthesizer` when it + /// is automaton-backed: see [`FastAutomaton::to_regex_with`]. A + /// regex-backed term is borrowed as is. + pub fn to_regex_with( + &self, + synthesizer: &S, + ) -> Result, EngineError> { + Ok(match self { + Term::RegularExpression(regex) => Cow::Borrowed(regex), + Term::Automaton(automaton) => Cow::Owned(automaton.to_regex_with(synthesizer)?), + }) + } + /// Converts the term to a regular expression pattern. pub fn to_pattern(&self) -> Result { Ok(self.to_regex()?.to_string()) diff --git a/src/regex/mod.rs b/src/regex/mod.rs index 3a2eb8d..02e8898 100644 --- a/src/regex/mod.rs +++ b/src/regex/mod.rs @@ -272,94 +272,82 @@ impl RegularExpression { } } - /// Returns a heuristic score for the readability of the pattern. + /// Returns the cognitive complexity of the pattern: a readability score, + /// independent of the characters used. + /// + /// - class: 1. + /// - concatenation: the sum of its parts. + /// - alternation: its branches read one level deeper, plus `1 + depth` per + /// extra branch. + /// - quantifier: its operand, plus the quantifier's weight × `(1 + depth)`; + /// `?`, `*`, `+` are cheapest, then `{n}`, `{n,}`, `{m,n}`. A quantified + /// group reads one level deeper, and a quantifier with another quantifier + /// inside it pays an extra `1 + depth`. + /// + /// The empty string costs 0; the empty language `[]` is a class (1). pub fn evaluate_complexity(&self) -> f64 { - let (score, depth, _) = self.eval_inner(); - score + Self::depth_penalty(depth) - } - - /// Returns: (score, max_depth, contains_repetition) - fn eval_inner(&self) -> (f64, usize, bool) { - match self { - RegularExpression::Character(range) => { - let len = range.to_regex().len() as f64; - // small, capped cost for raw length - let base = 1.0 + 0.05 * len.min(40.0); - (base, 1, false) - } - - RegularExpression::Repetition(inner, min, max_opt) => { - let (inner_score, inner_depth, inner_has_rep) = inner.eval_inner(); - - // multipliers tuned for readability impact - let mut m = match max_opt { - None => 1.6, - Some(max) if max > min => 1.3, - Some(max) if max == min && *min > 1 => 1.1, - _ => 1.0, - }; - - // nested quantifiers like (...+)+ are harder - if inner_has_rep { - m *= 1.5; - } - - (inner_score * m, inner_depth + 1, true) - } - - RegularExpression::Concat(items) => { - let mut sum = 0.0; - let mut max_depth = 0usize; - let mut has_rep = false; - - for (i, it) in items.iter().enumerate() { - let (s, d, h) = it.eval_inner(); - sum += s; - if i > 0 { - // tiny discount: linear sequences are relatively easy to read - sum *= 0.98; - } - if d > max_depth { - max_depth = d; - } - has_rep |= h; + const CLASS: f64 = 1.0; + const NESTED_QUANTIFIER: f64 = 1.0; + + // Iterative, since a hand-built tree can nest arbitrarily deep. Each + // node adds its own cost at its depth. A quantifier's extra cost for + // holding another one is settled at the end: each quantifier marks + // the nearest one around it, which suffices, as that one is in turn + // inside any further ones. + let mut score = 0.0; + // Per quantifier: its `1 + depth`, and whether it holds another one. + let mut quantifiers: Vec<(f64, bool)> = Vec::new(); + let mut stack = vec![(self, 0usize, None::)]; + while let Some((node, depth, enclosing)) = stack.pop() { + let nesting = 1.0 + depth as f64; + match node { + RegularExpression::Character(_) => score += CLASS, + RegularExpression::Concat(items) => { + stack.extend(items.iter().map(|item| (item, depth, enclosing))); } - - (sum, max_depth + 1, has_rep) - } - - RegularExpression::Alternation(branches) => { - if branches.is_empty() { - return (0.0, 1, false); + RegularExpression::Alternation(branches) => { + score += branches.len().saturating_sub(1) as f64 * nesting; + stack.extend(branches.iter().map(|branch| (branch, depth + 1, enclosing))); } - let mut sum = 0.0; - let mut max_depth = 0usize; - let mut has_rep = false; - - for b in branches { - let (s, d, h) = b.eval_inner(); - sum += s; - if d > max_depth { - max_depth = d; + RegularExpression::Repetition(inner, min, max) => { + score += Self::quantifier_weight(*min, *max) * nesting; + if let Some(enclosing) = enclosing { + quantifiers[enclosing].1 = true; } - has_rep |= h; + quantifiers.push((nesting, false)); + // A single class needs no group; anything else is a group + // the reader has to enter. + let inner_depth = match **inner { + RegularExpression::Character(_) => depth, + _ => depth + 1, + }; + stack.push((inner, inner_depth, Some(quantifiers.len() - 1))); } - - // branching cost: more alternatives = harder to scan - let k = branches.len() as f64; - let multiplier = 1.0 + 0.15 * (k - 1.0); - - (sum * multiplier, max_depth + 1, has_rep) } } + score + + quantifiers + .iter() + .filter(|(_, nested)| *nested) + .map(|(nesting, _)| NESTED_QUANTIFIER * nesting) + .sum::() } - fn depth_penalty(depth: usize) -> f64 { - // no penalty up to depth 2, then quadratic growth - if depth <= 2 { - 0.0 - } else { - ((depth - 2) as f64).powi(2) * 0.8 + fn quantifier_weight(min: u32, max: Option) -> f64 { + /// `?`, `*`, `+`. + const SIMPLE_QUANTIFIER: f64 = 0.5; + /// `{n}`. + const EXACT_COUNT: f64 = 1.0; + /// `{n,}`. + const OPEN_COUNT: f64 = 1.5; + /// `{m,n}`. + const RANGE_COUNT: f64 = 2.0; + + match (min, max) { + (0, Some(1)) | (0, None) | (1, None) => SIMPLE_QUANTIFIER, + (_, None) => OPEN_COUNT, + (min, Some(max)) if min == max => EXACT_COUNT, + _ => RANGE_COUNT, } } } @@ -368,6 +356,23 @@ impl RegularExpression { mod tests { use super::*; + #[test] + fn test_evaluate_complexity() { + let complexity = |pattern: &str| { + RegularExpression::parse(pattern, false) + .unwrap() + .evaluate_complexity() + }; + assert_eq!(complexity(""), 0.0); + assert_eq!(complexity("[]"), 1.0); + assert_eq!(complexity("abc"), 3.0); + assert_eq!(complexity("[a-c]*"), 1.5); + assert_eq!(complexity("(ab|c)"), 4.0); + assert_eq!(complexity("(ab)*"), 2.5); + assert_eq!(complexity("(a*b)+"), 4.5); + assert!(complexity("[a-egh]*hbh?") < complexity("(h+b)+h?")); + } + #[test] fn test_empty() -> Result<(), String> { let automaton = RegularExpression::new_empty(); diff --git a/tests/data/kleene_parity.json b/tests/data/kleene_parity.json new file mode 100644 index 0000000..1e21b6a --- /dev/null +++ b/tests/data/kleene_parity.json @@ -0,0 +1 @@ +[{"pattern":"ab","base_ranges":["[^ab]","a","b"],"graph":{"node_type":[1,0,0,2],"is_accept":[0,0,1,0],"fwd_depth":[0,1,2,1],"bwd_depth":[2,1,0,65],"to_reject":[0,0,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,3,1,3,3,3,2,3,3,3,3,3,3],"edge_dst":[3,1,3,3,3,2,3,3,3,3,3,3,0,0,0,1,1,1,2,2,2,3,3,3]},"first_tok_logits":[-10.07668,-10.15442,-4.32634,12.8201,-2.02275,-0.89406,2.96708,6.29489,-5.52928,-2.72819,-5.8963,-4.62489,-5.55338,-4.77665,-6.84739,-6.79226,-7.78847,-5.62163,-7.71996,-6.58441,-6.85748,-7.88063,-5.96944,-5.26394,-6.58062,-3.27438],"first_mask_logits":[-6.39989,0.66896,4.2227,-7.04473,-7.10521,-8.20475,-7.57515,-9.30018,-5.40567,-7.70866,-11.1332,-6.41728,-5.10549,-8.53324,-7.21307,-8.17123],"tokens":[3,6,6,5],"masks":[0,2,4,0],"regex":"ab"},{"pattern":"a*","base_ranges":["[^a]","a"],"graph":{"node_type":[1,2],"is_accept":[1,0],"fwd_depth":[0,1],"bwd_depth":[0,65],"to_reject":[1,0],"edge_type":[0,1,0,1,16,17,16,17],"edge_src":[0,0,1,1,1,0,1,1],"edge_dst":[1,0,1,1,0,0,1,1]},"first_tok_logits":[-10.36497,-10.69919,-2.68688,6.42316,-2.56532,1.11879,-1.02868,13.77583,-3.23631,-5.00852,-9.71075,-7.48051,-5.68004,-6.10327,-7.31269,-8.3211,-6.93864,-7.34226,-7.60333,-7.75468,-6.96385,-6.66226,-6.4129,-5.37518,-6.2885,1.06418],"first_mask_logits":[-2.99299,2.50806,-3.77851,-9.45497,-5.04443,-5.64603,-6.7266,-8.04392,-4.26578,-5.13402,-10.72933,-7.11475,-6.65418,-5.50673,-7.21476,-6.45943],"tokens":[7,8,25,6],"masks":[0,0,0,2],"regex":"a*"},{"pattern":"(a|b)*abb","base_ranges":["[^ab]","a","b"],"graph":{"node_type":[1,0,0,0,2],"is_accept":[0,0,0,1,0],"fwd_depth":[0,1,2,3,1],"bwd_depth":[3,2,1,0,65],"to_reject":[0,0,0,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,4,1,0,4,1,2,4,1,3,4,1,0,4,4,4],"edge_dst":[4,1,0,4,1,2,4,1,3,4,1,0,4,4,4,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4]},"first_tok_logits":[-11.01499,-11.06064,-6.14988,12.73409,0.26721,-1.92069,-0.3849,6.02018,-5.13721,-3.061,-6.7344,-4.36311,-4.11911,-4.74627,-6.06054,-7.14834,-6.66315,-6.86935,-7.86375,-7.55975,-6.10709,-8.40941,-6.58856,-6.88006,-8.0123,-1.21982],"first_mask_logits":[-3.35833,0.00236,3.90074,-5.92673,-5.77899,-3.09195,-3.81705,-6.83601,-1.64278,-5.57699,-9.45001,-6.40356,-2.10538,-5.33965,-8.34623,-6.88637],"tokens":[3,7,8,25,6,6,7,10,10,6,5],"masks":[0,0,0,0,6,2,0,0,0,4,0],"regex":"[ab]*ab{2}"},{"pattern":"[a-egh]*hbh?","base_ranges":["[^a-egh]","[ac-eg]","h","b"],"graph":{"node_type":[1,0,0,0,2],"is_accept":[0,0,1,1,0],"fwd_depth":[0,1,2,3,1],"bwd_depth":[2,1,0,0,65],"to_reject":[0,0,1,1,0],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,4,0,1,0,4,0,1,2,4,0,3,0,4,0,1,2,4,4,4,4],"edge_dst":[4,0,1,0,4,0,1,2,4,0,3,0,4,0,1,2,4,4,4,4,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4]},"first_tok_logits":[-11.38034,-11.49763,-5.40419,13.57665,1.18323,-1.25268,-0.76274,3.49708,-5.1156,-2.62122,-6.07272,-3.87737,-4.49239,-4.83062,-5.41159,-5.99253,-5.49406,-5.60088,-7.69531,-8.14968,-7.19452,-8.72298,-8.18236,-6.99434,-9.13264,-1.87313],"first_mask_logits":[-4.41045,0.24423,0.92945,-3.08164,-5.81544,-4.98645,-5.79149,-9.12595,-3.6773,-5.2169,-9.39645,-7.38648,-2.40945,-5.09911,-9.70911,-7.41748],"tokens":[3,7,8,25,6,6,6,7,8,9,6,5],"masks":[0,0,0,0,14,4,8,0,0,0,4,0],"regex":"[a-egh]*hbh?"},{"pattern":".*abc","base_ranges":["a","b","[^a-c]","c"],"graph":{"node_type":[1,0,0,0],"is_accept":[0,0,0,1],"fwd_depth":[0,1,2,3],"bwd_depth":[3,2,1,0],"to_reject":[0,0,0,1],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,1,0,0,0,1,2,0,0,1,0,0,3,1,0,0,0],"edge_dst":[1,0,0,0,1,2,0,0,1,0,0,3,1,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3]},"first_tok_logits":[-11.69655,-11.87468,-5.56228,14.59394,1.08676,-0.59562,0.48573,3.41888,-6.02639,-2.61327,-6.64009,-5.35285,-5.56074,-6.10851,-6.43291,-7.75646,-6.69424,-6.45151,-10.28358,-9.89863,-8.02349,-8.63013,-9.00341,-6.93333,-11.47243,-2.02729],"first_mask_logits":[-2.73445,1.07674,-4.07974,-1.35737,-11.74988,-9.3048,-9.50407,-13.11033,-8.83506,-7.49255,-11.74765,-9.41432,-6.42844,-9.1569,-9.21198,-8.82607],"tokens":[3,7,8,25,6,6,6,6,5],"masks":[0,0,0,0,15,1,2,8,0],"regex":".*abc"},{"pattern":"(ab|cd)+e","base_ranges":["[^a-e]","b","d","e","a","c"],"graph":{"node_type":[1,0,0,0,0,2],"is_accept":[0,0,0,0,1,0],"fwd_depth":[0,1,1,2,3,1],"bwd_depth":[3,2,2,1,0,65],"to_reject":[0,0,0,0,1,0],"edge_type":[0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,0,1,2,3,4,5,16,17,18,19,20,21,16,17,18,19,20,21,16,17,18,19,20,21,16,17,18,19,20,21,16,17,18,19,20,21,16,17,18,19,20,21],"edge_src":[0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,1,2,5,3,5,5,5,5,5,5,3,5,5,5,5,5,5,4,1,2,5,5,5,5,5,5,5,5,5,5,5,5],"edge_dst":[5,5,5,5,1,2,5,3,5,5,5,5,5,5,3,5,5,5,5,5,5,4,1,2,5,5,5,5,5,5,5,5,5,5,5,5,0,0,0,0,0,0,1,1,1,1,1,1,2,2,2,2,2,2,3,3,3,3,3,3,4,4,4,4,4,4,5,5,5,5,5,5]},"first_tok_logits":[-10.33026,-10.27715,-4.23903,11.90382,2.607,-0.88775,-0.65323,3.0938,-3.51309,-2.24088,-6.5339,-5.11115,-5.24629,-4.46946,-4.62129,-4.33047,-4.72517,-3.71695,-7.66899,-7.26026,-7.06947,-6.42325,-7.09932,-4.61966,-7.66193,-2.39199],"first_mask_logits":[-5.87091,-3.71326,-3.30679,2.75096,-3.40071,-3.35261,-4.35052,-6.8096,-3.59247,-3.62706,-7.35112,-4.44333,-2.1802,-5.51062,-8.20721,-8.12992],"tokens":[3,7,9,25,4,3,6,6,5,3,6,6,5,5,6,5],"masks":[0,0,0,0,0,0,16,2,0,0,32,4,0,0,8,0],"regex":"(ab|cd)+e"},{"pattern":"a(b|c)*d","base_ranges":["[^a-d]","a","d","[bc]"],"graph":{"node_type":[1,0,0,2],"is_accept":[0,0,1,0],"fwd_depth":[0,1,2,1],"bwd_depth":[2,1,0,65],"to_reject":[0,0,1,0],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,3,1,3,3,3,3,2,1,3,3,3,3,3,3,3,3],"edge_dst":[3,1,3,3,3,3,2,1,3,3,3,3,3,3,3,3,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3]},"first_tok_logits":[-10.3907,-10.52303,-4.73264,13.13507,-2.64854,-0.68557,2.96036,6.29255,-5.43341,-3.04095,-5.86854,-4.2642,-5.46475,-5.25904,-6.59786,-6.46837,-7.294,-5.78936,-8.17776,-7.58045,-7.46144,-9.04809,-6.55697,-5.74125,-6.71707,-3.29705],"first_mask_logits":[-4.80352,-2.82675,6.53027,-4.54761,-6.66414,-7.33729,-6.82637,-8.76298,-4.81023,-7.04765,-11.63065,-7.33964,-3.84515,-8.82838,-9.60383,-9.3438],"tokens":[3,6,7,8,25,6,6,5],"masks":[0,2,0,0,0,8,4,0],"regex":"a[bc]*d"},{"pattern":"x(x{3})*x?","base_ranges":["[^x]","x"],"graph":{"node_type":[1,0,0,2],"is_accept":[0,1,1,0],"fwd_depth":[0,1,2,1],"bwd_depth":[1,0,0,65],"to_reject":[0,1,1,0],"edge_type":[0,1,0,1,0,1,0,1,16,17,16,17,16,17,16,17],"edge_src":[0,0,1,1,2,2,3,3,3,1,3,2,3,0,3,3],"edge_dst":[3,1,3,2,3,0,3,3,0,0,1,1,2,2,3,3]},"first_tok_logits":[-12.00443,-11.95268,-5.67828,14.37285,1.62863,-2.92998,2.52142,2.30831,-6.15795,-2.31583,-6.64944,-4.22751,-3.97556,-4.33436,-6.63486,-7.09598,-6.67169,-6.05285,-7.71602,-7.74485,-7.73302,-7.2729,-6.74425,-6.46619,-9.18278,-2.30223],"first_mask_logits":[-4.2896,5.00347,-1.89846,-6.04216,-5.68549,-6.1943,-6.15222,-8.05355,-5.89234,-4.74145,-8.90577,-7.75422,-3.93584,-5.04453,-8.76729,-8.17916],"tokens":[3,6,7,8,25,3,6,6,6,5,7,8,9,6,5],"masks":[0,2,0,0,0,0,2,2,2,0,0,0,0,2,0],"regex":"x(xxx)*x?"},{"pattern":"(abc)*(a|ab)","base_ranges":["[^a-c]","c","b","a"],"graph":{"node_type":[1,0,0,2],"is_accept":[0,1,1,0],"fwd_depth":[0,1,2,1],"bwd_depth":[1,0,0,65],"to_reject":[0,1,1,0],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,3,3,3,1,3,3,2,3,3,0,3,3,3,3,3,3],"edge_dst":[3,3,3,1,3,3,2,3,3,0,3,3,3,3,3,3,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3]},"first_tok_logits":[-11.44932,-11.52742,-5.96837,16.06324,0.12948,-1.92422,2.56896,2.80072,-5.60861,-2.5859,-6.319,-4.95361,-4.67604,-4.04088,-6.78364,-7.45163,-7.62656,-6.71528,-8.97494,-8.07573,-8.34036,-7.59565,-7.82188,-5.98403,-8.86059,-2.78598],"first_mask_logits":[-5.63343,2.01586,-0.48311,-1.69325,-5.59169,-5.02387,-5.87941,-7.96649,-4.35071,-4.1105,-8.8599,-8.49226,-4.29223,-7.12827,-9.57998,-9.28874],"tokens":[3,6,7,8,25,3,6,6,6,5,7,8,9,6,5],"masks":[0,8,0,0,0,0,4,2,8,0,0,0,0,4,0],"regex":"a(bca)*b?"},{"pattern":"[0-9]{2,4}","base_ranges":["[^0-9]","[0-9]"],"graph":{"node_type":[1,0,0,0,0,2],"is_accept":[0,0,1,1,1,0],"fwd_depth":[0,1,2,3,4,1],"bwd_depth":[2,1,0,0,0,65],"to_reject":[0,0,1,1,1,0],"edge_type":[0,1,0,1,0,1,0,1,0,1,0,1,16,17,16,17,16,17,16,17,16,17,16,17],"edge_src":[0,0,1,1,2,2,3,3,4,4,5,5,5,1,5,2,5,3,5,4,5,5,5,5],"edge_dst":[5,1,5,2,5,3,5,4,5,5,5,5,0,0,1,1,2,2,3,3,4,4,5,5]},"first_tok_logits":[-11.04194,-11.13417,-5.91679,7.97197,-0.55195,-0.91762,0.39344,11.00041,-6.45414,-4.23808,-6.92928,-4.80183,-3.46523,-3.12209,-4.81285,-6.48083,-7.18406,-6.3688,-8.24608,-8.22951,-7.33365,-7.52752,-6.86638,-5.85713,-7.32967,-3.44912],"first_mask_logits":[-4.95972,5.36189,-0.60046,-4.34185,-5.30968,-5.45159,-4.23644,-7.74485,-4.96911,-8.14098,-13.44219,-7.78033,-5.32214,-6.75968,-7.43668,-7.90846],"tokens":[7,10,12,6],"masks":[0,0,0,2],"regex":"[0-9]{2,4}"},{"pattern":"(a*ba*ba*)*","base_ranges":["[^ab]","b","a"],"graph":{"node_type":[1,0,0,0,2],"is_accept":[1,0,0,1,0],"fwd_depth":[0,1,1,2,1],"bwd_depth":[0,1,2,0,65],"to_reject":[1,0,0,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,4,1,2,4,3,1,4,1,2,4,1,3,4,4,4],"edge_dst":[4,1,2,4,3,1,4,1,2,4,1,3,4,4,4,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4]},"first_tok_logits":[-13.67813,-13.95369,-3.58772,5.13491,3.98237,-1.43226,-0.43159,10.73554,-2.01376,-4.45942,-8.39522,-7.0368,-6.72202,-6.43329,-6.83617,-6.19877,-5.96226,-5.91444,-9.91197,-10.00211,-10.19067,-9.34397,-8.44153,-8.00022,-8.58773,-4.24451],"first_mask_logits":[-5.21334,0.40355,-1.71803,-8.86068,-5.05524,-5.99221,-6.78278,-8.99333,-5.10913,-7.19262,-14.41626,-10.87755,-7.63333,-11.96667,-12.27603,-9.89972],"tokens":[7,8,25,3,7,8,25,6,6,7,8,25,6,6,7,8,25,6,5],"masks":[0,0,0,0,0,0,0,4,2,0,0,0,4,2,0,0,0,4,0],"regex":"(a*ba*ba*)*"},{"pattern":"[^a]*a[^a]*","base_ranges":["a","[^a]"],"graph":{"node_type":[1,0,2],"is_accept":[0,1,0],"fwd_depth":[0,1,2],"bwd_depth":[1,0,65],"to_reject":[0,1,0],"edge_type":[0,1,0,1,0,1,16,17,16,17,16,17],"edge_src":[0,0,1,1,2,2,1,0,2,1,2,2],"edge_dst":[1,0,2,1,2,2,0,0,1,1,2,2]},"first_tok_logits":[-12.54731,-12.7253,-5.48549,13.6682,-0.0524,-0.49024,1.04664,4.18384,-6.27854,-3.61036,-6.7374,-4.58268,-4.06314,-4.86346,-5.77998,-6.05516,-5.37004,-5.58142,-7.79332,-8.36222,-7.01307,-7.88183,-6.99438,-6.16157,-9.20189,-3.36055],"first_mask_logits":[1.17352,2.53975,-2.12918,-7.50328,-8.22025,-6.68944,-6.17722,-11.71448,-5.97927,-6.81281,-11.44476,-9.13844,-6.31206,-5.92542,-11.71729,-8.72913],"tokens":[3,7,8,25,6,6,7,8,25,6,5],"masks":[0,0,0,0,2,1,0,0,0,2,0],"regex":"[^a]*a[^a]*"},{"pattern":"(ab)*|(ba)*","base_ranges":["[^ab]","a","b"],"graph":{"node_type":[1,0,0,0,0,2],"is_accept":[1,0,0,1,1,0],"fwd_depth":[0,1,1,2,2,1],"bwd_depth":[0,1,1,0,0,65],"to_reject":[1,0,0,1,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,5,1,2,5,5,3,5,4,5,5,1,5,5,5,2,5,5,5],"edge_dst":[5,1,2,5,5,3,5,4,5,5,1,5,5,5,2,5,5,5,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]},"first_tok_logits":[-9.35956,-9.13184,-2.85991,2.89158,11.92206,-3.43461,0.79812,0.36625,-2.76724,-2.55644,-5.78879,-5.59066,-5.04702,-3.4174,-3.60447,-4.18451,-4.22751,-4.68259,-7.76724,-7.16913,-5.5962,-5.07577,-2.46131,-2.31215,-4.60097,-1.80603],"first_mask_logits":[-6.14712,0.09587,2.43895,-5.58924,-5.36556,-4.54639,-3.67568,-6.46074,-2.36128,-3.58814,-7.53412,-6.84516,-6.43463,-8.2642,-8.51905,-9.29288],"tokens":[4,7,8,25,3,6,6,5,7,8,25,3,6,6,5,5],"masks":[0,0,0,0,0,6,4,0,0,0,0,0,2,4,0,0],"regex":"(([ab]b)*|(ab)*)"},{"pattern":"[a-z]+@[a-z]+\\.(com|org)","base_ranges":["[^\\.@a-z]","\\.","@","[abd-fh-lnpqs-z]","r","c","m","g","o"],"graph":{"node_type":[1,0,0,0,0,0,0,0,0,0,2],"is_accept":[0,0,0,0,0,0,0,0,0,1,0],"fwd_depth":[0,1,2,3,4,5,5,6,6,7,1],"bwd_depth":[7,6,5,4,3,2,2,1,1,0,65],"to_reject":[0,0,0,0,0,0,0,0,0,1,0],"edge_type":[0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,0,1,2,3,4,5,6,7,8,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24,16,17,18,19,20,21,22,23,24],"edge_src":[0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,1,1,1,1,1,1,10,10,2,1,1,1,1,1,1,10,10,10,3,3,3,3,3,3,10,4,10,3,3,3,3,3,3,10,10,10,10,10,5,10,10,6,10,10,10,10,10,10,10,10,7,10,10,10,10,8,10,10,10,10,10,10,10,10,10,10,9,10,10,10,10,10,10,10,10,10,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10],"edge_dst":[10,10,10,1,1,1,1,1,1,10,10,2,1,1,1,1,1,1,10,10,10,3,3,3,3,3,3,10,4,10,3,3,3,3,3,3,10,10,10,10,10,5,10,10,6,10,10,10,10,10,10,10,10,7,10,10,10,10,8,10,10,10,10,10,10,10,10,10,10,9,10,10,10,10,10,10,10,10,10,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10]},"first_tok_logits":[-10.04955,-10.24338,-4.40584,12.12487,1.06558,-1.42334,0.15395,4.70665,-5.58419,-2.701,-4.58469,-3.89926,-4.46389,-4.2328,-5.47664,-5.95897,-5.82868,-4.31161,-6.70784,-6.1221,-5.5158,-6.64931,-5.28544,-4.56532,-5.91424,-3.70048],"first_mask_logits":[-7.20519,3.25124,1.79269,-1.94827,-3.46217,-1.59189,-0.63419,-3.49658,3.12846,-7.448,-9.69012,-5.37116,-4.90935,-5.72535,-9.1151,-8.05904],"tokens":[3,7,9,25,6,6,7,9,25,6,6,4,3,6,6,6,5,3,6,6,6,5,5,5],"masks":[0,0,0,0,504,4,0,0,0,504,2,0,0,288,256,64,0,0,256,16,128,0,0,0],"regex":"[a-z]+@[a-z]+\\.([co]om|org)"},{"pattern":"((a|b)(c|d))*","base_ranges":["[^a-d]","[ab]","[cd]"],"graph":{"node_type":[1,0,2],"is_accept":[1,0,0],"fwd_depth":[0,1,1],"bwd_depth":[0,1,65],"to_reject":[1,0,0],"edge_type":[0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,2,1,2,2,2,0,2,2,2],"edge_dst":[2,1,2,2,2,0,2,2,2,0,0,0,1,1,1,2,2,2]},"first_tok_logits":[-10.37514,-10.58854,-3.78075,4.4952,-4.57928,-0.26335,2.24458,15.13544,-2.68496,-5.48734,-9.18906,-6.67457,-5.83038,-6.10909,-7.58261,-8.23886,-7.73304,-8.30378,-9.22812,-8.56824,-8.23944,-7.96841,-6.2879,-5.36694,-5.26115,1.18156],"first_mask_logits":[-5.50637,-1.16617,1.86417,-8.06851,-2.92268,-5.57109,-6.84812,-8.09223,-4.02098,-5.54467,-10.83781,-6.95933,-6.02192,-10.12383,-7.20624,-7.83954],"tokens":[7,8,25,3,6,6,5],"masks":[0,0,0,0,2,4,0],"regex":"([ab][cd])*"},{"pattern":"a{3}b{2}c","base_ranges":["[^a-c]","c","b","a"],"graph":{"node_type":[1,0,0,0,0,0,0,2],"is_accept":[0,0,0,0,0,0,1,0],"fwd_depth":[0,1,2,3,4,5,6,1],"bwd_depth":[6,5,4,3,2,1,0,65],"to_reject":[0,0,0,0,0,0,1,0],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,7,7,7,1,7,7,7,2,7,7,7,3,7,7,4,7,7,7,5,7,7,6,7,7,7,7,7,7,7,7,7,7],"edge_dst":[7,7,7,1,7,7,7,2,7,7,7,3,7,7,4,7,7,7,5,7,7,6,7,7,7,7,7,7,7,7,7,7,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7]},"first_tok_logits":[-9.98282,-10.04832,-5.13171,14.2712,-1.73501,-1.87707,0.77555,5.12245,-5.70313,-1.97819,-3.76151,-2.84267,-3.93088,-4.06369,-5.19276,-6.58091,-7.55794,-6.37427,-7.74551,-7.65262,-6.15749,-6.8868,-5.74989,-3.9717,-7.44098,-3.14155],"first_mask_logits":[-6.09403,5.47271,0.12616,-2.21769,-6.86007,-7.36398,-4.95865,-8.52597,-3.72217,-5.5905,-9.3338,-5.48707,-4.87782,-5.86645,-7.23206,-7.22732],"tokens":[3,7,11,11,6,7,10,10,6,6,5],"masks":[0,0,0,0,8,0,0,0,4,2,0],"regex":"a{3}b{2}c"},{"pattern":"(0|1(01*0)*1)*","base_ranges":["[^01]","1","0"],"graph":{"node_type":[1,0,0,2],"is_accept":[1,0,0,0],"fwd_depth":[0,1,2,1],"bwd_depth":[0,1,2,65],"to_reject":[1,0,0,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,3,1,0,3,0,2,3,2,1,3,3,3],"edge_dst":[3,1,0,3,0,2,3,2,1,3,3,3,0,0,0,1,1,1,2,2,2,3,3,3]},"first_tok_logits":[-13.0639,-13.3017,-4.42116,7.96541,-0.51918,-1.11682,-0.0041,11.76104,-3.80162,-4.66503,-8.60965,-6.64517,-4.97222,-5.19273,-7.80172,-8.56095,-7.02104,-7.41947,-9.73019,-8.9704,-8.8094,-8.11224,-7.35106,-6.56328,-8.04447,-1.54402],"first_mask_logits":[-5.12028,1.09825,0.29431,-7.74804,-4.70872,-5.7099,-9.82267,-10.25846,-6.60292,-7.79912,-12.36789,-10.26324,-7.19172,-9.12672,-9.35765,-8.13209],"tokens":[7,8,25,4,6,3,6,7,8,25,3,6,7,8,25,6,6,5,6,5,5],"masks":[0,0,0,0,4,0,2,0,0,0,0,4,0,0,0,2,4,0,2,0,0],"regex":"(0|1(01*0)*1)*"},{"pattern":"[ab]*a[ab]{2}","base_ranges":["[^ab]","a","b"],"graph":{"node_type":[1,0,0,0,0,0,0,0,2],"is_accept":[0,0,0,0,1,1,1,1,0],"fwd_depth":[0,1,2,2,3,3,3,3,1],"bwd_depth":[3,2,1,1,0,0,0,0,65],"to_reject":[0,0,0,0,1,1,1,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7,7,7,8,8,8,8,1,0,8,2,3,8,4,5,8,6,7,8,4,5,8,6,7,8,2,3,8,1,0,8,8,8],"edge_dst":[8,1,0,8,2,3,8,4,5,8,6,7,8,4,5,8,6,7,8,2,3,8,1,0,8,8,8,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7,7,7,8,8,8]},"first_tok_logits":[-10.96125,-10.96513,-4.99858,13.76636,0.63047,-1.10159,-0.70392,2.86512,-4.42927,-2.24408,-6.29012,-4.13331,-4.31024,-4.61657,-5.4954,-5.8288,-5.04202,-5.53318,-6.57946,-6.26138,-5.04627,-6.78829,-5.71796,-5.79629,-7.64137,-1.03227],"first_mask_logits":[-4.00797,5.18959,0.8388,-5.92818,-5.38045,-2.82298,-3.46049,-7.34063,-1.64872,-3.48057,-8.45308,-4.01701,-0.85943,-3.41168,-7.67117,-6.02388],"tokens":[3,7,8,25,6,6,7,10,10,6,5],"masks":[0,0,0,0,6,2,0,0,0,6,0],"regex":"[ab]*a[ab]{2}"},{"pattern":".*(abc|def).*","base_ranges":["d","a","b","e","[^a-f]","c","f"],"graph":{"node_type":[1,0,0,0,0,0],"is_accept":[0,0,0,0,0,1],"fwd_depth":[0,1,1,2,2,3],"bwd_depth":[3,2,2,1,1,0],"to_reject":[0,0,0,0,0,65],"edge_type":[0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22],"edge_src":[0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3,4,4,4,4,4,4,4,5,5,5,5,5,5,5,1,2,0,0,0,0,0,1,2,0,3,0,0,0,1,2,4,0,0,0,0,1,2,0,0,0,0,5,1,2,0,0,0,5,0,5,5,5,5,5,5,5],"edge_dst":[1,2,0,0,0,0,0,1,2,0,3,0,0,0,1,2,4,0,0,0,0,1,2,0,0,0,0,5,1,2,0,0,0,5,0,5,5,5,5,5,5,5,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3,4,4,4,4,4,4,4,5,5,5,5,5,5,5]},"first_tok_logits":[-12.65553,-12.74655,-5.13984,11.80739,5.15273,-1.58374,-0.85791,1.94368,-5.54165,-2.21771,-6.29152,-4.76869,-5.47167,-5.59643,-6.08631,-6.23721,-6.19637,-5.5837,-7.47441,-8.34871,-5.5998,-6.97547,-8.91191,-7.6218,-9.07886,-3.02692],"first_mask_logits":[-3.59228,1.61879,0.76087,-1.80642,-1.21213,1.42753,0.33543,-10.42815,-6.67994,-6.87378,-11.46061,-8.9322,-7.65872,-8.29792,-11.38297,-8.01],"tokens":[3,7,8,25,6,4,3,6,6,6,5,3,6,6,6,5,5,7,8,25,6,5],"masks":[0,0,0,0,127,0,0,1,8,64,0,0,2,4,32,0,0,0,0,0,127,0],"regex":".*(def|abc).*"},{"pattern":"(a|bc|def)+","base_ranges":["[^a-f]","e","c","f","d","b","a"],"graph":{"node_type":[1,0,0,0,0,2],"is_accept":[0,0,0,1,0,0],"fwd_depth":[0,1,1,1,2,1],"bwd_depth":[1,2,1,0,1,65],"to_reject":[0,0,0,1,0,0],"edge_type":[0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,0,1,2,3,4,5,6,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22,16,17,18,19,20,21,22],"edge_src":[0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,1,2,3,5,4,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,1,2,3,5,5,5,3,5,5,5,5,5,5,5,5,5,5],"edge_dst":[5,5,5,5,1,2,3,5,4,5,5,5,5,5,5,5,3,5,5,5,5,5,5,5,5,1,2,3,5,5,5,3,5,5,5,5,5,5,5,5,5,5,0,0,0,0,0,0,0,1,1,1,1,1,1,1,2,2,2,2,2,2,2,3,3,3,3,3,3,3,4,4,4,4,4,4,4,5,5,5,5,5,5,5]},"first_tok_logits":[-11.33109,-11.08067,-4.65471,1.86544,1.20304,-0.01543,-0.11027,12.96196,-5.40453,-3.11772,-9.40874,-7.0101,-6.04905,-5.31561,-6.20329,-6.3274,-7.37248,-7.02266,-7.76975,-6.24668,-5.26505,-4.91436,-5.98975,-4.99974,-6.55573,1.61669],"first_mask_logits":[-7.60948,0.84757,-2.79415,-5.16749,-3.08604,-0.39703,-1.80205,-6.73657,-3.91532,-5.42967,-10.23098,-2.55474,-5.00158,-5.30051,-5.30591,-6.83901],"tokens":[7,9,25,4,6,3,6,6,5,3,6,6,6,5,5],"masks":[0,0,0,0,64,0,32,4,0,0,16,2,8,0,0],"regex":"(a|bc|def)+"},{"pattern":"[a-c]*d[e-g]*","base_ranges":["[^a-g]","d","[a-c]","[e-g]"],"graph":{"node_type":[1,0,2],"is_accept":[0,1,0],"fwd_depth":[0,1,1],"bwd_depth":[1,0,65],"to_reject":[0,1,0],"edge_type":[0,1,2,3,0,1,2,3,0,1,2,3,16,17,18,19,16,17,18,19,16,17,18,19],"edge_src":[0,0,0,0,1,1,1,1,2,2,2,2,2,1,0,2,2,2,2,1,2,2,2,2],"edge_dst":[2,1,0,2,2,2,2,1,2,2,2,2,0,0,0,0,1,1,1,1,2,2,2,2]},"first_tok_logits":[-10.69823,-10.9037,-4.26422,15.3646,-1.03419,-0.46999,1.12605,2.99787,-5.80328,-3.08517,-5.89842,-4.32282,-3.6014,-5.10059,-5.75272,-6.16106,-5.18482,-5.52749,-7.94392,-8.14,-6.65795,-6.52782,-6.09518,-5.05562,-7.85472,-2.77668],"first_mask_logits":[-4.77967,6.65272,-2.34785,-4.98112,-7.14698,-5.67201,-5.8272,-9.48286,-5.71634,-4.54101,-10.48449,-7.02211,-5.15526,-5.13966,-10.34113,-8.07941],"tokens":[3,7,8,25,6,6,7,8,25,6,5],"masks":[0,0,0,0,4,2,0,0,0,8,0],"regex":"[a-c]*d[e-g]*"},{"pattern":"(x|y)*z(x|y)*","base_ranges":["[^x-z]","z","[xy]"],"graph":{"node_type":[1,0,2],"is_accept":[0,1,0],"fwd_depth":[0,1,1],"bwd_depth":[1,0,65],"to_reject":[0,1,0],"edge_type":[0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,2,1,0,2,2,1,2,2,2],"edge_dst":[2,1,0,2,2,1,2,2,2,0,0,0,1,1,1,2,2,2]},"first_tok_logits":[-12.21957,-12.35677,-5.1988,13.96476,-0.23691,-1.2202,1.56724,3.9818,-6.03822,-3.4769,-6.16906,-4.33307,-3.56481,-5.03605,-5.80276,-6.34621,-5.40564,-5.96472,-8.13907,-8.43599,-7.32061,-7.59671,-6.59845,-6.2756,-8.63996,-3.22243],"first_mask_logits":[-4.72062,7.97222,-2.56921,-8.529,-8.05163,-6.86083,-7.39953,-10.92507,-7.08645,-6.5186,-12.18146,-9.34705,-6.98082,-5.6734,-10.90263,-8.59921],"tokens":[3,7,8,25,6,6,7,8,25,6,5],"masks":[0,0,0,0,4,2,0,0,0,4,0],"regex":"[xy]*z[xy]*"},{"pattern":"a?b?c?d?","base_ranges":["[^a-d]","a","b","c","d"],"graph":{"node_type":[1,0,0,0,0,2],"is_accept":[1,1,1,1,1,0],"fwd_depth":[0,1,1,1,1,1],"bwd_depth":[0,0,0,0,0,65],"to_reject":[1,1,1,1,1,0],"edge_type":[0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,16,17,18,19,20,16,17,18,19,20,16,17,18,19,20,16,17,18,19,20,16,17,18,19,20,16,17,18,19,20],"edge_src":[0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,5,5,5,5,5,5,1,2,3,4,5,5,2,3,4,5,5,5,3,4,5,5,5,5,4,5,5,5,5,5,5,5,5,5,5],"edge_dst":[5,1,2,3,4,5,5,2,3,4,5,5,5,3,4,5,5,5,5,4,5,5,5,5,5,5,5,5,5,5,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4,4,4,5,5,5,5,5]},"first_tok_logits":[-11.69652,-11.85802,-4.58932,11.58343,6.52953,-2.00652,-0.11551,1.28191,-3.48344,-3.11118,-6.98061,-5.90254,-4.59745,-4.29434,-4.86327,-4.69392,-4.39735,-5.33021,-7.53462,-8.25933,-8.48914,-7.81789,-6.19142,-5.27238,-7.96071,-4.44732],"first_mask_logits":[-6.46973,-2.37635,-1.12461,-3.81416,-3.02138,-7.09571,-5.8228,-8.05391,-3.29975,-2.38992,-8.90888,-7.98504,-4.32629,-6.86682,-9.89822,-9.35999],"tokens":[3,7,8,9,6,7,8,9,6,7,8,9,6,7,8,9,6,5],"masks":[0,0,0,0,2,0,0,0,4,0,0,0,8,0,0,0,16,0],"regex":"a?b?c?d?"},{"pattern":"(aa|bb)*(ab|ba)(aa|bb)*","base_ranges":["[^ab]","a","b"],"graph":{"node_type":[1,0,0,0,0,0,2],"is_accept":[0,0,0,1,0,0,0],"fwd_depth":[0,1,1,2,3,3,1],"bwd_depth":[2,1,1,0,1,1,65],"to_reject":[0,0,0,1,0,0,0],"edge_type":[0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18,16,17,18],"edge_src":[0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,6,1,2,6,0,3,6,3,0,6,4,5,6,3,6,6,6,3,6,6,6],"edge_dst":[6,1,2,6,0,3,6,3,0,6,4,5,6,3,6,6,6,3,6,6,6,0,0,0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6]},"first_tok_logits":[-14.37455,-14.3687,-5.98592,10.8281,-0.08498,-1.70962,0.17135,7.12931,-4.94735,-3.47491,-5.72251,-3.13612,-3.61599,-3.36436,-5.93514,-6.41683,-6.48859,-5.63936,-8.71801,-8.14503,-8.68627,-8.64662,-8.14459,-7.11091,-10.49281,-3.30572],"first_mask_logits":[-3.76618,3.11097,0.27597,-4.67463,-6.71554,-5.78187,-7.04555,-9.43536,-4.99051,-5.19971,-10.68163,-10.04079,-4.70412,-7.85694,-9.65294,-8.88587],"tokens":[3,7,8,25,4,3,6,6,5,3,6,6,5,5,4,3,6,6,5,3,6,6,5,5,7,8,25,4,3,6,6,5,3,6,6,5,5,5],"masks":[0,0,0,0,0,0,6,2,0,0,4,4,0,0,0,0,2,4,0,0,4,2,0,0,0,0,0,0,0,2,2,0,0,4,4,0,0,0],"regex":"([ab]a|bb)*(ab|ba)(aa|bb)*"}] \ No newline at end of file diff --git a/tests/data/kleene_tiny/config.json b/tests/data/kleene_tiny/config.json new file mode 100644 index 0000000..f123149 --- /dev/null +++ b/tests/data/kleene_tiny/config.json @@ -0,0 +1,51 @@ +{ + "format": "kleene", + "format_version": 1, + "limits": { + "max_bases": 16 + }, + "model_config": { + "d_model": 16, + "dec_layers": 1, + "edge_dim": 4, + "enc_global_layers": 1, + "enc_layers": 1, + "ffn": 32, + "max_bases": 16, + "max_depth": 64, + "max_len": 8, + "n_heads": 2, + "vocab": { + "ALT": 4, + "BOS": 1, + "CHAR": 6, + "CONCAT": 3, + "COUNT_0": 8, + "COUNT_1": 9, + "COUNT_10": 18, + "COUNT_11": 19, + "COUNT_12": 20, + "COUNT_13": 21, + "COUNT_14": 22, + "COUNT_15": 23, + "COUNT_16": 24, + "COUNT_2": 10, + "COUNT_3": 11, + "COUNT_4": 12, + "COUNT_5": 13, + "COUNT_6": 14, + "COUNT_7": 15, + "COUNT_8": 16, + "COUNT_9": 17, + "COUNT_INF": 25, + "END": 5, + "EOS": 2, + "PAD": 0, + "REPEAT": 7 + } + }, + "name": "kleene-tiny", + "trained_on": { + "max_states": 16 + } +} diff --git a/tests/data/kleene_tiny/model.safetensors b/tests/data/kleene_tiny/model.safetensors new file mode 100644 index 0000000..5240c9b Binary files /dev/null and b/tests/data/kleene_tiny/model.safetensors differ diff --git a/tests/neural_synthesis.rs b/tests/neural_synthesis.rs new file mode 100644 index 0000000..4129a36 --- /dev/null +++ b/tests/neural_synthesis.rs @@ -0,0 +1,80 @@ +//! Neural synthesis end to end, with a model of the real format but tiny and +//! untrained (`tests/data/kleene_tiny`, written by +//! `kleene::tests::write_tiny_model`): its proposals are mostly wrong, which +//! the conversions must catch, but it runs the whole inference, from a single +//! automaton to the batches of decomposition. +//! +//! The model is embedded rather than read, so the tests need no filesystem: +//! CI runs them on `wasm32-unknown-unknown` in a module with no imports at +//! all, so with no clock and no source of randomness. + +#![cfg(feature = "neural-synthesis")] + +use regexsolver::fast_automaton::FastAutomaton; +use regexsolver::neural_synthesis::{Device, NeuralSynthesizer}; +use regexsolver::regex::RegularExpression; + +fn synthesizer() -> NeuralSynthesizer { + NeuralSynthesizer::from_bytes( + include_bytes!("data/kleene_tiny/config.json"), + include_bytes!("data/kleene_tiny/model.safetensors").to_vec(), + Device::Cpu, + ) + .unwrap() +} + +fn automaton(pattern: &str) -> FastAutomaton { + RegularExpression::new(pattern) + .unwrap() + .to_automaton() + .unwrap() +} + +fn assert_keeps_the_language(regex: &RegularExpression, automaton: &FastAutomaton) { + assert!( + regex.to_automaton().unwrap().equivalent(automaton).unwrap(), + "{regex}" + ); +} + +#[test] +fn proposes_for_automata_the_model_takes() { + let sampled = synthesizer(); + let greedy = synthesizer().with_candidates(1); + assert_eq!(sampled.model_name(), "kleene-tiny"); + for pattern in ["abc", "(ab|cd)*e", "[a-c]*d[e-g]*", "(a|b)*abb"] { + let automaton = automaton(pattern); + for synthesizer in [&sampled, &greedy] { + if let Some(proposed) = synthesizer.propose(&automaton).unwrap() { + assert_keeps_the_language(&proposed, &automaton); + } + let regex = automaton.to_regex_with(synthesizer).unwrap(); + assert_keeps_the_language(®ex, &automaton); + } + } + + let automata: Vec = ["x*y", "(xy)+", "x|yz"].map(automaton).into(); + let batch: Vec<&FastAutomaton> = automata.iter().collect(); + for (proposed, automaton) in sampled.propose_batch(&batch).unwrap().iter().zip(&automata) { + if let Some(proposed) = proposed { + assert_keeps_the_language(proposed, automaton); + } + } +} + +#[test] +fn decomposes_automata_beyond_the_model() { + let synthesizer = synthesizer(); + let mut intersection = automaton(".*abc.*"); + for other in [".*def.*", ".*gh.*"] { + intersection = intersection.intersection(&automaton(other)).unwrap(); + } + let mut dfa = intersection.determinize().unwrap().into_owned(); + dfa.minimize().unwrap(); + assert!(dfa.number_of_states() > synthesizer.max_states()); + + let regex = dfa.to_regex_with(&synthesizer).unwrap(); + assert_keeps_the_language(®ex, &dfa); + // State elimination's regex is a candidate, so the result is no worse. + assert!(regex.evaluate_complexity() <= dfa.to_regex().unwrap().evaluate_complexity()); +} diff --git a/tests/public_api.rs b/tests/public_api.rs index 15859e0..37c8e5f 100644 --- a/tests/public_api.rs +++ b/tests/public_api.rs @@ -455,3 +455,72 @@ fn errors_are_comparable_cloneable_and_non_exhaustive() { }; assert_eq!("bounds", described); } + +/// `to_regex_with` takes any `RegexSynthesizer`, the crate's or a dependent's +/// own, concrete or as a trait object. +#[test] +fn to_regex_with_takes_any_synthesizer() { + use regexsolver::fast_automaton::{RegexSynthesizer, StateElimination}; + + /// A dependent's own: state elimination's regex, simplified. + struct Simplified; + impl RegexSynthesizer for Simplified { + fn synthesize(&self, automaton: &FastAutomaton) -> Result { + Ok(automaton.to_regex()?.simplify()) + } + } + + let automaton = RegularExpression::new("(ab|cd)*e") + .unwrap() + .to_automaton() + .unwrap(); + assert_eq!( + automaton.to_regex_with(&StateElimination).unwrap(), + automaton.to_regex().unwrap() + ); + let synthesizers: [&dyn RegexSynthesizer; 2] = [&StateElimination, &Simplified]; + for synthesizer in synthesizers { + let regex = automaton.to_regex_with(synthesizer).unwrap(); + assert!( + regex + .to_automaton() + .unwrap() + .equivalent(&automaton) + .unwrap() + ); + + let converted = Term::Automaton(automaton.clone()); + assert_eq!( + converted.to_regex_with(synthesizer).unwrap().into_owned(), + regex + ); + let regex_backed = term("abc"); + assert!(matches!( + regex_backed.to_regex_with(synthesizer).unwrap(), + Cow::Borrowed(_) + )); + } +} + +#[cfg(feature = "neural-synthesis")] +#[test] +fn neural_synthesis_refuses_unusable_models() { + use regexsolver::neural_synthesis::{Device, NeuralSynthesisError, NeuralSynthesizer}; + + let config = br#"{"name": "x", "format": "other", "format_version": 1}"#; + let error = NeuralSynthesizer::from_bytes(config, vec![], Device::Cpu).unwrap_err(); + assert_eq!( + NeuralSynthesisError::UnsupportedFormat { + format: "other".to_string(), + version: 1 + }, + error + ); + assert_eq!(error, error.clone()); + assert!(!error.to_string().is_empty()); + + assert!(matches!( + NeuralSynthesizer::from_dir("no-such-model", Device::Cpu), + Err(NeuralSynthesisError::Io(_)) + )); +}